/ client / src / pages / spools / functions.tsx
functions.tsx
  1  import { useSelect, useTranslate } from "@refinedev/core";
  2  import { useQueries } from "@tanstack/react-query";
  3  import { Form, InputNumber, Modal, Radio } from "antd";
  4  import { useForm } from "antd/es/form/Form";
  5  import type { InputNumberRef } from "rc-input-number";
  6  import { useCallback, useMemo, useRef, useState } from "react";
  7  import { formatLength, formatWeight } from "../../utils/parsing";
  8  import { SpoolType, useGetExternalDBFilaments } from "../../utils/queryExternalDB";
  9  import { getAPIURL } from "../../utils/url";
 10  import { IFilament } from "../filaments/model";
 11  import { ISpool } from "./model";
 12  
 13  export async function setSpoolArchived(spool: ISpool, archived: boolean) {
 14    const init: RequestInit = {
 15      method: "PATCH",
 16      headers: {
 17        "Content-Type": "application/json",
 18      },
 19      body: JSON.stringify({
 20        archived: archived,
 21      }),
 22    };
 23    const request = new Request(getAPIURL() + "/spool/" + spool.id);
 24    await fetch(request, init);
 25  }
 26  
 27  /**
 28   * Use some spool filament from this spool. Either specify length or weight.
 29   * @param spool The spool
 30   * @param length The length to add/subtract from the spool, in mm
 31   * @param weight The weight to add/subtract from the spool, in g
 32   */
 33  export async function useSpoolFilament(spool: ISpool, length?: number, weight?: number) {
 34    const init: RequestInit = {
 35      method: "PUT",
 36      headers: {
 37        "Content-Type": "application/json",
 38      },
 39      body: JSON.stringify({
 40        use_length: length,
 41        use_weight: weight,
 42      }),
 43    };
 44    const request = new Request(`${getAPIURL()}/spool/${spool.id}/use`);
 45    await fetch(request, init);
 46  }
 47  
 48  /**
 49   * Adjust usage based on the spool's current gross weight
 50   * @param spool The spool
 51   * @param weight The weight of the spool, in g
 52   */
 53  export async function useSpoolFilamentMeasure(spool: ISpool, weight: number) {
 54    const init: RequestInit = {
 55      method: "PUT",
 56      headers: {
 57        "Content-Type": "application/json",
 58      },
 59      body: JSON.stringify({
 60        weight: weight,
 61      }),
 62    };
 63    const request = new Request(`${getAPIURL()}/spool/${spool.id}/measure`);
 64    await fetch(request, init);
 65  }
 66  
 67  /**
 68   * Returns an array of queries using the useQueries hook from @tanstack/react-query.
 69   * Each query fetches a spool by its ID from the server.
 70   *
 71   * @param {number[]} ids - An array of spool IDs to fetch.
 72   * @return An array of query results, each containing the fetched spool data.
 73   */
 74  export function useGetSpoolsByIds(ids: number[]) {
 75    return useQueries({
 76      queries: ids.map((id) => {
 77        return {
 78          queryKey: ["spool", id],
 79          queryFn: async () => {
 80            const res = await fetch(getAPIURL() + "/spool/" + id);
 81            return (await res.json()) as ISpool;
 82          },
 83        };
 84      }),
 85    });
 86  }
 87  
 88  /**
 89   * Formats a filament label with the given parameters.
 90   */
 91  export function formatFilamentLabel(
 92    name: string,
 93    diameter: number,
 94    vendorName?: string,
 95    material?: string,
 96    weight?: number,
 97    spoolType?: SpoolType,
 98  ): string {
 99    const portions = [];
100    if (vendorName) {
101      portions.push(vendorName);
102    }
103    portions.push(name);
104    const extras = [];
105    if (material) {
106      extras.push(material);
107    }
108    extras.push(formatLength(diameter));
109    if (weight) {
110      extras.push(formatWeight(weight));
111    }
112    if (spoolType) {
113      extras.push(spoolType.charAt(0).toUpperCase() + spoolType.slice(1) + " spool");
114    }
115    return `${portions.join(" - ")} (${extras.join(", ")})`;
116  }
117  
118  interface SelectOption {
119    label: string;
120    value: string | number;
121    weight?: number;
122    spool_weight?: number;
123    is_internal: boolean;
124  }
125  
126  export function useGetFilamentSelectOptions() {
127    // Setup hooks
128    const t = useTranslate();
129    const { query: internalFilaments } = useSelect<IFilament>({
130      resource: "filament",
131      pagination: { mode: "off" },
132    });
133    const externalFilaments = useGetExternalDBFilaments();
134  
135    // Format and sort internal filament options
136    const filamentSelectInternal: SelectOption[] = useMemo(() => {
137      const data =
138        internalFilaments.data?.data.map((item) => {
139          return {
140            label: formatFilamentLabel(
141              item.name ?? `ID ${item.id}`,
142              item.diameter,
143              item.vendor?.name,
144              item.material,
145              item.weight,
146            ),
147            value: item.id,
148            weight: item.weight,
149            spool_weight: item.spool_weight,
150            is_internal: true,
151          };
152        }) ?? [];
153      data.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
154      return data;
155    }, [internalFilaments.data?.data]);
156  
157    // Format and sort external filament options
158    const filamentSelectExternal: SelectOption[] = useMemo(() => {
159      const data =
160        externalFilaments.data?.map((item) => {
161          return {
162            label: formatFilamentLabel(
163              item.name,
164              item.diameter,
165              item.manufacturer,
166              item.material,
167              item.weight,
168              item.spool_type,
169            ),
170            value: item.id,
171            weight: item.weight,
172            spool_weight: item.spool_weight || undefined,
173            is_internal: false,
174          };
175        }) ?? [];
176      data.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
177      return data;
178    }, [externalFilaments.data]);
179  
180    return {
181      options: [
182        {
183          label: <span>{t("spool.fields.filament_internal")}</span>,
184          options: filamentSelectInternal,
185        },
186        {
187          label: <span>{t("spool.fields.filament_external")}</span>,
188          options: filamentSelectExternal,
189        },
190      ],
191      internalSelectOptions: filamentSelectInternal,
192      externalSelectOptions: filamentSelectExternal,
193      allExternalFilaments: externalFilaments.data,
194    };
195  }
196  
197  type MeasurementType = "length" | "weight" | "measured_weight";
198  
199  export function useSpoolAdjustModal() {
200    const t = useTranslate();
201    const [form] = useForm();
202  
203    const [curSpool, setCurSpool] = useState<ISpool | null>(null);
204    const [measurementType, setMeasurementType] = useState<MeasurementType>("length");
205    const inputNumberRef = useRef<InputNumberRef | null>(null);
206  
207    const openSpoolAdjustModal = useCallback((spool: ISpool) => {
208      setCurSpool(spool);
209      setTimeout(() => {
210        inputNumberRef.current?.focus();
211      }, 0);
212    }, []);
213  
214    const spoolAdjustModal = useMemo(() => {
215      if (curSpool === null) {
216        return null;
217      }
218  
219      const onSubmit = async () => {
220        if (curSpool === null) {
221          return;
222        }
223  
224        const value = form.getFieldValue("filament_value");
225        if (value === undefined || value === null) {
226          return;
227        }
228  
229        if (measurementType === "length") {
230          await useSpoolFilament(curSpool, value, undefined);
231        } else if (measurementType === "weight") {
232          await useSpoolFilament(curSpool, undefined, value);
233        } else {
234          await useSpoolFilamentMeasure(curSpool, value);
235        }
236  
237        setCurSpool(null);
238      };
239  
240      return (
241        <Modal title={t("spool.titles.adjust")} open onCancel={() => setCurSpool(null)} onOk={form.submit}>
242          <p>{t("spool.form.adjust_filament_help")}</p>
243          <Form form={form} initialValues={{ measurement_type: measurementType }} onFinish={onSubmit}>
244            <Form.Item label={t("spool.form.measurement_type_label")} name="measurement_type">
245              <Radio.Group
246                value={measurementType}
247                onChange={({ target: { value } }) => setMeasurementType(value as MeasurementType)}
248              >
249                <Radio.Button value="length">{t("spool.form.measurement_type.length")}</Radio.Button>
250                <Radio.Button value="weight">{t("spool.form.measurement_type.weight")}</Radio.Button>
251                <Radio.Button value="measured_weight">{t("spool.fields.measured_weight")}</Radio.Button>
252              </Radio.Group>
253            </Form.Item>
254            <Form.Item label={t("spool.form.adjust_filament_value")} name="filament_value">
255              <InputNumber ref={inputNumberRef} precision={1} addonAfter={measurementType === "length" ? "mm" : "g"} />
256            </Form.Item>
257          </Form>
258        </Modal>
259      );
260    }, [curSpool, measurementType, t]);
261  
262    return {
263      openSpoolAdjustModal,
264      spoolAdjustModal,
265    };
266  }