diff --git a/client/package-lock.json b/client/package-lock.json index b31659d..57b07f7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -30,6 +30,8 @@ "i18next-http-backend": "^2.5.2", "lodash": "^4.17.21", "react": "^18.3.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-i18next": "^15.0.0", "react-router-dom": "^6.26.0", @@ -1939,6 +1941,21 @@ "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "node_modules/@refinedev/antd": { "version": "5.42.0", "resolved": "https://registry.npmjs.org/@refinedev/antd/-/antd-5.42.0.tgz", @@ -4236,6 +4253,16 @@ "node": ">=8" } }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -4847,8 +4874,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-equals": { "version": "2.0.4", @@ -8128,6 +8154,43 @@ "node": ">=0.10.0" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -8300,6 +8363,14 @@ "esprima": "~4.0.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", diff --git a/client/package.json b/client/package.json index f71ddab..38f8c3a 100644 --- a/client/package.json +++ b/client/package.json @@ -29,6 +29,8 @@ "i18next-http-backend": "^2.5.2", "lodash": "^4.17.21", "react": "^18.3.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-i18next": "^15.0.0", "react-router-dom": "^6.26.0", @@ -70,4 +72,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/client/src/App.tsx b/client/src/App.tsx index 788e184..0e6422a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,6 +10,7 @@ import { HighlightOutlined, HomeOutlined, QuestionOutlined, + TableOutlined, ToolOutlined, UserOutlined, } from "@ant-design/icons"; @@ -148,6 +149,14 @@ function App() { icon: , }, }, + { + name: "locations", + list: "/locations", + meta: { + canDelete: false, + icon: , + }, + }, { name: "settings", list: "/settings", @@ -222,6 +231,7 @@ function App() { } /> } /> + } /> } /> diff --git a/client/src/pages/locations/functions.ts b/client/src/pages/locations/functions.ts new file mode 100644 index 0000000..4bb91af --- /dev/null +++ b/client/src/pages/locations/functions.ts @@ -0,0 +1,18 @@ +import { getAPIURL } from "../../utils/url"; +import { ISpool } from "../spools/model"; + +export async function setSpoolLocation(spool_id: number, location: string | null): Promise { + const response = await fetch(getAPIURL() + "/spool/" + spool_id, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + location: location, + }), + }); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); +} diff --git a/client/src/pages/locations/index.tsx b/client/src/pages/locations/index.tsx new file mode 100644 index 0000000..c628df7 --- /dev/null +++ b/client/src/pages/locations/index.tsx @@ -0,0 +1,151 @@ +import { IResourceComponentsProps, useInvalidate, useList, useTranslate } from "@refinedev/core"; +import { theme } from "antd"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import React from "react"; +import { DndProvider, useDrag, useDrop } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; + +import SpoolIcon from "../../components/spoolIcon"; +import { ISpool } from "../spools/model"; +import { setSpoolLocation } from "./functions"; +import "./locations.css"; + +dayjs.extend(utc); + +const { useToken } = theme; + +function SpoolCard({ spool }: { spool: ISpool }) { + const { token } = useToken(); + const [{ opacity }, dragRef] = useDrag( + () => ({ + type: "spool", + item: spool, + collect: (monitor) => ({ + opacity: monitor.isDragging() ? 0.5 : 1, + }), + }), + [] + ); + + const colorObj = spool.filament.multi_color_hexes + ? { + colors: spool.filament.multi_color_hexes.split(","), + vertical: spool.filament.multi_color_direction === "longitudinal", + } + : spool.filament.color_hex || "#000000"; + + let filament_name: string; + if (spool.filament.vendor && "name" in spool.filament.vendor) { + filament_name = `${spool.filament.vendor.name} - ${spool.filament.name}`; + } else { + filament_name = spool.filament.name ?? spool.filament.id.toString(); + } + + const style = { + opacity, + backgroundColor: token.colorBgContainerDisabled, + }; + + return ( + + + + + #{spool.id} {filament_name} + + + {spool.remaining_weight} / {spool.filament.weight} g + + + + ); +} + +function LocationContainer({ title, spools }: { title: string; spools: ISpool[] }) { + const { token } = useToken(); + const invalidate = useInvalidate(); + + const [, drop] = useDrop(() => ({ + accept: "spool", + drop: (item: ISpool) => { + setSpoolLocation(item.id, title).then(() => { + invalidate({ + resource: "spool", + id: item.id, + invalidates: ["list", "detail"], + }); + }); + }, + })); + + const style = { + backgroundColor: token.colorBgContainer, + borderRadius: token.borderRadiusLG, + }; + + return ( + + {title} + + {spools.map((spool) => ( + + ))} + + + ); +} + +export const Locations: React.FC = () => { + const t = useTranslate(); + + const { data, isLoading, isError } = useList({ + resource: "spool", + meta: { + queryParams: { + ["allow_archived"]: false, + }, + }, + pagination: { + mode: "off", + }, + }); + + if (isLoading) { + return Loading...; + } + + if (isError) { + return Failed to load spools; + } + + const spools = data?.data ?? []; + spools.sort((a, b) => a.id - b.id); + + // Locations is all the unique locations of the spools + const spoolLocations: Record = {}; + spools.forEach((spool) => { + const loc = spool.location ?? t("spool.no_location"); + if (!spoolLocations[loc]) { + spoolLocations[loc] = []; + } + spoolLocations[loc].push(spool); + }); + + const locationsList = Object.keys(spoolLocations); + locationsList.sort((a, b) => { + return a.localeCompare(b); + }); + + const containers = locationsList.map((loc) => { + return ; + }); + + return ( + + {containers} + + ); +}; + +export default Locations; diff --git a/client/src/pages/locations/locations.css b/client/src/pages/locations/locations.css new file mode 100644 index 0000000..76abccc --- /dev/null +++ b/client/src/pages/locations/locations.css @@ -0,0 +1,40 @@ +.loc-metacontainer { + display: flex; + flex-wrap: wrap; +} + +.loc-container { + padding: 1em; + width: 20em; +} + +.loc-container .loc-spools { + padding: 0.2em; + display: flex; + flex-direction: column; + gap: 0.5em; + max-height: 100em; + overflow-y: scroll; +} + +.loc-container .spool { + padding: 0.5em 0.5em 0.5em 0; + display: flex; + align-items: center; + cursor: pointer; + border-radius: 0.5em; +} + +.loc-container .spool .info { + display: flex; + flex-direction: column; + gap: 0.5em; +} + +.loc-container .spool .info .title { + font-size: 1em; +} + +.loc-container .spool .info .subtitle { + font-size: 0.8em; +}