Create internal filament from external flow done

This commit is contained in:
Donkie
2024-05-12 21:38:59 +02:00
parent e4d2ecf0dc
commit 3de2c94098
9 changed files with 187 additions and 38 deletions

View File

@@ -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<IFilament> {
const vendor = await getOrCreateVendorFromExternal(externalFilament.manufacturer);
const body: Omit<IFilament, "id" | "registered" | "extra"> & { 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();
}

View File

@@ -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 };
}

View File

@@ -143,6 +143,8 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
)}
<Title level={5}>{t("filament.fields.article_number")}</Title>
<TextField value={record?.article_number} />
<Title level={5}>{t("filament.fields.external_id")}</Title>
<TextField value={record?.external_id} />
<Title level={5}>{t("filament.fields.comment")}</Title>
<TextField value={enrichText(record?.comment)} />
<Title level={4}>{t("settings.extra_fields.tab")}</Title>

View File

@@ -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<ISpoolParsedExtras, "id" | "registered"> & {
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<IResourceComponentsProps & CreateOrCloneProps
const { form, formProps, formLoading, onFinish, redirect } = useForm<
ISpool,
HttpError,
ISpoolParsedExtras,
ISpoolRequest,
ISpoolParsedExtras
>({
redirect: false,
@@ -91,6 +109,17 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
const handleSubmit = async (redirectTo: "list" | "edit" | "create") => {
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<IResourceComponentsProps & CreateOrCloneProps
} else {
await onFinish(values);
}
redirect(redirectTo, (values as ISpool).id);
redirect(redirectTo);
};
const { queryResult: filamentsQuery } = useSelect<IFilament>({
@@ -116,7 +146,19 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
});
}, [form, extraFields.data, formProps.initialValues]);
const filamentSelectInternal = filamentsQuery.data?.data.map((item) => {
//
// 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<IResourceComponentsProps & CreateOrCloneProps
value: item.id,
weight: item.weight,
spool_weight: item.spool_weight,
is_internal: true,
};
});
filamentSelectInternal?.sort((a, b) => 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: <span>Internal</span>,
label: <span>{t("spool.fields.filament_internal")}</span>,
options: filamentSelectInternal ?? [],
},
{
title: "External",
label: <span>External</span>,
label: <span>{t("spool.fields.filament_external")}</span>,
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<IResourceComponentsProps & CreateOrCloneProps
return net_weight + spool_weight;
};
const getTotalWeightFromFilament = (): number => {
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<IResourceComponentsProps & CreateOrCloneProps
filterOption={(input, option) =>
typeof option?.label === "string" && selectSearchMatches(input, option?.label)
}
onChange={(value) => {
filamentChange(value);
}}
/>
</Form.Item>
{selectedFilament?.is_internal === false && (
<Alert message={t("spool.fields_help.external_filament")} type="info" />
)}
<Form.Item
label={t("spool.fields.price")}
help={t("spool.fields_help.price")}

48
client/src/pages/vendors/functions.ts vendored Normal file
View File

@@ -0,0 +1,48 @@
import { getAPIURL } from "../../utils/url";
import { IVendor } from "./model";
/**
* Get a vendor given its external ID.
*/
export async function getVendorByExternalID(external_id: string): Promise<IVendor | null> {
// 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<IVendor> {
const existingVendor = await getVendorByExternalID(vendor_external_id);
if (existingVendor) {
return existingVendor;
}
const body: Omit<IVendor, "id" | "registered" | "extra"> = {
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();
}

View File

@@ -4,6 +4,7 @@ export interface IVendor {
name: string;
comment?: string;
empty_spool_weight?: number;
external_id?: string;
extra: { [key: string]: string };
}

View File

@@ -44,6 +44,8 @@ export const VendorShow: React.FC<IResourceComponentsProps> = () => {
<TextField value={enrichText(record?.comment)} />
<Title level={5}>{t("vendor.fields.empty_spool_weight")}</Title>
<TextField value={record?.empty_spool_weight} />
<Title level={5}>{t("vendor.fields.external_id")}</Title>
<TextField value={record?.external_id} />
<Title level={4}>{t("settings.extra_fields.tab")}</Title>
{extraFields?.data?.map((field, index) => (
<ExtraFieldDisplay key={index} field={field} value={record?.extra[field.key]} />