manufacturing and gantt

This commit is contained in:
2026-03-15 12:11:46 -05:00
parent a9d31730f8
commit 16582d3cea
26 changed files with 1614 additions and 75 deletions
@@ -1,10 +1,11 @@
import type { InventoryBomLineInput, InventoryItemInput, InventoryItemOptionDto } from "@mrp/shared/dist/inventory/types.js";
import type { InventoryBomLineInput, InventoryItemInput, InventoryItemOperationInput, InventoryItemOptionDto } from "@mrp/shared/dist/inventory/types.js";
import type { ManufacturingStationDto } from "@mrp/shared";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
import { emptyInventoryBomLineInput, emptyInventoryItemInput, inventoryStatusOptions, inventoryTypeOptions, inventoryUnitOptions } from "./config";
import { emptyInventoryBomLineInput, emptyInventoryItemInput, emptyInventoryOperationInput, inventoryStatusOptions, inventoryTypeOptions, inventoryUnitOptions } from "./config";
interface InventoryFormPageProps {
mode: "create" | "edit";
@@ -16,6 +17,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
const { itemId } = useParams();
const [form, setForm] = useState<InventoryItemInput>(emptyInventoryItemInput);
const [componentOptions, setComponentOptions] = useState<InventoryItemOptionDto[]>([]);
const [stations, setStations] = useState<ManufacturingStationDto[]>([]);
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...");
@@ -80,6 +82,14 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
notes: line.notes,
position: line.position,
})),
operations: item.operations.map((operation) => ({
stationId: operation.stationId,
setupMinutes: operation.setupMinutes,
runMinutesPerUnit: operation.runMinutesPerUnit,
moveMinutes: operation.moveMinutes,
position: operation.position,
notes: operation.notes,
})),
});
setComponentSearchTerms(item.bomLines.map((line) => line.componentSku));
setStatus("Inventory item loaded.");
@@ -90,6 +100,14 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
});
}, [itemId, mode, token]);
useEffect(() => {
if (!token) {
return;
}
api.getManufacturingStations(token).then(setStations).catch(() => setStations([]));
}, [token]);
function updateField<Key extends keyof InventoryItemInput>(key: Key, value: InventoryItemInput[Key]) {
setForm((current) => ({ ...current, [key]: value }));
}
@@ -123,6 +141,33 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
setComponentSearchTerms((current) => [...current, ""]);
}
function updateOperation(index: number, nextOperation: InventoryItemOperationInput) {
setForm((current) => ({
...current,
operations: current.operations.map((operation, operationIndex) => (operationIndex === index ? nextOperation : operation)),
}));
}
function addOperation() {
setForm((current) => ({
...current,
operations: [
...current.operations,
{
...emptyInventoryOperationInput,
position: current.operations.length === 0 ? 10 : Math.max(...current.operations.map((operation) => operation.position)) + 10,
},
],
}));
}
function removeOperation(index: number) {
setForm((current) => ({
...current,
operations: current.operations.filter((_operation, operationIndex) => operationIndex !== index),
}));
}
function removeBomLine(index: number) {
setForm((current) => ({
...current,
@@ -289,6 +334,74 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
/>
</label>
</section>
{form.type === "ASSEMBLY" || form.type === "MANUFACTURED" ? (
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Manufacturing Routing</p>
<h4 className="mt-2 text-lg font-bold text-text">Station and time template</h4>
<p className="mt-2 text-sm text-muted">These operations are copied automatically into work orders and drive gantt scheduling without manual planner task entry.</p>
</div>
<button type="button" onClick={addOperation} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Add operation
</button>
</div>
{form.operations.length === 0 ? (
<div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
Add at least one station operation for this buildable item.
</div>
) : (
<div className="mt-5 space-y-4">
{form.operations.map((operation, index) => (
<div key={`${operation.stationId}-${operation.position}-${index}`} className="rounded-3xl border border-line/70 bg-page/60 p-3">
<div className="grid gap-3 xl:grid-cols-[1.2fr_0.55fr_0.7fr_0.55fr_0.55fr_auto]">
<label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Station</span>
<select
value={operation.stationId}
onChange={(event) => updateOperation(index, { ...operation, stationId: event.target.value })}
className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
>
<option value="">Select station</option>
{stations.filter((station) => station.isActive).map((station) => (
<option key={station.id} value={station.id}>
{station.code} - {station.name}
</option>
))}
</select>
</label>
<label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Setup</span>
<input type="number" min={0} step={1} value={operation.setupMinutes} onChange={(event) => updateOperation(index, { ...operation, setupMinutes: Number.parseInt(event.target.value, 10) || 0 })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Run / Unit</span>
<input type="number" min={0} step={1} value={operation.runMinutesPerUnit} onChange={(event) => updateOperation(index, { ...operation, runMinutesPerUnit: Number.parseInt(event.target.value, 10) || 0 })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Move</span>
<input type="number" min={0} step={1} value={operation.moveMinutes} onChange={(event) => updateOperation(index, { ...operation, moveMinutes: Number.parseInt(event.target.value, 10) || 0 })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Position</span>
<input type="number" min={0} step={10} value={operation.position} onChange={(event) => updateOperation(index, { ...operation, position: Number(event.target.value) || 0 })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<div className="flex items-end">
<button type="button" onClick={() => removeOperation(index)} className="rounded-2xl border border-rose-400/40 px-2 py-2 text-sm font-semibold text-rose-700 dark:text-rose-300">
Remove
</button>
</div>
</div>
<label className="mt-4 block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Notes</span>
<input value={operation.notes} onChange={(event) => updateOperation(index, { ...operation, notes: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
</div>
))}
</div>
)}
</section>
) : null}
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>