Commit 6354c9d3ef385fd6b6d2ecf4af8cb70d9c6234f7

Authored by zichun
1 parent 8b9e0ab2

fix(web): add DynamicExtFields to all create pages

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 &#39;@/api/client&#39;
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 &#39;@/api/client&#39;
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 &#39;@/api/client&#39;
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
... ...