From 3de2c9409856be655f119f8b271b588164649145 Mon Sep 17 00:00:00 2001 From: Donkie Date: Sun, 12 May 2024 21:38:59 +0200 Subject: [PATCH] Create internal filament from external flow done --- client/public/locales/en/common.json | 7 +- client/src/pages/filaments/functions.ts | 38 ++++++++ client/src/pages/filaments/model.tsx | 1 + client/src/pages/filaments/show.tsx | 2 + client/src/pages/spools/create.tsx | 120 +++++++++++++++++------- client/src/pages/vendors/functions.ts | 48 ++++++++++ client/src/pages/vendors/model.tsx | 1 + client/src/pages/vendors/show.tsx | 2 + client/src/utils/queryExternalDB.ts | 6 +- 9 files changed, 187 insertions(+), 38 deletions(-) create mode 100644 client/src/pages/filaments/functions.ts create mode 100644 client/src/pages/vendors/functions.ts diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 357d2d9..e30c352 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -136,6 +136,8 @@ "id": "ID", "filament_name": "Filament", "filament": "Filament", + "filament_internal": "Internal", + "filament_external": "External", "price": "Price", "material": "Material", "weight_to_use": "Weight", @@ -163,7 +165,8 @@ "initial_weight": "The initial weight of filament on the spool (net weight). Will use the weight from the filament object if not set.", "spool_weight": "The weight of the spool when it is empty. Leave empty to use the value from the filament or the manufacturer.", "location": "Where the spool is located if you have multiple locations where you store your spools.", - "lot_nr": "Manufacturer's lot number. Can be used to ensure a print has consistent color if multiple spools are used." + "lot_nr": "Manufacturer's lot number. Can be used to ensure a print has consistent color if multiple spools are used.", + "external_filament": "You have selected a filament from the external database. A filament object (and possibly a manufacturer object) will be created automatically when you create this spool. This can create duplicate filament objects if you have already created a filament object for this filament." }, "titles": { "create": "Create Spool", @@ -201,6 +204,7 @@ "settings_extruder_temp": "Extruder Temp", "settings_bed_temp": "Bed Temp", "color_hex": "Color", + "external_id": "External ID", "spools": "Show Spools" }, "fields_help": { @@ -232,6 +236,7 @@ "id": "ID", "name": "Name", "empty_spool_weight": "Empty Spool Weight", + "external_id": "External ID", "registered": "Registered", "comment": "Comment" }, diff --git a/client/src/pages/filaments/functions.ts b/client/src/pages/filaments/functions.ts new file mode 100644 index 0000000..c47a7fb --- /dev/null +++ b/client/src/pages/filaments/functions.ts @@ -0,0 +1,38 @@ +import { ExternalFilament } from "../../utils/queryExternalDB"; +import { getAPIURL } from "../../utils/url"; +import { getOrCreateVendorFromExternal } from "../vendors/functions"; +import { IFilament } from "./model"; + +/** + * Create a new internal filament given an external filament object. + * Returns the created internal filament. + */ +export async function createFilamentFromExternal(externalFilament: ExternalFilament): Promise { + const vendor = await getOrCreateVendorFromExternal(externalFilament.manufacturer); + + const body: Omit & { vendor_id: number } = { + name: externalFilament.name, + material: externalFilament.material, + vendor_id: vendor.id, + density: externalFilament.density, + diameter: externalFilament.diameter, + weight: externalFilament.weight, + spool_weight: externalFilament.spool_weight || undefined, + color_hex: externalFilament.color_hex, + settings_extruder_temp: externalFilament.extruder_temp || undefined, + settings_bed_temp: externalFilament.bed_temp || undefined, + external_id: externalFilament.id, + }; + + const response = await fetch(getAPIURL() + "/filament", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); +} diff --git a/client/src/pages/filaments/model.tsx b/client/src/pages/filaments/model.tsx index 20b693f..2ebb728 100644 --- a/client/src/pages/filaments/model.tsx +++ b/client/src/pages/filaments/model.tsx @@ -16,6 +16,7 @@ export interface IFilament { settings_extruder_temp?: number; settings_bed_temp?: number; color_hex?: string; + external_id?: string; extra: { [key: string]: string }; } diff --git a/client/src/pages/filaments/show.tsx b/client/src/pages/filaments/show.tsx index 567cafb..2640094 100644 --- a/client/src/pages/filaments/show.tsx +++ b/client/src/pages/filaments/show.tsx @@ -143,6 +143,8 @@ export const FilamentShow: React.FC = () => { )} {t("filament.fields.article_number")} + {t("filament.fields.external_id")} + {t("filament.fields.comment")} {t("settings.extra_fields.tab")} diff --git a/client/src/pages/spools/create.tsx b/client/src/pages/spools/create.tsx index a686b5f..aea54e7 100644 --- a/client/src/pages/spools/create.tsx +++ b/client/src/pages/spools/create.tsx @@ -1,7 +1,20 @@ import React, { useState } from "react"; import { HttpError, IResourceComponentsProps, useTranslate } from "@refinedev/core"; import { Create, useForm, useSelect } from "@refinedev/antd"; -import { Form, Input, DatePicker, Select, InputNumber, Radio, Divider, Button, Typography } from "antd"; +import { + Form, + Input, + DatePicker, + Select, + InputNumber, + Radio, + Divider, + Button, + Typography, + ConfigProvider, + Alert, + Space, +} from "antd"; import dayjs from "dayjs"; import TextArea from "antd/es/input/TextArea"; import { IFilament } from "../filaments/model"; @@ -14,7 +27,8 @@ import { EntityType, useGetFields } from "../../utils/queryFields"; import { ExtraFieldFormItem, StringifiedExtras } from "../../components/extraFields"; import utc from "dayjs/plugin/utc"; import { getCurrencySymbol, useCurrency } from "../../utils/settings"; -import { useGetExternalDBFilaments } from "../../utils/queryExternalDB"; +import { ExternalFilament, useGetExternalDBFilaments } from "../../utils/queryExternalDB"; +import { createFilamentFromExternal } from "../filaments/functions"; dayjs.extend(utc); @@ -22,6 +36,10 @@ interface CreateOrCloneProps { mode: "create" | "clone"; } +type ISpoolRequest = Omit & { + filament_id: number | string; +}; + function formatFilamentLabel(name: string, diameter: number, vendorName?: string, material?: string, weight?: number) { const portions = []; if (vendorName) { @@ -36,7 +54,7 @@ function formatFilamentLabel(name: string, diameter: number, vendorName?: string if (weight) { extras.push(formatWeight(weight)); } - return `${portions.join(" - ")} (${extras.join(" ")})`; + return `${portions.join(" - ")} (${extras.join(", ")})`; } /** @@ -57,7 +75,7 @@ export const SpoolCreate: React.FC({ redirect: false, @@ -91,6 +109,17 @@ export const SpoolCreate: React.FC { const values = StringifiedExtras(await form.validateFields()); + if (typeof values.filament_id === "string") { + // Filament ID being a string indicates its an external filament. + // If so, we should first create the internal filament version, then create the spool(s) + const externalFilament = externalFilaments.data?.find((f) => f.id === values.filament_id); + if (!externalFilament) { + throw new Error("Unknown external filament"); + } + const internalFilament = await createFilamentFromExternal(externalFilament); + values.filament_id = internalFilament.id; + } + if (quantity > 1) { const submit = Array(quantity).fill(values); // queue multiple creates this way for now Refine doesn't seem to map Arrays to createMany or multiple creates like it says it does @@ -98,7 +127,8 @@ export const SpoolCreate: React.FC({ @@ -116,7 +146,19 @@ export const SpoolCreate: React.FC { + // + // Set up the filament selection options + // + + interface SelectOption { + label: string; + value: string | number; + weight?: number; + spool_weight?: number; + is_internal: boolean; + } + + const filamentSelectInternal: SelectOption[] | undefined = filamentsQuery.data?.data.map((item) => { return { label: formatFilamentLabel( item.name ?? `ID ${item.id}`, @@ -128,61 +170,71 @@ export const SpoolCreate: React.FC a.label.localeCompare(b.label, undefined, { sensitivity: "base" })); - const filamentSelectExternal = externalFilaments.data?.map((item) => { + const filamentSelectExternal: SelectOption[] | undefined = externalFilaments.data?.map((item) => { return { label: formatFilamentLabel(item.name, item.diameter, item.manufacturer, item.material, item.weight), value: item.id, weight: item.weight, - spool_weight: item.spool_weight, + spool_weight: item.spool_weight || undefined, + is_internal: false, }; }); filamentSelectExternal?.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" })); const filamentSelectOptions = [ { - title: "Internal", - label: Internal, + label: {t("spool.fields.filament_internal")}, options: filamentSelectInternal ?? [], }, { - title: "External", - label: External, + label: {t("spool.fields.filament_external")}, options: filamentSelectExternal ?? [], }, ]; + const selectedFilamentID: number | string | undefined = Form.useWatch("filament_id", form); + let selectedFilament: SelectOption | null = null; + // selectedFilamentID is a number of it's an internal filament, and a string of it's an external filament. + if (typeof selectedFilamentID === "number") { + selectedFilament = + filamentSelectInternal?.find((obj) => { + return obj.value === selectedFilamentID; + }) ?? null; + } else if (typeof selectedFilamentID === "string") { + selectedFilament = + filamentSelectExternal?.find((obj) => { + return obj.value === selectedFilamentID; + }) ?? null; + } + + // + // Weight calculations + // + const [weightToEnter, setWeightToEnter] = useState(1); const [usedWeight, setUsedWeight] = useState(0); - const selectedFilamentID = Form.useWatch("filament_id", form); - const selectedFilament = filamentSelectInternal?.find((obj) => { - return obj.value === selectedFilamentID; - }); - - const filamentChange = (newID: number) => { - const newSelectedFilament = filamentSelectInternal?.find((obj) => { - return obj.value === newID; - }); - + React.useEffect(() => { const initial_weight = initialWeightValue ?? 0; const spool_weight = spoolWeightValue ?? 0; - const newFilamentWeight = newSelectedFilament?.weight || 0; - const newSpoolWeight = newSelectedFilament?.spool_weight || 0; + const newFilamentWeight = selectedFilament?.weight || 0; + const newSpoolWeight = selectedFilament?.spool_weight || 0; - const currentCalculatedFilamentWeight = getTotalWeightFromFilament(); - if ((initial_weight === 0 || initial_weight === currentCalculatedFilamentWeight) && newFilamentWeight > 0) { + const currentCalculatedFilamentWeight = selectedFilament ? calcTotalWeight(selectedFilament) : 0; + if (newFilamentWeight > 0) { form.setFieldValue("initial_weight", newFilamentWeight); } - if ((spool_weight === 0 || spool_weight === (selectedFilament?.spool_weight ?? 0)) && newSpoolWeight > 0) { + if (newSpoolWeight > 0) { form.setFieldValue("spool_weight", newSpoolWeight); } - }; + }, [selectedFilament]); const weightChange = (weight: number) => { setUsedWeight(weight); @@ -222,9 +274,9 @@ export const SpoolCreate: React.FC { - return (selectedFilament?.weight ?? 0) + (selectedFilament?.spool_weight ?? 0); - }; + function calcTotalWeight(filament: SelectOption): number { + return (filament.weight ?? 0) + (filament.spool_weight ?? 0); + } const getMeasuredWeight = (): number => { const grossWeight = getGrossWeight(); @@ -343,11 +395,11 @@ export const SpoolCreate: React.FC typeof option?.label === "string" && selectSearchMatches(input, option?.label) } - onChange={(value) => { - filamentChange(value); - }} /> + {selectedFilament?.is_internal === false && ( + + )} { + // Make a search using GET and query params + const response = await fetch(`${getAPIURL()}/vendor?${new URLSearchParams({ external_id })}`); + if (!response.ok) { + return null; + } + + const data: IVendor[] = await response.json(); + if (data.length === 0) { + return null; + } + + return data[0]; +} + +/** + * Create a new internal filament given an external filament object. + * Returns the created internal filament. + */ +export async function getOrCreateVendorFromExternal(vendor_external_id: string): Promise { + const existingVendor = await getVendorByExternalID(vendor_external_id); + if (existingVendor) { + return existingVendor; + } + + const body: Omit = { + name: vendor_external_id, + external_id: vendor_external_id, + }; + + const response = await fetch(getAPIURL() + "/vendor", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); +} diff --git a/client/src/pages/vendors/model.tsx b/client/src/pages/vendors/model.tsx index 85d33ad..e64440e 100644 --- a/client/src/pages/vendors/model.tsx +++ b/client/src/pages/vendors/model.tsx @@ -4,6 +4,7 @@ export interface IVendor { name: string; comment?: string; empty_spool_weight?: number; + external_id?: string; extra: { [key: string]: string }; } diff --git a/client/src/pages/vendors/show.tsx b/client/src/pages/vendors/show.tsx index 760eae3..0cee3a0 100644 --- a/client/src/pages/vendors/show.tsx +++ b/client/src/pages/vendors/show.tsx @@ -44,6 +44,8 @@ export const VendorShow: React.FC = () => { {t("vendor.fields.empty_spool_weight")} + {t("vendor.fields.external_id")} + {t("settings.extra_fields.tab")} {extraFields?.data?.map((field, index) => ( diff --git a/client/src/utils/queryExternalDB.ts b/client/src/utils/queryExternalDB.ts index b3348f2..247e08a 100644 --- a/client/src/utils/queryExternalDB.ts +++ b/client/src/utils/queryExternalDB.ts @@ -1,12 +1,12 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getAPIURL } from "./url"; -interface ExternalFilament { +export interface ExternalFilament { id: string; manufacturer: string; name: string; material: string; - density: number | null; + density: number; weight: number; spool_weight: number | null; diameter: number; @@ -15,7 +15,7 @@ interface ExternalFilament { bed_temp: number | null; } -interface ExternalMaterial { +export interface ExternalMaterial { material: string; density: number; }