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:
2026-01-14 20:18:56 -06:00
parent 388669d0c6
commit d804329b77
8 changed files with 307 additions and 13 deletions

View File

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

View File

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