Commit 6354c9d3ef385fd6b6d2ecf4af8cb70d9c6234f7
1 parent
8b9e0ab2
fix(web): add DynamicExtFields to all create pages
Showing
6 changed files
with
82 additions
and
11 deletions
distribution/src/main/kotlin/org/vibeerp/demo/DemoSeedRunner.kt
| ... | ... | @@ -186,7 +186,7 @@ class DemoSeedRunner( |
| 186 | 186 | val locations = listOf( |
| 187 | 187 | DemoLocation("WH-RAW", "Raw Materials Warehouse", "WAREHOUSE"), |
| 188 | 188 | DemoLocation("WH-FG", "Finished Goods Warehouse", "WAREHOUSE"), |
| 189 | - DemoLocation("WH-QUARANTINE", "Quarantine Area", "QUARANTINE"), | |
| 189 | + DemoLocation("WH-STAGING", "Staging Area", "VIRTUAL"), | |
| 190 | 190 | ) |
| 191 | 191 | |
| 192 | 192 | val sql = """ |
| ... | ... | @@ -228,9 +228,9 @@ class DemoSeedRunner( |
| 228 | 228 | ) |
| 229 | 229 | |
| 230 | 230 | val balances = listOf( |
| 231 | - DemoBalance("PAPER-A3-120G", "WH-RAW", BigDecimal("5000.0000")), | |
| 232 | - DemoBalance("INK-CMYK-BLACK", "WH-RAW", BigDecimal("200.0000")), | |
| 233 | - DemoBalance("BROCHURE-A4", "WH-FG", BigDecimal("500.0000")), | |
| 231 | + DemoBalance("PAPER-A3-120G", "WH-RAW", BigDecimal("10000.0000")), | |
| 232 | + DemoBalance("INK-CMYK-BLACK", "WH-RAW", BigDecimal("1000.0000")), | |
| 233 | + DemoBalance("BROCHURE-A4", "WH-FG", BigDecimal("5000.0000")), | |
| 234 | 234 | ) |
| 235 | 235 | |
| 236 | 236 | val balanceSql = """ |
| ... | ... | @@ -286,14 +286,13 @@ class DemoSeedRunner( |
| 286 | 286 | |
| 287 | 287 | // --- SO-001: Acme Publishing, 2 lines --- |
| 288 | 288 | val so1Id = UUID.randomUUID() |
| 289 | - val so1Total = BigDecimal("1000.0000") | |
| 290 | - .add(BigDecimal("400.0000")) // 1000 * 0.50 + 5 * 80 | |
| 289 | + val so1Total = BigDecimal("900.0000") // 1000 * 0.50 + 500 * 0.80 | |
| 291 | 290 | insertSalesOrder(so1Id, "DEMO-SO-001", "CUST-ACME", orderDate, "USD", so1Total, now, principal) |
| 292 | 291 | |
| 293 | 292 | insertSalesOrderLine(so1Id, 1, "BROCHURE-A4", |
| 294 | 293 | BigDecimal("1000.0000"), BigDecimal("0.5000"), "USD", now, principal) |
| 295 | - insertSalesOrderLine(so1Id, 2, "DESIGN-HOURLY", | |
| 296 | - BigDecimal("5.0000"), BigDecimal("80.0000"), "USD", now, principal) | |
| 294 | + insertSalesOrderLine(so1Id, 2, "BROCHURE-A4", | |
| 295 | + BigDecimal("500.0000"), BigDecimal("0.8000"), "USD", now, principal) | |
| 297 | 296 | |
| 298 | 297 | // --- SO-002: Globex Print, 1 line --- |
| 299 | 298 | val so2Id = UUID.randomUUID() | ... | ... |
web/src/pages/CreateLocationPage.tsx
| ... | ... | @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom' |
| 3 | 3 | import { inventory } from '@/api/client' |
| 4 | 4 | import { PageHeader } from '@/components/PageHeader' |
| 5 | 5 | import { ErrorBox } from '@/components/ErrorBox' |
| 6 | +import { DynamicExtFields } from '@/components/DynamicExtFields' | |
| 6 | 7 | |
| 7 | 8 | const LOCATION_TYPES = ['WAREHOUSE', 'BIN', 'VIRTUAL'] as const |
| 8 | 9 | |
| ... | ... | @@ -11,6 +12,7 @@ export function CreateLocationPage() { |
| 11 | 12 | const [code, setCode] = useState('') |
| 12 | 13 | const [name, setName] = useState('') |
| 13 | 14 | const [type, setType] = useState<string>('WAREHOUSE') |
| 15 | + const [ext, setExt] = useState<Record<string, unknown>>({}) | |
| 14 | 16 | const [submitting, setSubmitting] = useState(false) |
| 15 | 17 | const [error, setError] = useState<Error | null>(null) |
| 16 | 18 | |
| ... | ... | @@ -19,7 +21,8 @@ export function CreateLocationPage() { |
| 19 | 21 | setError(null) |
| 20 | 22 | setSubmitting(true) |
| 21 | 23 | try { |
| 22 | - await inventory.createLocation({ code, name, type }) | |
| 24 | + const extPayload = Object.keys(ext).length > 0 ? ext : undefined | |
| 25 | + await inventory.createLocation({ code, name, type, ...(extPayload ? { ext: extPayload } : {}) }) | |
| 23 | 26 | navigate('/locations') |
| 24 | 27 | } catch (err: unknown) { |
| 25 | 28 | setError(err instanceof Error ? err : new Error(String(err))) |
| ... | ... | @@ -53,6 +56,11 @@ export function CreateLocationPage() { |
| 53 | 56 | {LOCATION_TYPES.map((t) => <option key={t} value={t}>{t}</option>)} |
| 54 | 57 | </select> |
| 55 | 58 | </div> |
| 59 | + <DynamicExtFields | |
| 60 | + entityName="Location" | |
| 61 | + values={ext} | |
| 62 | + onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))} | |
| 63 | + /> | |
| 56 | 64 | {error && <ErrorBox error={error} />} |
| 57 | 65 | <button type="submit" className="btn-primary" disabled={submitting}> |
| 58 | 66 | {submitting ? 'Creating...' : 'Create Location'} | ... | ... |
web/src/pages/CreatePurchaseOrderPage.tsx
| ... | ... | @@ -4,6 +4,7 @@ import { catalog, partners, purchaseOrders } from '@/api/client' |
| 4 | 4 | import type { Item, Partner } from '@/types/api' |
| 5 | 5 | import { PageHeader } from '@/components/PageHeader' |
| 6 | 6 | import { ErrorBox } from '@/components/ErrorBox' |
| 7 | +import { DynamicExtFields } from '@/components/DynamicExtFields' | |
| 7 | 8 | |
| 8 | 9 | interface LineInput { |
| 9 | 10 | itemCode: string |
| ... | ... | @@ -20,6 +21,7 @@ export function CreatePurchaseOrderPage() { |
| 20 | 21 | const [lines, setLines] = useState<LineInput[]>([{ itemCode: '', quantity: '', unitPrice: '' }]) |
| 21 | 22 | const [items, setItems] = useState<Item[]>([]) |
| 22 | 23 | const [supplierList, setSupplierList] = useState<Partner[]>([]) |
| 24 | + const [ext, setExt] = useState<Record<string, unknown>>({}) | |
| 23 | 25 | const [submitting, setSubmitting] = useState(false) |
| 24 | 26 | const [error, setError] = useState<Error | null>(null) |
| 25 | 27 | |
| ... | ... | @@ -51,6 +53,7 @@ export function CreatePurchaseOrderPage() { |
| 51 | 53 | setError(null) |
| 52 | 54 | setSubmitting(true) |
| 53 | 55 | try { |
| 56 | + const extPayload = Object.keys(ext).length > 0 ? ext : undefined | |
| 54 | 57 | const created = await purchaseOrders.create({ |
| 55 | 58 | code, partnerCode, currencyCode, |
| 56 | 59 | orderDate: new Date().toISOString().slice(0, 10), |
| ... | ... | @@ -62,6 +65,7 @@ export function CreatePurchaseOrderPage() { |
| 62 | 65 | unitPrice: Number(l.unitPrice), |
| 63 | 66 | currencyCode, |
| 64 | 67 | })), |
| 68 | + ...(extPayload ? { ext: extPayload } : {}), | |
| 65 | 69 | }) |
| 66 | 70 | navigate(`/purchase-orders/${created.id}`) |
| 67 | 71 | } catch (err: unknown) { |
| ... | ... | @@ -128,6 +132,11 @@ export function CreatePurchaseOrderPage() { |
| 128 | 132 | </div> |
| 129 | 133 | </div> |
| 130 | 134 | |
| 135 | + <DynamicExtFields | |
| 136 | + entityName="PurchaseOrder" | |
| 137 | + values={ext} | |
| 138 | + onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))} | |
| 139 | + /> | |
| 131 | 140 | {error && <ErrorBox error={error} />} |
| 132 | 141 | <button type="submit" className="btn-primary" disabled={submitting}> |
| 133 | 142 | {submitting ? 'Creating...' : 'Create Purchase Order'} | ... | ... |
web/src/pages/CreateSalesOrderPage.tsx
| ... | ... | @@ -4,6 +4,7 @@ import { catalog, partners, salesOrders } from '@/api/client' |
| 4 | 4 | import type { Item, Partner } from '@/types/api' |
| 5 | 5 | import { PageHeader } from '@/components/PageHeader' |
| 6 | 6 | import { ErrorBox } from '@/components/ErrorBox' |
| 7 | +import { DynamicExtFields } from '@/components/DynamicExtFields' | |
| 7 | 8 | |
| 8 | 9 | interface LineInput { |
| 9 | 10 | itemCode: string |
| ... | ... | @@ -21,6 +22,7 @@ export function CreateSalesOrderPage() { |
| 21 | 22 | ]) |
| 22 | 23 | const [items, setItems] = useState<Item[]>([]) |
| 23 | 24 | const [partnerList, setPartnerList] = useState<Partner[]>([]) |
| 25 | + const [ext, setExt] = useState<Record<string, unknown>>({}) | |
| 24 | 26 | const [submitting, setSubmitting] = useState(false) |
| 25 | 27 | const [error, setError] = useState<Error | null>(null) |
| 26 | 28 | |
| ... | ... | @@ -52,6 +54,7 @@ export function CreateSalesOrderPage() { |
| 52 | 54 | setError(null) |
| 53 | 55 | setSubmitting(true) |
| 54 | 56 | try { |
| 57 | + const extPayload = Object.keys(ext).length > 0 ? ext : undefined | |
| 55 | 58 | const created = await salesOrders.create({ |
| 56 | 59 | code, |
| 57 | 60 | partnerCode, |
| ... | ... | @@ -64,7 +67,8 @@ export function CreateSalesOrderPage() { |
| 64 | 67 | unitPrice: Number(l.unitPrice), |
| 65 | 68 | currencyCode, |
| 66 | 69 | })), |
| 67 | - }) | |
| 70 | + ...(extPayload ? { ext: extPayload } : {}), | |
| 71 | + } as Parameters<typeof salesOrders.create>[0]) | |
| 68 | 72 | navigate(`/sales-orders/${created.id}`) |
| 69 | 73 | } catch (err: unknown) { |
| 70 | 74 | setError(err instanceof Error ? err : new Error(String(err))) |
| ... | ... | @@ -177,6 +181,12 @@ export function CreateSalesOrderPage() { |
| 177 | 181 | </div> |
| 178 | 182 | </div> |
| 179 | 183 | |
| 184 | + <DynamicExtFields | |
| 185 | + entityName="SalesOrder" | |
| 186 | + values={ext} | |
| 187 | + onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))} | |
| 188 | + /> | |
| 189 | + | |
| 180 | 190 | {error && <ErrorBox error={error} />} |
| 181 | 191 | |
| 182 | 192 | <div className="flex items-center gap-3 pt-2"> | ... | ... |
web/src/pages/CreateWorkOrderPage.tsx
| ... | ... | @@ -4,6 +4,7 @@ import { catalog, inventory, production } from '@/api/client' |
| 4 | 4 | import type { Item, Location } from '@/types/api' |
| 5 | 5 | import { PageHeader } from '@/components/PageHeader' |
| 6 | 6 | import { ErrorBox } from '@/components/ErrorBox' |
| 7 | +import { DynamicExtFields } from '@/components/DynamicExtFields' | |
| 7 | 8 | |
| 8 | 9 | interface BomLine { itemCode: string; quantityPerUnit: string; sourceLocationCode: string } |
| 9 | 10 | interface OpLine { operationCode: string; workCenter: string; standardMinutes: string } |
| ... | ... | @@ -18,6 +19,7 @@ export function CreateWorkOrderPage() { |
| 18 | 19 | const [ops, setOps] = useState<OpLine[]>([]) |
| 19 | 20 | const [items, setItems] = useState<Item[]>([]) |
| 20 | 21 | const [locations, setLocations] = useState<Location[]>([]) |
| 22 | + const [ext, setExt] = useState<Record<string, unknown>>({}) | |
| 21 | 23 | const [submitting, setSubmitting] = useState(false) |
| 22 | 24 | const [error, setError] = useState<Error | null>(null) |
| 23 | 25 | |
| ... | ... | @@ -47,6 +49,7 @@ export function CreateWorkOrderPage() { |
| 47 | 49 | setError(null) |
| 48 | 50 | setSubmitting(true) |
| 49 | 51 | try { |
| 52 | + const extPayload = Object.keys(ext).length > 0 ? ext : undefined | |
| 50 | 53 | const created = await production.createWorkOrder({ |
| 51 | 54 | code, outputItemCode, |
| 52 | 55 | outputQuantity: Number(outputQuantity), |
| ... | ... | @@ -61,6 +64,7 @@ export function CreateWorkOrderPage() { |
| 61 | 64 | workCenter: o.workCenter, |
| 62 | 65 | standardMinutes: Number(o.standardMinutes), |
| 63 | 66 | })), |
| 67 | + ...(extPayload ? { ext: extPayload } : {}), | |
| 64 | 68 | }) |
| 65 | 69 | navigate(`/work-orders/${created.id}`) |
| 66 | 70 | } catch (err: unknown) { |
| ... | ... | @@ -160,6 +164,11 @@ export function CreateWorkOrderPage() { |
| 160 | 164 | </div> |
| 161 | 165 | </div> |
| 162 | 166 | |
| 167 | + <DynamicExtFields | |
| 168 | + entityName="WorkOrder" | |
| 169 | + values={ext} | |
| 170 | + onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))} | |
| 171 | + /> | |
| 163 | 172 | {error && <ErrorBox error={error} />} |
| 164 | 173 | <button type="submit" className="btn-primary" disabled={submitting}> |
| 165 | 174 | {submitting ? 'Creating...' : 'Create Work Order'} | ... | ... |
web/src/pages/MetadataAdminPage.tsx
| ... | ... | @@ -145,6 +145,8 @@ export function MetadataAdminPage() { |
| 145 | 145 | const [cfPii, setCfPii] = useState(false) |
| 146 | 146 | const [cfLabelEn, setCfLabelEn] = useState('') |
| 147 | 147 | const [cfLabelZh, setCfLabelZh] = useState('') |
| 148 | + const [cfAllowedValues, setCfAllowedValues] = useState('') | |
| 149 | + const [cfMaxLength, setCfMaxLength] = useState('') | |
| 148 | 150 | const [cfSaving, setCfSaving] = useState(false) |
| 149 | 151 | |
| 150 | 152 | // ── Loaders ─────────────────────────────────────────────────────── |
| ... | ... | @@ -192,6 +194,8 @@ export function MetadataAdminPage() { |
| 192 | 194 | setCfTargetEntity('') |
| 193 | 195 | setCfFieldKey('') |
| 194 | 196 | setCfTypeKind('string') |
| 197 | + setCfAllowedValues('') | |
| 198 | + setCfMaxLength('') | |
| 195 | 199 | setCfRequired(false) |
| 196 | 200 | setCfPii(false) |
| 197 | 201 | setCfLabelEn('') |
| ... | ... | @@ -222,7 +226,11 @@ export function MetadataAdminPage() { |
| 222 | 226 | const body: Omit<CustomFieldDef, 'source'> = { |
| 223 | 227 | key: cfFieldKey, |
| 224 | 228 | targetEntity: cfTargetEntity, |
| 225 | - type: { kind: cfTypeKind }, | |
| 229 | + type: { | |
| 230 | + kind: cfTypeKind, | |
| 231 | + ...(cfTypeKind === 'enum' && cfAllowedValues ? { allowedValues: cfAllowedValues.split(',').map((v) => v.trim()).filter(Boolean) } : {}), | |
| 232 | + ...(cfTypeKind === 'string' && cfMaxLength ? { maxLength: Number(cfMaxLength) } : {}), | |
| 233 | + }, | |
| 226 | 234 | required: cfRequired, |
| 227 | 235 | pii: cfPii, |
| 228 | 236 | labelTranslations: { |
| ... | ... | @@ -613,6 +621,34 @@ export function MetadataAdminPage() { |
| 613 | 621 | ))} |
| 614 | 622 | </select> |
| 615 | 623 | </div> |
| 624 | + {cfTypeKind === 'enum' && ( | |
| 625 | + <div> | |
| 626 | + <label className="block text-xs font-medium text-slate-700"> | |
| 627 | + Allowed values (comma-separated) | |
| 628 | + </label> | |
| 629 | + <input | |
| 630 | + type="text" | |
| 631 | + value={cfAllowedValues} | |
| 632 | + onChange={(e) => setCfAllowedValues(e.target.value)} | |
| 633 | + placeholder="LOW, NORMAL, HIGH, URGENT" | |
| 634 | + className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" | |
| 635 | + /> | |
| 636 | + </div> | |
| 637 | + )} | |
| 638 | + {cfTypeKind === 'string' && ( | |
| 639 | + <div> | |
| 640 | + <label className="block text-xs font-medium text-slate-700"> | |
| 641 | + Max length | |
| 642 | + </label> | |
| 643 | + <input | |
| 644 | + type="number" | |
| 645 | + value={cfMaxLength} | |
| 646 | + onChange={(e) => setCfMaxLength(e.target.value)} | |
| 647 | + placeholder="255" | |
| 648 | + className="mt-1 w-full rounded-md border border-slate-300 px-2 py-1.5 text-sm" | |
| 649 | + /> | |
| 650 | + </div> | |
| 651 | + )} | |
| 616 | 652 | <div className="flex items-end gap-4 pb-1"> |
| 617 | 653 | <label className="flex items-center gap-1.5 text-sm text-slate-700"> |
| 618 | 654 | <input | ... | ... |