Files
silo/web/src/components/items/EditItemPane.tsx
forbes-0023 b3c748ef10 refactor: move sourcing_link and standard_cost from item columns to revision properties
- Add migration 013 to copy sourcing_link/standard_cost values into
  current revision properties JSONB and drop the columns from items table
- Remove SourcingLink/StandardCost from Go Item struct and all DB queries
  (items.go, audit_queries.go, projects.go)
- Remove from API request/response structs and handlers
- Update CSV/ODS/BOM export/import to read these from revision properties
- Update audit handlers to score as regular property fields
- Remove from frontend Item type and hardcoded form fields
- MainTab now reads sourcing_link/standard_cost from item.properties
- CreateItemPane/EditItemPane no longer have dedicated fields for these;
  they will be rendered as schema-driven property fields
2026-02-11 09:50:31 -06:00

221 lines
5.9 KiB
TypeScript

import { useState, useEffect } from "react";
import { get, put } from "../../api/client";
import type { Item } from "../../api/types";
interface EditItemPaneProps {
partNumber: string;
onSaved: () => void;
onCancel: () => void;
}
export function EditItemPane({
partNumber,
onSaved,
onCancel,
}: EditItemPaneProps) {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pn, setPN] = useState("");
const [itemType, setItemType] = useState("");
const [description, setDescription] = useState("");
const [sourcingType, setSourcingType] = useState("");
const [longDescription, setLongDescription] = useState("");
useEffect(() => {
setLoading(true);
get<Item>(`/api/items/${encodeURIComponent(partNumber)}`)
.then((item) => {
setPN(item.part_number);
setItemType(item.item_type);
setDescription(item.description);
setSourcingType(item.sourcing_type ?? "");
setLongDescription(item.long_description ?? "");
})
.catch(() => setError("Failed to load item"))
.finally(() => setLoading(false));
}, [partNumber]);
const handleSave = async () => {
setSaving(true);
setError(null);
try {
await put(`/api/items/${encodeURIComponent(partNumber)}`, {
part_number: pn !== partNumber ? pn : undefined,
item_type: itemType || undefined,
description: description || undefined,
sourcing_type: sourcingType || undefined,
long_description: longDescription || undefined,
});
onSaved();
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to save item");
} finally {
setSaving(false);
}
};
if (loading)
return (
<div style={{ padding: "1rem", color: "var(--ctp-subtext0)" }}>
Loading...
</div>
);
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.75rem",
padding: "0.5rem 0.75rem",
borderBottom: "1px solid var(--ctp-surface1)",
backgroundColor: "var(--ctp-mantle)",
flexShrink: 0,
}}
>
<span
style={{
color: "var(--ctp-blue)",
fontWeight: 600,
fontSize: "0.9rem",
}}
>
Edit {partNumber}
</span>
<span style={{ flex: 1 }} />
<button
onClick={() => void handleSave()}
disabled={saving}
style={{
padding: "0.3rem 0.75rem",
fontSize: "0.8rem",
border: "none",
borderRadius: "0.3rem",
backgroundColor: "var(--ctp-blue)",
color: "var(--ctp-crust)",
cursor: "pointer",
opacity: saving ? 0.6 : 1,
}}
>
{saving ? "Saving..." : "Save"}
</button>
<button onClick={onCancel} style={headerBtnStyle}>
Cancel
</button>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "0.75rem" }}>
{error && (
<div
style={{
color: "var(--ctp-red)",
backgroundColor: "rgba(243,139,168,0.1)",
padding: "0.5rem",
borderRadius: "0.3rem",
marginBottom: "0.5rem",
fontSize: "0.85rem",
}}
>
{error}
</div>
)}
<FormGroup label="Part Number">
<input
value={pn}
onChange={(e) => setPN(e.target.value)}
style={inputStyle}
/>
</FormGroup>
<FormGroup label="Type">
<select
value={itemType}
onChange={(e) => setItemType(e.target.value)}
style={inputStyle}
>
<option value="part">Part</option>
<option value="assembly">Assembly</option>
<option value="document">Document</option>
<option value="tooling">Tooling</option>
</select>
</FormGroup>
<FormGroup label="Description">
<input
value={description}
onChange={(e) => setDescription(e.target.value)}
style={inputStyle}
/>
</FormGroup>
<FormGroup label="Sourcing Type">
<select
value={sourcingType}
onChange={(e) => setSourcingType(e.target.value)}
style={inputStyle}
>
<option value=""></option>
<option value="manufactured">Manufactured</option>
<option value="purchased">Purchased</option>
<option value="outsourced">Outsourced</option>
</select>
</FormGroup>
<FormGroup label="Long Description">
<textarea
value={longDescription}
onChange={(e) => setLongDescription(e.target.value)}
style={{ ...inputStyle, minHeight: 80, resize: "vertical" }}
/>
</FormGroup>
</div>
</div>
);
}
function FormGroup({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<div style={{ marginBottom: "0.6rem" }}>
<label
style={{
display: "block",
fontSize: "0.75rem",
color: "var(--ctp-subtext0)",
marginBottom: "0.2rem",
}}
>
{label}
</label>
{children}
</div>
);
}
const inputStyle: React.CSSProperties = {
width: "100%",
padding: "0.35rem 0.5rem",
fontSize: "0.85rem",
backgroundColor: "var(--ctp-base)",
border: "1px solid var(--ctp-surface1)",
borderRadius: "0.3rem",
color: "var(--ctp-text)",
};
const headerBtnStyle: React.CSSProperties = {
background: "none",
border: "none",
cursor: "pointer",
color: "var(--ctp-subtext1)",
fontSize: "0.8rem",
padding: "0.2rem 0.4rem",
};