feat: Add spool adjustment history tracking (#6)
Track history of all spool weight/length adjustments with timestamps and comments:
Backend:
- Add SpoolAdjustment database model with spool_id, timestamp, type, value, comment
- Add Alembic migration for spool_adjustment table
- Add SpoolAdjustment Pydantic model for API responses
- Update use_weight, use_length, and measure functions to record adjustments
- Add GET /api/v1/spool/{id}/adjustments endpoint for history retrieval
- Add optional comment parameter to use/measure endpoints
Frontend:
- Add ISpoolAdjustment interface to spool model
- Add collapsible adjustment history table to spool show page
- Display timestamp, type (weight/length), amount, and comment
- Add translation keys for history table
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,3 +28,12 @@ export interface ISpool {
|
||||
|
||||
// ISpoolParsedExtras is the same as ISpool, but with the extra field parsed into its real types
|
||||
export type ISpoolParsedExtras = Omit<ISpool, "extra"> & { extra?: { [key: string]: unknown } };
|
||||
|
||||
export interface ISpoolAdjustment {
|
||||
id: number;
|
||||
spool_id: number;
|
||||
timestamp: string;
|
||||
adjustment_type: "weight" | "length";
|
||||
value: number;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { InboxOutlined, PrinterOutlined, ToTopOutlined, ToolOutlined } from "@ant-design/icons";
|
||||
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useInvalidate, useShow, useTranslate } from "@refinedev/core";
|
||||
import { Button, Modal, Typography } from "antd";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button, Collapse, Modal, Table, Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React from "react";
|
||||
@@ -11,10 +12,10 @@ import SpoolIcon from "../../components/spoolIcon";
|
||||
import { enrichText } from "../../utils/parsing";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
import { useCurrencyFormatter } from "../../utils/settings";
|
||||
import { getBasePath } from "../../utils/url";
|
||||
import { getAPIURL, getBasePath } from "../../utils/url";
|
||||
import { IFilament } from "../filaments/model";
|
||||
import { setSpoolArchived, useSpoolAdjustModal } from "./functions";
|
||||
import { ISpool } from "./model";
|
||||
import { ISpool, ISpoolAdjustment } from "./model";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@@ -34,6 +35,16 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
||||
|
||||
const record = data?.data;
|
||||
|
||||
// Fetch adjustment history
|
||||
const { data: adjustmentsData, isLoading: adjustmentsLoading } = useQuery({
|
||||
queryKey: ["spool-adjustments", record?.id],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${getAPIURL()}/spool/${record?.id}/adjustments`);
|
||||
return (await res.json()) as ISpoolAdjustment[];
|
||||
},
|
||||
enabled: !!record?.id,
|
||||
});
|
||||
|
||||
const spoolPrice = (item?: ISpool) => {
|
||||
const price = item?.price ?? item?.filament.price;
|
||||
if (price === undefined) {
|
||||
@@ -224,6 +235,59 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
||||
{extraFields?.data?.map((field, index) => (
|
||||
<ExtraFieldDisplay key={index} field={field} value={record?.extra[field.key]} />
|
||||
))}
|
||||
<Title level={4} style={{ marginTop: 24 }}>
|
||||
{t("spool.fields.adjustment_history")}
|
||||
</Title>
|
||||
<Collapse
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: t("spool.fields.show_history"),
|
||||
children: (
|
||||
<Table<ISpoolAdjustment>
|
||||
dataSource={adjustmentsData}
|
||||
loading={adjustmentsLoading}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={{ pageSize: 10 }}
|
||||
columns={[
|
||||
{
|
||||
title: t("spool.fields.adjustment_timestamp"),
|
||||
dataIndex: "timestamp",
|
||||
key: "timestamp",
|
||||
render: (value: string) => (
|
||||
<span title={dayjs.utc(value).local().format()}>
|
||||
{dayjs.utc(value).local().format("YYYY-MM-DD HH:mm:ss")}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("spool.fields.adjustment_type"),
|
||||
dataIndex: "adjustment_type",
|
||||
key: "adjustment_type",
|
||||
render: (value: string) => t(`spool.fields.adjustment_type_${value}`),
|
||||
},
|
||||
{
|
||||
title: t("spool.fields.adjustment_value"),
|
||||
dataIndex: "value",
|
||||
key: "value",
|
||||
render: (value: number, record: ISpoolAdjustment) => {
|
||||
const unit = record.adjustment_type === "weight" ? "g" : "mm";
|
||||
const formatted = value.toFixed(2);
|
||||
return `${value > 0 ? "+" : ""}${formatted} ${unit}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("spool.fields.adjustment_comment"),
|
||||
dataIndex: "comment",
|
||||
key: "comment",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user