CreateWorkOrderPage.tsx 8.75 KB
import { useEffect, useState, type FormEvent } from 'react'
import { useNavigate } from 'react-router-dom'
import { catalog, inventory, production } from '@/api/client'
import type { Item, Location } from '@/types/api'
import { PageHeader } from '@/components/PageHeader'
import { ErrorBox } from '@/components/ErrorBox'

interface BomLine { itemCode: string; quantityPerUnit: string; sourceLocationCode: string }
interface OpLine { operationCode: string; workCenter: string; standardMinutes: string }

export function CreateWorkOrderPage() {
  const navigate = useNavigate()
  const [code, setCode] = useState('')
  const [outputItemCode, setOutputItemCode] = useState('')
  const [outputQuantity, setOutputQuantity] = useState('')
  const [dueDate, setDueDate] = useState('')
  const [bom, setBom] = useState<BomLine[]>([])
  const [ops, setOps] = useState<OpLine[]>([])
  const [items, setItems] = useState<Item[]>([])
  const [locations, setLocations] = useState<Location[]>([])
  const [submitting, setSubmitting] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    Promise.all([catalog.listItems(), inventory.listLocations()]).then(([i, l]) => {
      setItems(i)
      setLocations(l.filter((x) => x.active))
    })
  }, [])

  const addBom = () =>
    setBom([...bom, { itemCode: '', quantityPerUnit: '1', sourceLocationCode: locations[0]?.code ?? '' }])
  const removeBom = (i: number) => setBom(bom.filter((_, idx) => idx !== i))
  const updateBom = (i: number, f: keyof BomLine, v: string) => {
    const next = [...bom]; next[i] = { ...next[i], [f]: v }; setBom(next)
  }

  const addOp = () =>
    setOps([...ops, { operationCode: '', workCenter: '', standardMinutes: '30' }])
  const removeOp = (i: number) => setOps(ops.filter((_, idx) => idx !== i))
  const updateOp = (i: number, f: keyof OpLine, v: string) => {
    const next = [...ops]; next[i] = { ...next[i], [f]: v }; setOps(next)
  }

  const onSubmit = async (e: FormEvent) => {
    e.preventDefault()
    setError(null)
    setSubmitting(true)
    try {
      const created = await production.createWorkOrder({
        code, outputItemCode,
        outputQuantity: Number(outputQuantity),
        dueDate: dueDate || null,
        inputs: bom.map((b, i) => ({
          lineNo: i + 1, itemCode: b.itemCode,
          quantityPerUnit: Number(b.quantityPerUnit),
          sourceLocationCode: b.sourceLocationCode,
        })),
        operations: ops.map((o, i) => ({
          lineNo: i + 1, operationCode: o.operationCode,
          workCenter: o.workCenter,
          standardMinutes: Number(o.standardMinutes),
        })),
      })
      navigate(`/work-orders/${created.id}`)
    } catch (err: unknown) {
      setError(err instanceof Error ? err : new Error(String(err)))
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <div>
      <PageHeader
        title="New Work Order"
        subtitle="Create a production work order with optional BOM inputs and routing operations."
        actions={<button className="btn-secondary" onClick={() => navigate('/work-orders')}>Cancel</button>}
      />
      <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl">
        <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
          <div>
            <label className="block text-sm font-medium text-slate-700">WO code</label>
            <input type="text" required value={code} onChange={(e) => setCode(e.target.value)}
              placeholder="WO-PRINT-0002" className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" />
          </div>
          <div>
            <label className="block text-sm font-medium text-slate-700">Output item</label>
            <select required value={outputItemCode} onChange={(e) => setOutputItemCode(e.target.value)}
              className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm">
              <option value="">Select item...</option>
              {items.map((it) => <option key={it.id} value={it.code}>{it.code} — {it.name}</option>)}
            </select>
          </div>
          <div>
            <label className="block text-sm font-medium text-slate-700">Output qty</label>
            <input type="number" required min="1" step="1" value={outputQuantity}
              onChange={(e) => setOutputQuantity(e.target.value)}
              className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm text-right" />
          </div>
        </div>
        <div>
          <label className="block text-sm font-medium text-slate-700">Due date (optional)</label>
          <input type="date" value={dueDate} onChange={(e) => setDueDate(e.target.value)}
            className="mt-1 max-w-xs rounded-md border border-slate-300 px-3 py-2 text-sm" />
        </div>

        {/* ─── BOM inputs ─────────────────────────────────────── */}
        <div>
          <div className="flex items-center justify-between mb-2">
            <label className="text-sm font-medium text-slate-700">BOM inputs (materials consumed per unit of output)</label>
            <button type="button" className="btn-secondary text-xs" onClick={addBom}>+ Add input</button>
          </div>
          {bom.length === 0 && <p className="text-xs text-slate-400">No BOM lines. Output will be produced without consuming materials.</p>}
          <div className="space-y-2">
            {bom.map((b, idx) => (
              <div key={idx} className="flex items-center gap-2">
                <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span>
                <select value={b.itemCode} onChange={(e) => updateBom(idx, 'itemCode', e.target.value)}
                  className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm">
                  <option value="">Item...</option>
                  {items.map((it) => <option key={it.id} value={it.code}>{it.code}</option>)}
                </select>
                <input type="number" min="0.01" step="0.01" placeholder="Qty/unit" value={b.quantityPerUnit}
                  onChange={(e) => updateBom(idx, 'quantityPerUnit', e.target.value)}
                  className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" />
                <select value={b.sourceLocationCode} onChange={(e) => updateBom(idx, 'sourceLocationCode', e.target.value)}
                  className="w-32 rounded-md border border-slate-300 px-2 py-1.5 text-sm">
                  {locations.map((l) => <option key={l.id} value={l.code}>{l.code}</option>)}
                </select>
                <button type="button" className="text-slate-400 hover:text-rose-500" onClick={() => removeBom(idx)}>&times;</button>
              </div>
            ))}
          </div>
        </div>

        {/* ─── Routing operations ─────────────────────────────── */}
        <div>
          <div className="flex items-center justify-between mb-2">
            <label className="text-sm font-medium text-slate-700">Routing operations (sequential steps)</label>
            <button type="button" className="btn-secondary text-xs" onClick={addOp}>+ Add operation</button>
          </div>
          {ops.length === 0 && <p className="text-xs text-slate-400">No routing. Work order completes in one step.</p>}
          <div className="space-y-2">
            {ops.map((o, idx) => (
              <div key={idx} className="flex items-center gap-2">
                <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span>
                <input type="text" required placeholder="Op code (e.g. CTP)" value={o.operationCode}
                  onChange={(e) => updateOp(idx, 'operationCode', e.target.value)}
                  className="w-28 rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
                <input type="text" required placeholder="Work center" value={o.workCenter}
                  onChange={(e) => updateOp(idx, 'workCenter', e.target.value)}
                  className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm" />
                <input type="number" min="1" step="1" placeholder="Std min" value={o.standardMinutes}
                  onChange={(e) => updateOp(idx, 'standardMinutes', e.target.value)}
                  className="w-20 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" />
                <button type="button" className="text-slate-400 hover:text-rose-500" onClick={() => removeOp(idx)}>&times;</button>
              </div>
            ))}
          </div>
        </div>

        {error && <ErrorBox error={error} />}
        <button type="submit" className="btn-primary" disabled={submitting}>
          {submitting ? 'Creating...' : 'Create Work Order'}
        </button>
      </form>
    </div>
  )
}