- 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
221 lines
5.9 KiB
TypeScript
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",
|
|
};
|