Commit c098b53f498429e04e5871cfb005f7ae2cdb96a0
1 parent
6354c9d3
feat(web): ERP-style form layout prototype (Sales Order)
Replace the card-based create form with a traditional Chinese ERP layout: dense 4-column label/value header grid, top toolbar, tabbed section, and spreadsheet-style inline-editable line items with running totals. All labels go through the i18n system (16 new message keys in en/zh-CN). This is the prototype; same pattern will be applied to all entity forms.
Showing
2 changed files
with
274 additions
and
111 deletions
web/src/i18n/messages.ts
| @@ -142,6 +142,23 @@ export const en = { | @@ -142,6 +142,23 @@ export const en = { | ||
| 142 | 'label.enabled': 'Enabled', | 142 | 'label.enabled': 'Enabled', |
| 143 | 'label.conditionLogic': 'Logic', | 143 | 'label.conditionLogic': 'Logic', |
| 144 | 'action.newRule': 'New Rule', | 144 | 'action.newRule': 'New Rule', |
| 145 | + | ||
| 146 | + // ─── ERP form ───────────────────────────────────────────── | ||
| 147 | + 'page.salesOrder.create.title': 'New Sales Order', | ||
| 148 | + 'label.orderCode': 'Order Code', | ||
| 149 | + 'label.orderDate': 'Order Date', | ||
| 150 | + 'label.customer': 'Customer', | ||
| 151 | + 'label.createdBy': 'Created By', | ||
| 152 | + 'label.createdAt': 'Created At', | ||
| 153 | + 'label.autoGenerated': 'Auto-generated', | ||
| 154 | + 'tab.orderLines': 'Order Lines', | ||
| 155 | + 'label.item': 'Item', | ||
| 156 | + 'label.lineTotal': 'Line Total', | ||
| 157 | + 'label.selectItem': 'Select item...', | ||
| 158 | + 'label.orderTotal': 'Order Total', | ||
| 159 | + 'label.customFields': 'Custom Fields', | ||
| 160 | + 'action.addRow': 'Add Row', | ||
| 161 | + 'action.removeRow': 'Remove', | ||
| 145 | } as const | 162 | } as const |
| 146 | 163 | ||
| 147 | export const zhCN: Record<MessageKey, string> = { | 164 | export const zhCN: Record<MessageKey, string> = { |
| @@ -275,6 +292,23 @@ export const zhCN: Record<MessageKey, string> = { | @@ -275,6 +292,23 @@ export const zhCN: Record<MessageKey, string> = { | ||
| 275 | 'label.enabled': '启用', | 292 | 'label.enabled': '启用', |
| 276 | 'label.conditionLogic': '逻辑', | 293 | 'label.conditionLogic': '逻辑', |
| 277 | 'action.newRule': '新建规则', | 294 | 'action.newRule': '新建规则', |
| 295 | + | ||
| 296 | + // ─── ERP 表单 ───────────────────────────────────────────── | ||
| 297 | + 'page.salesOrder.create.title': '新建销售订单', | ||
| 298 | + 'label.orderCode': '订单编码', | ||
| 299 | + 'label.orderDate': '订单日期', | ||
| 300 | + 'label.customer': '客户', | ||
| 301 | + 'label.createdBy': '创建人', | ||
| 302 | + 'label.createdAt': '创建时间', | ||
| 303 | + 'label.autoGenerated': '自动生成', | ||
| 304 | + 'tab.orderLines': '订单明细', | ||
| 305 | + 'label.item': '物料', | ||
| 306 | + 'label.lineTotal': '行合计', | ||
| 307 | + 'label.selectItem': '请选择物料...', | ||
| 308 | + 'label.orderTotal': '订单合计', | ||
| 309 | + 'label.customFields': '自定义字段', | ||
| 310 | + 'action.addRow': '添加行', | ||
| 311 | + 'action.removeRow': '删除', | ||
| 278 | } | 312 | } |
| 279 | 313 | ||
| 280 | export const locales = { | 314 | export const locales = { |
web/src/pages/CreateSalesOrderPage.tsx
| @@ -2,9 +2,9 @@ import { useEffect, useState, type FormEvent } from 'react' | @@ -2,9 +2,9 @@ import { useEffect, useState, type FormEvent } from 'react' | ||
| 2 | import { useNavigate } from 'react-router-dom' | 2 | import { useNavigate } from 'react-router-dom' |
| 3 | import { catalog, partners, salesOrders } from '@/api/client' | 3 | import { catalog, partners, salesOrders } from '@/api/client' |
| 4 | import type { Item, Partner } from '@/types/api' | 4 | import type { Item, Partner } from '@/types/api' |
| 5 | -import { PageHeader } from '@/components/PageHeader' | ||
| 6 | import { ErrorBox } from '@/components/ErrorBox' | 5 | import { ErrorBox } from '@/components/ErrorBox' |
| 7 | import { DynamicExtFields } from '@/components/DynamicExtFields' | 6 | import { DynamicExtFields } from '@/components/DynamicExtFields' |
| 7 | +import { useT } from '@/i18n/LocaleContext' | ||
| 8 | 8 | ||
| 9 | interface LineInput { | 9 | interface LineInput { |
| 10 | itemCode: string | 10 | itemCode: string |
| @@ -14,6 +14,7 @@ interface LineInput { | @@ -14,6 +14,7 @@ interface LineInput { | ||
| 14 | 14 | ||
| 15 | export function CreateSalesOrderPage() { | 15 | export function CreateSalesOrderPage() { |
| 16 | const navigate = useNavigate() | 16 | const navigate = useNavigate() |
| 17 | + const t = useT() | ||
| 17 | const [code, setCode] = useState('') | 18 | const [code, setCode] = useState('') |
| 18 | const [partnerCode, setPartnerCode] = useState('') | 19 | const [partnerCode, setPartnerCode] = useState('') |
| 19 | const [currencyCode] = useState('USD') | 20 | const [currencyCode] = useState('USD') |
| @@ -49,6 +50,11 @@ export function CreateSalesOrderPage() { | @@ -49,6 +50,11 @@ export function CreateSalesOrderPage() { | ||
| 49 | setLines(next) | 50 | setLines(next) |
| 50 | } | 51 | } |
| 51 | 52 | ||
| 53 | + const orderTotal = lines.reduce( | ||
| 54 | + (sum, l) => sum + Number(l.quantity || 0) * Number(l.unitPrice || 0), | ||
| 55 | + 0, | ||
| 56 | + ) | ||
| 57 | + | ||
| 52 | const onSubmit = async (e: FormEvent) => { | 58 | const onSubmit = async (e: FormEvent) => { |
| 53 | e.preventDefault() | 59 | e.preventDefault() |
| 54 | setError(null) | 60 | setError(null) |
| @@ -77,127 +83,250 @@ export function CreateSalesOrderPage() { | @@ -77,127 +83,250 @@ export function CreateSalesOrderPage() { | ||
| 77 | } | 83 | } |
| 78 | } | 84 | } |
| 79 | 85 | ||
| 86 | + /* ── label cell (light blue bg, right-aligned) ── */ | ||
| 87 | + const labelCls = | ||
| 88 | + 'bg-sky-50 border-b border-r border-slate-200 px-3 py-2 text-right text-slate-600 whitespace-nowrap text-sm' | ||
| 89 | + /* ── value cell ── */ | ||
| 90 | + const valueCls = 'border-b border-slate-200 px-2 py-1' | ||
| 91 | + const valueClsR = `${valueCls} border-r` // value cell with right border (cols 1 & 3) | ||
| 92 | + /* ── inline input ── */ | ||
| 93 | + const inputCls = 'w-full border-0 outline-none text-sm py-0.5 bg-transparent' | ||
| 94 | + | ||
| 80 | return ( | 95 | return ( |
| 81 | - <div> | ||
| 82 | - <PageHeader | ||
| 83 | - title="New Sales Order" | ||
| 84 | - subtitle="Create a sales order. Confirming it will auto-generate production work orders." | ||
| 85 | - actions={ | ||
| 86 | - <button className="btn-secondary" onClick={() => navigate('/sales-orders')}> | ||
| 87 | - Cancel | ||
| 88 | - </button> | ||
| 89 | - } | ||
| 90 | - /> | ||
| 91 | - <form onSubmit={onSubmit} className="card p-6 space-y-5 max-w-3xl"> | ||
| 92 | - <div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> | ||
| 93 | - <div> | ||
| 94 | - <label className="block text-sm font-medium text-slate-700">Order code</label> | ||
| 95 | - <input | ||
| 96 | - type="text" | ||
| 97 | - required | ||
| 98 | - value={code} | ||
| 99 | - onChange={(e) => setCode(e.target.value)} | ||
| 100 | - placeholder="SO-2026-0003" | ||
| 101 | - className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-brand-500 focus:ring-brand-500" | ||
| 102 | - /> | ||
| 103 | - </div> | ||
| 104 | - <div> | ||
| 105 | - <label className="block text-sm font-medium text-slate-700">Customer</label> | ||
| 106 | - <select | ||
| 107 | - required | ||
| 108 | - value={partnerCode} | ||
| 109 | - onChange={(e) => setPartnerCode(e.target.value)} | ||
| 110 | - className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-brand-500 focus:ring-brand-500" | ||
| 111 | - > | ||
| 112 | - {partnerList.map((p) => ( | ||
| 113 | - <option key={p.id} value={p.code}> | ||
| 114 | - {p.code} — {p.name} | ||
| 115 | - </option> | ||
| 116 | - ))} | ||
| 117 | - </select> | ||
| 118 | - </div> | ||
| 119 | - <div> | ||
| 120 | - <label className="block text-sm font-medium text-slate-700">Currency</label> | ||
| 121 | - <input | ||
| 122 | - type="text" | ||
| 123 | - value={currencyCode} | ||
| 124 | - disabled | ||
| 125 | - className="mt-1 w-full rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-sm" | ||
| 126 | - /> | 96 | + <form onSubmit={onSubmit} className="flex flex-col h-full"> |
| 97 | + {/* ════════ Title bar ════════ */} | ||
| 98 | + <div className="flex items-center justify-between bg-white border-b border-slate-200 px-4 py-2"> | ||
| 99 | + <h1 className="text-base font-semibold text-slate-800"> | ||
| 100 | + {t('page.salesOrder.create.title')} | ||
| 101 | + </h1> | ||
| 102 | + </div> | ||
| 103 | + | ||
| 104 | + {/* ════════ Top toolbar ════════ */} | ||
| 105 | + <div className="flex items-center gap-1 bg-slate-100 border-b border-slate-300 px-3 py-1.5 text-sm"> | ||
| 106 | + <button | ||
| 107 | + type="submit" | ||
| 108 | + className="flex items-center gap-1 px-3 py-1 rounded hover:bg-slate-200 disabled:opacity-50" | ||
| 109 | + disabled={submitting} | ||
| 110 | + > | ||
| 111 | + {submitting ? t('action.creating') : t('action.save')} | ||
| 112 | + </button> | ||
| 113 | + <div className="w-px h-5 bg-slate-300 mx-1" /> | ||
| 114 | + <button | ||
| 115 | + type="button" | ||
| 116 | + className="flex items-center gap-1 px-3 py-1 rounded hover:bg-slate-200" | ||
| 117 | + onClick={() => navigate('/sales-orders')} | ||
| 118 | + > | ||
| 119 | + {t('action.cancel')} | ||
| 120 | + </button> | ||
| 121 | + </div> | ||
| 122 | + | ||
| 123 | + {/* ════════ Scrollable body ════════ */} | ||
| 124 | + <div className="flex-1 overflow-y-auto p-4 bg-slate-50 space-y-4"> | ||
| 125 | + | ||
| 126 | + {/* ════════ Form header — dense grid ════════ */} | ||
| 127 | + <div className="border border-slate-300 bg-white"> | ||
| 128 | + <div className="grid grid-cols-4 text-sm"> | ||
| 129 | + {/* Row 1 */} | ||
| 130 | + <div className={labelCls}> | ||
| 131 | + <span className="text-rose-500">*</span> {t('label.orderCode')} | ||
| 132 | + </div> | ||
| 133 | + <div className={valueClsR}> | ||
| 134 | + <input | ||
| 135 | + type="text" | ||
| 136 | + required | ||
| 137 | + value={code} | ||
| 138 | + onChange={(e) => setCode(e.target.value)} | ||
| 139 | + placeholder="SO-2026-0003" | ||
| 140 | + className={inputCls} | ||
| 141 | + /> | ||
| 142 | + </div> | ||
| 143 | + <div className={labelCls}> | ||
| 144 | + {t('label.orderDate')} | ||
| 145 | + </div> | ||
| 146 | + <div className={valueCls}> | ||
| 147 | + <input | ||
| 148 | + type="text" | ||
| 149 | + value={new Date().toISOString().slice(0, 10)} | ||
| 150 | + disabled | ||
| 151 | + className={`${inputCls} text-slate-500`} | ||
| 152 | + /> | ||
| 153 | + </div> | ||
| 154 | + | ||
| 155 | + {/* Row 2 */} | ||
| 156 | + <div className={labelCls}> | ||
| 157 | + <span className="text-rose-500">*</span> {t('label.customer')} | ||
| 158 | + </div> | ||
| 159 | + <div className={valueClsR}> | ||
| 160 | + <select | ||
| 161 | + required | ||
| 162 | + value={partnerCode} | ||
| 163 | + onChange={(e) => setPartnerCode(e.target.value)} | ||
| 164 | + className={`${inputCls} cursor-pointer`} | ||
| 165 | + > | ||
| 166 | + {partnerList.map((p) => ( | ||
| 167 | + <option key={p.id} value={p.code}> | ||
| 168 | + {p.code} — {p.name} | ||
| 169 | + </option> | ||
| 170 | + ))} | ||
| 171 | + </select> | ||
| 172 | + </div> | ||
| 173 | + <div className={labelCls}> | ||
| 174 | + {t('label.currency')} | ||
| 175 | + </div> | ||
| 176 | + <div className={valueCls}> | ||
| 177 | + <span className="text-sm text-slate-500">{currencyCode}</span> | ||
| 178 | + </div> | ||
| 179 | + | ||
| 180 | + {/* Row 3 — system / auto fields */} | ||
| 181 | + <div className={labelCls}> | ||
| 182 | + {t('label.createdBy')} | ||
| 183 | + </div> | ||
| 184 | + <div className={valueClsR}> | ||
| 185 | + <span className="text-sm text-slate-400">{t('label.autoGenerated')}</span> | ||
| 186 | + </div> | ||
| 187 | + <div className={labelCls}> | ||
| 188 | + {t('label.createdAt')} | ||
| 189 | + </div> | ||
| 190 | + <div className={valueCls}> | ||
| 191 | + <span className="text-sm text-slate-400">{t('label.autoGenerated')}</span> | ||
| 192 | + </div> | ||
| 127 | </div> | 193 | </div> |
| 128 | </div> | 194 | </div> |
| 129 | 195 | ||
| 130 | - <div> | ||
| 131 | - <div className="flex items-center justify-between mb-2"> | ||
| 132 | - <label className="text-sm font-medium text-slate-700">Order lines</label> | ||
| 133 | - <button type="button" className="btn-secondary text-xs" onClick={addLine}> | ||
| 134 | - + Add line | ||
| 135 | - </button> | ||
| 136 | - </div> | ||
| 137 | - <div className="space-y-2"> | ||
| 138 | - {lines.map((line, idx) => ( | ||
| 139 | - <div key={idx} className="flex items-center gap-2"> | ||
| 140 | - <span className="w-6 text-xs text-slate-400 text-right">{idx + 1}</span> | ||
| 141 | - <select | ||
| 142 | - value={line.itemCode} | ||
| 143 | - onChange={(e) => updateLine(idx, 'itemCode', e.target.value)} | ||
| 144 | - className="flex-1 rounded-md border border-slate-300 px-2 py-1.5 text-sm" | ||
| 145 | - > | ||
| 146 | - <option value="">Select item...</option> | ||
| 147 | - {items.map((it) => ( | ||
| 148 | - <option key={it.id} value={it.code}> | ||
| 149 | - {it.code} — {it.name} | ||
| 150 | - </option> | ||
| 151 | - ))} | ||
| 152 | - </select> | ||
| 153 | - <input | ||
| 154 | - type="number" | ||
| 155 | - min="1" | ||
| 156 | - step="1" | ||
| 157 | - placeholder="Qty" | ||
| 158 | - value={line.quantity} | ||
| 159 | - onChange={(e) => updateLine(idx, 'quantity', e.target.value)} | ||
| 160 | - className="w-20 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" | ||
| 161 | - /> | ||
| 162 | - <input | ||
| 163 | - type="number" | ||
| 164 | - min="0" | ||
| 165 | - step="0.01" | ||
| 166 | - placeholder="Price" | ||
| 167 | - value={line.unitPrice} | ||
| 168 | - onChange={(e) => updateLine(idx, 'unitPrice', e.target.value)} | ||
| 169 | - className="w-24 rounded-md border border-slate-300 px-2 py-1.5 text-sm text-right" | ||
| 170 | - /> | ||
| 171 | - <button | ||
| 172 | - type="button" | ||
| 173 | - className="text-slate-400 hover:text-rose-500" | ||
| 174 | - onClick={() => removeLine(idx)} | ||
| 175 | - title="Remove line" | ||
| 176 | - > | ||
| 177 | - × | ||
| 178 | - </button> | ||
| 179 | - </div> | ||
| 180 | - ))} | ||
| 181 | - </div> | 196 | + {/* ════════ Tabs ════════ */} |
| 197 | + <div className="border-b border-slate-300"> | ||
| 198 | + <button | ||
| 199 | + type="button" | ||
| 200 | + className="px-4 py-2 text-sm border-b-2 border-blue-500 text-blue-600 font-medium" | ||
| 201 | + > | ||
| 202 | + {t('tab.orderLines')} | ||
| 203 | + </button> | ||
| 182 | </div> | 204 | </div> |
| 183 | 205 | ||
| 206 | + {/* ════════ Line items — spreadsheet table ════════ */} | ||
| 207 | + <div className="border border-slate-300 bg-white"> | ||
| 208 | + <table className="w-full text-sm"> | ||
| 209 | + <thead> | ||
| 210 | + <tr className="bg-slate-100 border-b border-slate-300"> | ||
| 211 | + <th className="px-2 py-2 text-center w-10 border-r border-slate-200">#</th> | ||
| 212 | + <th className="px-2 py-2 text-left border-r border-slate-200"> | ||
| 213 | + <span className="text-rose-500">*</span> {t('label.item')} | ||
| 214 | + </th> | ||
| 215 | + <th className="px-2 py-2 text-right border-r border-slate-200 w-24"> | ||
| 216 | + <span className="text-rose-500">*</span> {t('label.quantity')} | ||
| 217 | + </th> | ||
| 218 | + <th className="px-2 py-2 text-right border-r border-slate-200 w-28"> | ||
| 219 | + <span className="text-rose-500">*</span> {t('label.unitPrice')} | ||
| 220 | + </th> | ||
| 221 | + <th className="px-2 py-2 text-right border-r border-slate-200 w-28"> | ||
| 222 | + {t('label.lineTotal')} | ||
| 223 | + </th> | ||
| 224 | + <th className="px-2 py-2 text-center w-24">{t('label.actions')}</th> | ||
| 225 | + </tr> | ||
| 226 | + </thead> | ||
| 227 | + <tbody> | ||
| 228 | + {lines.map((line, idx) => ( | ||
| 229 | + <tr key={idx} className="border-b border-slate-200 hover:bg-sky-50"> | ||
| 230 | + <td className="px-2 py-1.5 text-center border-r border-slate-200 text-slate-400"> | ||
| 231 | + {idx + 1} | ||
| 232 | + </td> | ||
| 233 | + <td className="px-1 py-0.5 border-r border-slate-200"> | ||
| 234 | + <select | ||
| 235 | + value={line.itemCode} | ||
| 236 | + onChange={(e) => updateLine(idx, 'itemCode', e.target.value)} | ||
| 237 | + className="w-full border-0 outline-none text-sm py-1 bg-transparent" | ||
| 238 | + > | ||
| 239 | + <option value="">{t('label.selectItem')}</option> | ||
| 240 | + {items.map((it) => ( | ||
| 241 | + <option key={it.id} value={it.code}> | ||
| 242 | + {it.code} — {it.name} | ||
| 243 | + </option> | ||
| 244 | + ))} | ||
| 245 | + </select> | ||
| 246 | + </td> | ||
| 247 | + <td className="px-1 py-0.5 border-r border-slate-200"> | ||
| 248 | + <input | ||
| 249 | + type="number" | ||
| 250 | + min="1" | ||
| 251 | + step="1" | ||
| 252 | + placeholder="0" | ||
| 253 | + value={line.quantity} | ||
| 254 | + onChange={(e) => updateLine(idx, 'quantity', e.target.value)} | ||
| 255 | + className="w-full border-0 outline-none text-sm py-1 text-right bg-transparent" | ||
| 256 | + /> | ||
| 257 | + </td> | ||
| 258 | + <td className="px-1 py-0.5 border-r border-slate-200"> | ||
| 259 | + <input | ||
| 260 | + type="number" | ||
| 261 | + min="0" | ||
| 262 | + step="0.01" | ||
| 263 | + placeholder="0.00" | ||
| 264 | + value={line.unitPrice} | ||
| 265 | + onChange={(e) => updateLine(idx, 'unitPrice', e.target.value)} | ||
| 266 | + className="w-full border-0 outline-none text-sm py-1 text-right bg-transparent" | ||
| 267 | + /> | ||
| 268 | + </td> | ||
| 269 | + <td className="px-2 py-1.5 text-right border-r border-slate-200 text-slate-500"> | ||
| 270 | + {(Number(line.quantity || 0) * Number(line.unitPrice || 0)).toFixed(2)} | ||
| 271 | + </td> | ||
| 272 | + <td className="px-2 py-1.5 text-center"> | ||
| 273 | + <button | ||
| 274 | + type="button" | ||
| 275 | + onClick={addLine} | ||
| 276 | + title={t('action.addRow')} | ||
| 277 | + className="text-blue-500 hover:text-blue-700 mx-0.5 text-xs" | ||
| 278 | + > | ||
| 279 | + [+] | ||
| 280 | + </button> | ||
| 281 | + <button | ||
| 282 | + type="button" | ||
| 283 | + onClick={() => removeLine(idx)} | ||
| 284 | + title={t('action.removeRow')} | ||
| 285 | + className="text-rose-400 hover:text-rose-600 mx-0.5 text-xs" | ||
| 286 | + > | ||
| 287 | + [x] | ||
| 288 | + </button> | ||
| 289 | + </td> | ||
| 290 | + </tr> | ||
| 291 | + ))} | ||
| 292 | + </tbody> | ||
| 293 | + <tfoot> | ||
| 294 | + <tr className="bg-slate-50 border-t border-slate-300"> | ||
| 295 | + <td colSpan={2} className="px-2 py-1.5 text-right"> | ||
| 296 | + <button | ||
| 297 | + type="button" | ||
| 298 | + onClick={addLine} | ||
| 299 | + className="text-blue-600 hover:text-blue-800 text-xs font-medium" | ||
| 300 | + > | ||
| 301 | + {t('action.addLine')} | ||
| 302 | + </button> | ||
| 303 | + </td> | ||
| 304 | + <td colSpan={2} className="px-2 py-1.5 text-right font-medium text-slate-700"> | ||
| 305 | + {t('label.orderTotal')} | ||
| 306 | + </td> | ||
| 307 | + <td className="px-2 py-1.5 text-right font-semibold text-slate-800 border-r border-slate-200"> | ||
| 308 | + {orderTotal.toFixed(2)} | ||
| 309 | + </td> | ||
| 310 | + <td /> | ||
| 311 | + </tr> | ||
| 312 | + </tfoot> | ||
| 313 | + </table> | ||
| 314 | + </div> | ||
| 315 | + | ||
| 316 | + {/* ════════ Custom fields (Tier 1 ext) ════════ */} | ||
| 184 | <DynamicExtFields | 317 | <DynamicExtFields |
| 185 | entityName="SalesOrder" | 318 | entityName="SalesOrder" |
| 186 | values={ext} | 319 | values={ext} |
| 187 | onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))} | 320 | onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))} |
| 188 | /> | 321 | /> |
| 189 | 322 | ||
| 190 | - {error && <ErrorBox error={error} />} | ||
| 191 | - | ||
| 192 | - <div className="flex items-center gap-3 pt-2"> | ||
| 193 | - <button type="submit" className="btn-primary" disabled={submitting}> | ||
| 194 | - {submitting ? 'Creating...' : 'Create Sales Order'} | ||
| 195 | - </button> | ||
| 196 | - <span className="text-xs text-slate-400"> | ||
| 197 | - After creation, confirm the order to auto-generate work orders. | ||
| 198 | - </span> | ||
| 199 | - </div> | ||
| 200 | - </form> | ||
| 201 | - </div> | 323 | + {/* ════════ Error ════════ */} |
| 324 | + {error && ( | ||
| 325 | + <div className="max-w-2xl"> | ||
| 326 | + <ErrorBox error={error} /> | ||
| 327 | + </div> | ||
| 328 | + )} | ||
| 329 | + </div> | ||
| 330 | + </form> | ||
| 202 | ) | 331 | ) |
| 203 | } | 332 | } |