CreateSalesOrderPage.tsx 12.8 KB
import { useEffect, useState, type FormEvent } from 'react'
import { useNavigate } from 'react-router-dom'
import { catalog, partners, salesOrders } from '@/api/client'
import type { Item, Partner } from '@/types/api'
import { ErrorBox } from '@/components/ErrorBox'
import { DynamicExtFields } from '@/components/DynamicExtFields'
import { useT } from '@/i18n/LocaleContext'

interface LineInput {
  itemCode: string
  quantity: string
  unitPrice: string
}

export function CreateSalesOrderPage() {
  const navigate = useNavigate()
  const t = useT()
  const [code, setCode] = useState('')
  const [partnerCode, setPartnerCode] = useState('')
  const [currencyCode] = useState('USD')
  const [lines, setLines] = useState<LineInput[]>([
    { itemCode: '', quantity: '', unitPrice: '' },
  ])
  const [items, setItems] = useState<Item[]>([])
  const [partnerList, setPartnerList] = useState<Partner[]>([])
  const [ext, setExt] = useState<Record<string, unknown>>({})
  const [submitting, setSubmitting] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    Promise.all([catalog.listItems(), partners.list()]).then(([i, p]) => {
      setItems(i)
      const customers = p.filter((x) => x.type === 'CUSTOMER' || x.type === 'BOTH')
      setPartnerList(customers)
      if (customers.length > 0 && !partnerCode) setPartnerCode(customers[0].code)
    })
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  const addLine = () =>
    setLines([...lines, { itemCode: items[0]?.code ?? '', quantity: '1', unitPrice: '1.00' }])

  const removeLine = (idx: number) => {
    if (lines.length <= 1) return
    setLines(lines.filter((_, i) => i !== idx))
  }

  const updateLine = (idx: number, field: keyof LineInput, value: string) => {
    const next = [...lines]
    next[idx] = { ...next[idx], [field]: value }
    setLines(next)
  }

  const orderTotal = lines.reduce(
    (sum, l) => sum + Number(l.quantity || 0) * Number(l.unitPrice || 0),
    0,
  )

  const onSubmit = async (e: FormEvent) => {
    e.preventDefault()
    setError(null)
    setSubmitting(true)
    try {
      const extPayload = Object.keys(ext).length > 0 ? ext : undefined
      const created = await salesOrders.create({
        code,
        partnerCode,
        orderDate: new Date().toISOString().slice(0, 10),
        currencyCode,
        lines: lines.map((l, i) => ({
          lineNo: i + 1,
          itemCode: l.itemCode,
          quantity: Number(l.quantity),
          unitPrice: Number(l.unitPrice),
          currencyCode,
        })),
        ...(extPayload ? { ext: extPayload } : {}),
      } as Parameters<typeof salesOrders.create>[0])
      navigate(`/sales-orders/${created.id}`)
    } catch (err: unknown) {
      setError(err instanceof Error ? err : new Error(String(err)))
    } finally {
      setSubmitting(false)
    }
  }

  /* ── label cell (light blue bg, right-aligned) ── */
  const labelCls =
    'bg-sky-50 border-b border-r border-slate-200 px-3 py-2 text-right text-slate-600 whitespace-nowrap text-sm'
  /* ── value cell ── */
  const valueCls = 'border-b border-slate-200 px-2 py-1'
  const valueClsR = `${valueCls} border-r` // value cell with right border (cols 1 & 3)
  /* ── inline input ── */
  const inputCls = 'w-full border-0 outline-none text-sm py-0.5 bg-transparent'

  return (
    <form onSubmit={onSubmit} className="flex flex-col h-full">
      {/* ════════ Title bar ════════ */}
      <div className="flex items-center justify-between bg-white border-b border-slate-200 px-4 py-2">
        <h1 className="text-base font-semibold text-slate-800">
          {t('page.salesOrder.create.title')}
        </h1>
      </div>

      {/* ════════ Top toolbar ════════ */}
      <div className="flex items-center gap-1 bg-slate-100 border-b border-slate-300 px-3 py-1.5 text-sm">
        <button
          type="submit"
          className="flex items-center gap-1 px-3 py-1 rounded hover:bg-slate-200 disabled:opacity-50"
          disabled={submitting}
        >
          {submitting ? t('action.creating') : t('action.save')}
        </button>
        <div className="w-px h-5 bg-slate-300 mx-1" />
        <button
          type="button"
          className="flex items-center gap-1 px-3 py-1 rounded hover:bg-slate-200"
          onClick={() => navigate('/sales-orders')}
        >
          {t('action.cancel')}
        </button>
      </div>

      {/* ════════ Scrollable body ════════ */}
      <div className="flex-1 overflow-y-auto p-4 bg-slate-50 space-y-4">

        {/* ════════ Form header — dense grid ════════ */}
        <div className="border border-slate-300 bg-white">
          <div className="grid grid-cols-4 text-sm">
            {/* Row 1 */}
            <div className={labelCls}>
              <span className="text-rose-500">*</span> {t('label.orderCode')}
            </div>
            <div className={valueClsR}>
              <input
                type="text"
                required
                value={code}
                onChange={(e) => setCode(e.target.value)}
                placeholder="SO-2026-0003"
                className={inputCls}
              />
            </div>
            <div className={labelCls}>
              {t('label.orderDate')}
            </div>
            <div className={valueCls}>
              <input
                type="text"
                value={new Date().toISOString().slice(0, 10)}
                disabled
                className={`${inputCls} text-slate-500`}
              />
            </div>

            {/* Row 2 */}
            <div className={labelCls}>
              <span className="text-rose-500">*</span> {t('label.customer')}
            </div>
            <div className={valueClsR}>
              <select
                required
                value={partnerCode}
                onChange={(e) => setPartnerCode(e.target.value)}
                className={`${inputCls} cursor-pointer`}
              >
                {partnerList.map((p) => (
                  <option key={p.id} value={p.code}>
                    {p.code} — {p.name}
                  </option>
                ))}
              </select>
            </div>
            <div className={labelCls}>
              {t('label.currency')}
            </div>
            <div className={valueCls}>
              <span className="text-sm text-slate-500">{currencyCode}</span>
            </div>

            {/* Row 3 — system / auto fields */}
            <div className={labelCls}>
              {t('label.createdBy')}
            </div>
            <div className={valueClsR}>
              <span className="text-sm text-slate-400">{t('label.autoGenerated')}</span>
            </div>
            <div className={labelCls}>
              {t('label.createdAt')}
            </div>
            <div className={valueCls}>
              <span className="text-sm text-slate-400">{t('label.autoGenerated')}</span>
            </div>
          </div>
        </div>

        {/* ════════ Tabs ════════ */}
        <div className="border-b border-slate-300">
          <button
            type="button"
            className="px-4 py-2 text-sm border-b-2 border-blue-500 text-blue-600 font-medium"
          >
            {t('tab.orderLines')}
          </button>
        </div>

        {/* ════════ Line items — spreadsheet table ════════ */}
        <div className="border border-slate-300 bg-white">
          <table className="w-full text-sm">
            <thead>
              <tr className="bg-slate-100 border-b border-slate-300">
                <th className="px-2 py-2 text-center w-10 border-r border-slate-200">#</th>
                <th className="px-2 py-2 text-left border-r border-slate-200">
                  <span className="text-rose-500">*</span> {t('label.item')}
                </th>
                <th className="px-2 py-2 text-right border-r border-slate-200 w-24">
                  <span className="text-rose-500">*</span> {t('label.quantity')}
                </th>
                <th className="px-2 py-2 text-right border-r border-slate-200 w-28">
                  <span className="text-rose-500">*</span> {t('label.unitPrice')}
                </th>
                <th className="px-2 py-2 text-right border-r border-slate-200 w-28">
                  {t('label.lineTotal')}
                </th>
                <th className="px-2 py-2 text-center w-24">{t('label.actions')}</th>
              </tr>
            </thead>
            <tbody>
              {lines.map((line, idx) => (
                <tr key={idx} className="border-b border-slate-200 hover:bg-sky-50">
                  <td className="px-2 py-1.5 text-center border-r border-slate-200 text-slate-400">
                    {idx + 1}
                  </td>
                  <td className="px-1 py-0.5 border-r border-slate-200">
                    <select
                      value={line.itemCode}
                      onChange={(e) => updateLine(idx, 'itemCode', e.target.value)}
                      className="w-full border-0 outline-none text-sm py-1 bg-transparent"
                    >
                      <option value="">{t('label.selectItem')}</option>
                      {items.map((it) => (
                        <option key={it.id} value={it.code}>
                          {it.code} — {it.name}
                        </option>
                      ))}
                    </select>
                  </td>
                  <td className="px-1 py-0.5 border-r border-slate-200">
                    <input
                      type="number"
                      min="1"
                      step="1"
                      placeholder="0"
                      value={line.quantity}
                      onChange={(e) => updateLine(idx, 'quantity', e.target.value)}
                      className="w-full border-0 outline-none text-sm py-1 text-right bg-transparent"
                    />
                  </td>
                  <td className="px-1 py-0.5 border-r border-slate-200">
                    <input
                      type="number"
                      min="0"
                      step="0.01"
                      placeholder="0.00"
                      value={line.unitPrice}
                      onChange={(e) => updateLine(idx, 'unitPrice', e.target.value)}
                      className="w-full border-0 outline-none text-sm py-1 text-right bg-transparent"
                    />
                  </td>
                  <td className="px-2 py-1.5 text-right border-r border-slate-200 text-slate-500">
                    {(Number(line.quantity || 0) * Number(line.unitPrice || 0)).toFixed(2)}
                  </td>
                  <td className="px-2 py-1.5 text-center">
                    <button
                      type="button"
                      onClick={addLine}
                      title={t('action.addRow')}
                      className="text-blue-500 hover:text-blue-700 mx-0.5 text-xs"
                    >
                      [+]
                    </button>
                    <button
                      type="button"
                      onClick={() => removeLine(idx)}
                      title={t('action.removeRow')}
                      className="text-rose-400 hover:text-rose-600 mx-0.5 text-xs"
                    >
                      [x]
                    </button>
                  </td>
                </tr>
              ))}
            </tbody>
            <tfoot>
              <tr className="bg-slate-50 border-t border-slate-300">
                <td colSpan={2} className="px-2 py-1.5 text-right">
                  <button
                    type="button"
                    onClick={addLine}
                    className="text-blue-600 hover:text-blue-800 text-xs font-medium"
                  >
                    {t('action.addLine')}
                  </button>
                </td>
                <td colSpan={2} className="px-2 py-1.5 text-right font-medium text-slate-700">
                  {t('label.orderTotal')}
                </td>
                <td className="px-2 py-1.5 text-right font-semibold text-slate-800 border-r border-slate-200">
                  {orderTotal.toFixed(2)}
                </td>
                <td />
              </tr>
            </tfoot>
          </table>
        </div>

        {/* ════════ Custom fields (Tier 1 ext) ════════ */}
        <DynamicExtFields
          entityName="SalesOrder"
          values={ext}
          onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))}
        />

        {/* ════════ Error ════════ */}
        {error && (
          <div className="max-w-2xl">
            <ErrorBox error={error} />
          </div>
        )}
      </div>
    </form>
  )
}