inventory2
This commit is contained in:
@@ -16,9 +16,16 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
const { itemId } = useParams();
|
||||
const [form, setForm] = useState<InventoryItemInput>(emptyInventoryItemInput);
|
||||
const [componentOptions, setComponentOptions] = useState<InventoryItemOptionDto[]>([]);
|
||||
const [componentSearchTerms, setComponentSearchTerms] = useState<string[]>([]);
|
||||
const [activeComponentPicker, setActiveComponentPicker] = useState<number | null>(null);
|
||||
const [status, setStatus] = useState(mode === "create" ? "Create a new inventory item." : "Loading inventory item...");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
function getComponentLabel(componentItemId: string) {
|
||||
const match = componentOptions.find((option) => option.id === componentItemId);
|
||||
return match ? `${match.sku} - ${match.name}` : "";
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return;
|
||||
@@ -26,9 +33,22 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
|
||||
api
|
||||
.getInventoryItemOptions(token)
|
||||
.then((options) => setComponentOptions(options.filter((option) => option.id !== itemId)))
|
||||
.then((options) => {
|
||||
const nextOptions = options.filter((option) => option.id !== itemId);
|
||||
setComponentOptions(nextOptions);
|
||||
setComponentSearchTerms((current) =>
|
||||
form.bomLines.map((line, index) => {
|
||||
if (current[index]?.trim()) {
|
||||
return current[index];
|
||||
}
|
||||
|
||||
const match = nextOptions.find((option) => option.id === line.componentItemId);
|
||||
return match ? `${match.sku} - ${match.name}` : "";
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(() => setComponentOptions([]));
|
||||
}, [itemId, token]);
|
||||
}, [form.bomLines, itemId, token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "edit" || !token || !itemId) {
|
||||
@@ -57,6 +77,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
position: line.position,
|
||||
})),
|
||||
});
|
||||
setComponentSearchTerms(item.bomLines.map((line) => `${line.componentSku} - ${line.componentName}`));
|
||||
setStatus("Inventory item loaded.");
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
@@ -76,6 +97,14 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
}));
|
||||
}
|
||||
|
||||
function updateComponentSearchTerm(index: number, value: string) {
|
||||
setComponentSearchTerms((current) => {
|
||||
const nextTerms = [...current];
|
||||
nextTerms[index] = value;
|
||||
return nextTerms;
|
||||
});
|
||||
}
|
||||
|
||||
function addBomLine() {
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
@@ -87,6 +116,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
},
|
||||
],
|
||||
}));
|
||||
setComponentSearchTerms((current) => [...current, ""]);
|
||||
}
|
||||
|
||||
function removeBomLine(index: number) {
|
||||
@@ -94,6 +124,8 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
...current,
|
||||
bomLines: current.bomLines.filter((_line, lineIndex) => lineIndex !== index),
|
||||
}));
|
||||
setComponentSearchTerms((current) => current.filter((_term, termIndex) => termIndex !== index));
|
||||
setActiveComponentPicker((current) => (current === index ? null : current != null && current > index ? current - 1 : current));
|
||||
}
|
||||
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
@@ -268,18 +300,69 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
||||
<div className="grid gap-4 xl:grid-cols-[1.4fr_0.7fr_0.7fr_0.7fr_auto]">
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Component</span>
|
||||
<select
|
||||
value={line.componentItemId}
|
||||
onChange={(event) => updateBomLine(index, { ...line, componentItemId: event.target.value })}
|
||||
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand"
|
||||
>
|
||||
<option value="">Select component</option>
|
||||
{componentOptions.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{option.sku} - {option.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="relative">
|
||||
<input
|
||||
value={componentSearchTerms[index] ?? getComponentLabel(line.componentItemId)}
|
||||
onChange={(event) => {
|
||||
updateComponentSearchTerm(index, event.target.value);
|
||||
updateBomLine(index, { ...line, componentItemId: "" });
|
||||
setActiveComponentPicker(index);
|
||||
}}
|
||||
onFocus={() => setActiveComponentPicker(index)}
|
||||
onBlur={() => {
|
||||
window.setTimeout(() => {
|
||||
setActiveComponentPicker((current) => (current === index ? null : current));
|
||||
if (!form.bomLines[index]?.componentItemId) {
|
||||
updateComponentSearchTerm(index, "");
|
||||
} else {
|
||||
updateComponentSearchTerm(index, getComponentLabel(form.bomLines[index].componentItemId));
|
||||
}
|
||||
}, 120);
|
||||
}}
|
||||
placeholder="Search by SKU or item name"
|
||||
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand"
|
||||
/>
|
||||
{activeComponentPicker === index ? (
|
||||
<div className="absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel">
|
||||
{componentOptions
|
||||
.filter((option) => {
|
||||
const query = (componentSearchTerms[index] ?? "").trim().toLowerCase();
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return `${option.sku} ${option.name}`.toLowerCase().includes(query);
|
||||
})
|
||||
.slice(0, 12)
|
||||
.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
updateBomLine(index, { ...line, componentItemId: option.id });
|
||||
updateComponentSearchTerm(index, `${option.sku} - ${option.name}`);
|
||||
setActiveComponentPicker(null);
|
||||
}}
|
||||
className="block w-full border-b border-line/50 px-4 py-3 text-left text-sm text-text transition last:border-b-0 hover:bg-page/70"
|
||||
>
|
||||
<div className="font-semibold">{option.sku}</div>
|
||||
<div className="mt-1 text-xs text-muted">{option.name}</div>
|
||||
</button>
|
||||
))}
|
||||
{componentOptions.filter((option) => {
|
||||
const query = (componentSearchTerms[index] ?? "").trim().toLowerCase();
|
||||
if (!query) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return `${option.sku} ${option.name}`.toLowerCase().includes(query);
|
||||
}).length === 0 ? (
|
||||
<div className="px-4 py-3 text-sm text-muted">No matching components found.</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Qty</span>
|
||||
|
||||
Reference in New Issue
Block a user