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,7 +186,7 @@ class DemoSeedRunner(
186 val locations = listOf( 186 val locations = listOf(
187 DemoLocation("WH-RAW", "Raw Materials Warehouse", "WAREHOUSE"), 187 DemoLocation("WH-RAW", "Raw Materials Warehouse", "WAREHOUSE"),
188 DemoLocation("WH-FG", "Finished Goods Warehouse", "WAREHOUSE"), 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 val sql = """ 192 val sql = """
@@ -228,9 +228,9 @@ class DemoSeedRunner( @@ -228,9 +228,9 @@ class DemoSeedRunner(
228 ) 228 )
229 229
230 val balances = listOf( 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 val balanceSql = """ 236 val balanceSql = """
@@ -286,14 +286,13 @@ class DemoSeedRunner( @@ -286,14 +286,13 @@ class DemoSeedRunner(
286 286
287 // --- SO-001: Acme Publishing, 2 lines --- 287 // --- SO-001: Acme Publishing, 2 lines ---
288 val so1Id = UUID.randomUUID() 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 insertSalesOrder(so1Id, "DEMO-SO-001", "CUST-ACME", orderDate, "USD", so1Total, now, principal) 290 insertSalesOrder(so1Id, "DEMO-SO-001", "CUST-ACME", orderDate, "USD", so1Total, now, principal)
292 291
293 insertSalesOrderLine(so1Id, 1, "BROCHURE-A4", 292 insertSalesOrderLine(so1Id, 1, "BROCHURE-A4",
294 BigDecimal("1000.0000"), BigDecimal("0.5000"), "USD", now, principal) 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 // --- SO-002: Globex Print, 1 line --- 297 // --- SO-002: Globex Print, 1 line ---
299 val so2Id = UUID.randomUUID() 298 val so2Id = UUID.randomUUID()
web/src/pages/CreateLocationPage.tsx
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom' @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
3 import { inventory } from '@/api/client' 3 import { inventory } from '@/api/client'
4 import { PageHeader } from '@/components/PageHeader' 4 import { PageHeader } from '@/components/PageHeader'
5 import { ErrorBox } from '@/components/ErrorBox' 5 import { ErrorBox } from '@/components/ErrorBox'
  6 +import { DynamicExtFields } from '@/components/DynamicExtFields'
6 7
7 const LOCATION_TYPES = ['WAREHOUSE', 'BIN', 'VIRTUAL'] as const 8 const LOCATION_TYPES = ['WAREHOUSE', 'BIN', 'VIRTUAL'] as const
8 9
@@ -11,6 +12,7 @@ export function CreateLocationPage() { @@ -11,6 +12,7 @@ export function CreateLocationPage() {
11 const [code, setCode] = useState('') 12 const [code, setCode] = useState('')
12 const [name, setName] = useState('') 13 const [name, setName] = useState('')
13 const [type, setType] = useState<string>('WAREHOUSE') 14 const [type, setType] = useState<string>('WAREHOUSE')
  15 + const [ext, setExt] = useState<Record<string, unknown>>({})
14 const [submitting, setSubmitting] = useState(false) 16 const [submitting, setSubmitting] = useState(false)
15 const [error, setError] = useState<Error | null>(null) 17 const [error, setError] = useState<Error | null>(null)
16 18
@@ -19,7 +21,8 @@ export function CreateLocationPage() { @@ -19,7 +21,8 @@ export function CreateLocationPage() {
19 setError(null) 21 setError(null)
20 setSubmitting(true) 22 setSubmitting(true)
21 try { 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 navigate('/locations') 26 navigate('/locations')
24 } catch (err: unknown) { 27 } catch (err: unknown) {
25 setError(err instanceof Error ? err : new Error(String(err))) 28 setError(err instanceof Error ? err : new Error(String(err)))
@@ -53,6 +56,11 @@ export function CreateLocationPage() { @@ -53,6 +56,11 @@ export function CreateLocationPage() {
53 {LOCATION_TYPES.map((t) => <option key={t} value={t}>{t}</option>)} 56 {LOCATION_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
54 </select> 57 </select>
55 </div> 58 </div>
  59 + <DynamicExtFields
  60 + entityName="Location"
  61 + values={ext}
  62 + onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))}
  63 + />
56 {error && <ErrorBox error={error} />} 64 {error && <ErrorBox error={error} />}
57 <button type="submit" className="btn-primary" disabled={submitting}> 65 <button type="submit" className="btn-primary" disabled={submitting}>
58 {submitting ? 'Creating...' : 'Create Location'} 66 {submitting ? 'Creating...' : 'Create Location'}
web/src/pages/CreatePurchaseOrderPage.tsx
@@ -4,6 +4,7 @@ import { catalog, partners, purchaseOrders } from &#39;@/api/client&#39; @@ -4,6 +4,7 @@ import { catalog, partners, purchaseOrders } from &#39;@/api/client&#39;
4 import type { Item, Partner } from '@/types/api' 4 import type { Item, Partner } from '@/types/api'
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
  7 +import { DynamicExtFields } from '@/components/DynamicExtFields'
7 8
8 interface LineInput { 9 interface LineInput {
9 itemCode: string 10 itemCode: string
@@ -20,6 +21,7 @@ export function CreatePurchaseOrderPage() { @@ -20,6 +21,7 @@ export function CreatePurchaseOrderPage() {
20 const [lines, setLines] = useState<LineInput[]>([{ itemCode: '', quantity: '', unitPrice: '' }]) 21 const [lines, setLines] = useState<LineInput[]>([{ itemCode: '', quantity: '', unitPrice: '' }])
21 const [items, setItems] = useState<Item[]>([]) 22 const [items, setItems] = useState<Item[]>([])
22 const [supplierList, setSupplierList] = useState<Partner[]>([]) 23 const [supplierList, setSupplierList] = useState<Partner[]>([])
  24 + const [ext, setExt] = useState<Record<string, unknown>>({})
23 const [submitting, setSubmitting] = useState(false) 25 const [submitting, setSubmitting] = useState(false)
24 const [error, setError] = useState<Error | null>(null) 26 const [error, setError] = useState<Error | null>(null)
25 27
@@ -51,6 +53,7 @@ export function CreatePurchaseOrderPage() { @@ -51,6 +53,7 @@ export function CreatePurchaseOrderPage() {
51 setError(null) 53 setError(null)
52 setSubmitting(true) 54 setSubmitting(true)
53 try { 55 try {
  56 + const extPayload = Object.keys(ext).length > 0 ? ext : undefined
54 const created = await purchaseOrders.create({ 57 const created = await purchaseOrders.create({
55 code, partnerCode, currencyCode, 58 code, partnerCode, currencyCode,
56 orderDate: new Date().toISOString().slice(0, 10), 59 orderDate: new Date().toISOString().slice(0, 10),
@@ -62,6 +65,7 @@ export function CreatePurchaseOrderPage() { @@ -62,6 +65,7 @@ export function CreatePurchaseOrderPage() {
62 unitPrice: Number(l.unitPrice), 65 unitPrice: Number(l.unitPrice),
63 currencyCode, 66 currencyCode,
64 })), 67 })),
  68 + ...(extPayload ? { ext: extPayload } : {}),
65 }) 69 })
66 navigate(`/purchase-orders/${created.id}`) 70 navigate(`/purchase-orders/${created.id}`)
67 } catch (err: unknown) { 71 } catch (err: unknown) {
@@ -128,6 +132,11 @@ export function CreatePurchaseOrderPage() { @@ -128,6 +132,11 @@ export function CreatePurchaseOrderPage() {
128 </div> 132 </div>
129 </div> 133 </div>
130 134
  135 + <DynamicExtFields
  136 + entityName="PurchaseOrder"
  137 + values={ext}
  138 + onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))}
  139 + />
131 {error && <ErrorBox error={error} />} 140 {error && <ErrorBox error={error} />}
132 <button type="submit" className="btn-primary" disabled={submitting}> 141 <button type="submit" className="btn-primary" disabled={submitting}>
133 {submitting ? 'Creating...' : 'Create Purchase Order'} 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,6 +4,7 @@ import { catalog, partners, salesOrders } from &#39;@/api/client&#39;
4 import type { Item, Partner } from '@/types/api' 4 import type { Item, Partner } from '@/types/api'
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
  7 +import { DynamicExtFields } from '@/components/DynamicExtFields'
7 8
8 interface LineInput { 9 interface LineInput {
9 itemCode: string 10 itemCode: string
@@ -21,6 +22,7 @@ export function CreateSalesOrderPage() { @@ -21,6 +22,7 @@ export function CreateSalesOrderPage() {
21 ]) 22 ])
22 const [items, setItems] = useState<Item[]>([]) 23 const [items, setItems] = useState<Item[]>([])
23 const [partnerList, setPartnerList] = useState<Partner[]>([]) 24 const [partnerList, setPartnerList] = useState<Partner[]>([])
  25 + const [ext, setExt] = useState<Record<string, unknown>>({})
24 const [submitting, setSubmitting] = useState(false) 26 const [submitting, setSubmitting] = useState(false)
25 const [error, setError] = useState<Error | null>(null) 27 const [error, setError] = useState<Error | null>(null)
26 28
@@ -52,6 +54,7 @@ export function CreateSalesOrderPage() { @@ -52,6 +54,7 @@ export function CreateSalesOrderPage() {
52 setError(null) 54 setError(null)
53 setSubmitting(true) 55 setSubmitting(true)
54 try { 56 try {
  57 + const extPayload = Object.keys(ext).length > 0 ? ext : undefined
55 const created = await salesOrders.create({ 58 const created = await salesOrders.create({
56 code, 59 code,
57 partnerCode, 60 partnerCode,
@@ -64,7 +67,8 @@ export function CreateSalesOrderPage() { @@ -64,7 +67,8 @@ export function CreateSalesOrderPage() {
64 unitPrice: Number(l.unitPrice), 67 unitPrice: Number(l.unitPrice),
65 currencyCode, 68 currencyCode,
66 })), 69 })),
67 - }) 70 + ...(extPayload ? { ext: extPayload } : {}),
  71 + } as Parameters<typeof salesOrders.create>[0])
68 navigate(`/sales-orders/${created.id}`) 72 navigate(`/sales-orders/${created.id}`)
69 } catch (err: unknown) { 73 } catch (err: unknown) {
70 setError(err instanceof Error ? err : new Error(String(err))) 74 setError(err instanceof Error ? err : new Error(String(err)))
@@ -177,6 +181,12 @@ export function CreateSalesOrderPage() { @@ -177,6 +181,12 @@ export function CreateSalesOrderPage() {
177 </div> 181 </div>
178 </div> 182 </div>
179 183
  184 + <DynamicExtFields
  185 + entityName="SalesOrder"
  186 + values={ext}
  187 + onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))}
  188 + />
  189 +
180 {error && <ErrorBox error={error} />} 190 {error && <ErrorBox error={error} />}
181 191
182 <div className="flex items-center gap-3 pt-2"> 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,6 +4,7 @@ import { catalog, inventory, production } from &#39;@/api/client&#39;
4 import type { Item, Location } from '@/types/api' 4 import type { Item, Location } from '@/types/api'
5 import { PageHeader } from '@/components/PageHeader' 5 import { PageHeader } from '@/components/PageHeader'
6 import { ErrorBox } from '@/components/ErrorBox' 6 import { ErrorBox } from '@/components/ErrorBox'
  7 +import { DynamicExtFields } from '@/components/DynamicExtFields'
7 8
8 interface BomLine { itemCode: string; quantityPerUnit: string; sourceLocationCode: string } 9 interface BomLine { itemCode: string; quantityPerUnit: string; sourceLocationCode: string }
9 interface OpLine { operationCode: string; workCenter: string; standardMinutes: string } 10 interface OpLine { operationCode: string; workCenter: string; standardMinutes: string }
@@ -18,6 +19,7 @@ export function CreateWorkOrderPage() { @@ -18,6 +19,7 @@ export function CreateWorkOrderPage() {
18 const [ops, setOps] = useState<OpLine[]>([]) 19 const [ops, setOps] = useState<OpLine[]>([])
19 const [items, setItems] = useState<Item[]>([]) 20 const [items, setItems] = useState<Item[]>([])
20 const [locations, setLocations] = useState<Location[]>([]) 21 const [locations, setLocations] = useState<Location[]>([])
  22 + const [ext, setExt] = useState<Record<string, unknown>>({})
21 const [submitting, setSubmitting] = useState(false) 23 const [submitting, setSubmitting] = useState(false)
22 const [error, setError] = useState<Error | null>(null) 24 const [error, setError] = useState<Error | null>(null)
23 25
@@ -47,6 +49,7 @@ export function CreateWorkOrderPage() { @@ -47,6 +49,7 @@ export function CreateWorkOrderPage() {
47 setError(null) 49 setError(null)
48 setSubmitting(true) 50 setSubmitting(true)
49 try { 51 try {
  52 + const extPayload = Object.keys(ext).length > 0 ? ext : undefined
50 const created = await production.createWorkOrder({ 53 const created = await production.createWorkOrder({
51 code, outputItemCode, 54 code, outputItemCode,
52 outputQuantity: Number(outputQuantity), 55 outputQuantity: Number(outputQuantity),
@@ -61,6 +64,7 @@ export function CreateWorkOrderPage() { @@ -61,6 +64,7 @@ export function CreateWorkOrderPage() {
61 workCenter: o.workCenter, 64 workCenter: o.workCenter,
62 standardMinutes: Number(o.standardMinutes), 65 standardMinutes: Number(o.standardMinutes),
63 })), 66 })),
  67 + ...(extPayload ? { ext: extPayload } : {}),
64 }) 68 })
65 navigate(`/work-orders/${created.id}`) 69 navigate(`/work-orders/${created.id}`)
66 } catch (err: unknown) { 70 } catch (err: unknown) {
@@ -160,6 +164,11 @@ export function CreateWorkOrderPage() { @@ -160,6 +164,11 @@ export function CreateWorkOrderPage() {
160 </div> 164 </div>
161 </div> 165 </div>
162 166
  167 + <DynamicExtFields
  168 + entityName="WorkOrder"
  169 + values={ext}
  170 + onChange={(k, v) => setExt((prev) => ({ ...prev, [k]: v }))}
  171 + />
163 {error && <ErrorBox error={error} />} 172 {error && <ErrorBox error={error} />}
164 <button type="submit" className="btn-primary" disabled={submitting}> 173 <button type="submit" className="btn-primary" disabled={submitting}>
165 {submitting ? 'Creating...' : 'Create Work Order'} 174 {submitting ? 'Creating...' : 'Create Work Order'}
web/src/pages/MetadataAdminPage.tsx
@@ -145,6 +145,8 @@ export function MetadataAdminPage() { @@ -145,6 +145,8 @@ export function MetadataAdminPage() {
145 const [cfPii, setCfPii] = useState(false) 145 const [cfPii, setCfPii] = useState(false)
146 const [cfLabelEn, setCfLabelEn] = useState('') 146 const [cfLabelEn, setCfLabelEn] = useState('')
147 const [cfLabelZh, setCfLabelZh] = useState('') 147 const [cfLabelZh, setCfLabelZh] = useState('')
  148 + const [cfAllowedValues, setCfAllowedValues] = useState('')
  149 + const [cfMaxLength, setCfMaxLength] = useState('')
148 const [cfSaving, setCfSaving] = useState(false) 150 const [cfSaving, setCfSaving] = useState(false)
149 151
150 // ── Loaders ─────────────────────────────────────────────────────── 152 // ── Loaders ───────────────────────────────────────────────────────
@@ -192,6 +194,8 @@ export function MetadataAdminPage() { @@ -192,6 +194,8 @@ export function MetadataAdminPage() {
192 setCfTargetEntity('') 194 setCfTargetEntity('')
193 setCfFieldKey('') 195 setCfFieldKey('')
194 setCfTypeKind('string') 196 setCfTypeKind('string')
  197 + setCfAllowedValues('')
  198 + setCfMaxLength('')
195 setCfRequired(false) 199 setCfRequired(false)
196 setCfPii(false) 200 setCfPii(false)
197 setCfLabelEn('') 201 setCfLabelEn('')
@@ -222,7 +226,11 @@ export function MetadataAdminPage() { @@ -222,7 +226,11 @@ export function MetadataAdminPage() {
222 const body: Omit<CustomFieldDef, 'source'> = { 226 const body: Omit<CustomFieldDef, 'source'> = {
223 key: cfFieldKey, 227 key: cfFieldKey,
224 targetEntity: cfTargetEntity, 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 required: cfRequired, 234 required: cfRequired,
227 pii: cfPii, 235 pii: cfPii,
228 labelTranslations: { 236 labelTranslations: {
@@ -613,6 +621,34 @@ export function MetadataAdminPage() { @@ -613,6 +621,34 @@ export function MetadataAdminPage() {
613 ))} 621 ))}
614 </select> 622 </select>
615 </div> 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 <div className="flex items-end gap-4 pb-1"> 652 <div className="flex items-end gap-4 pb-1">
617 <label className="flex items-center gap-1.5 text-sm text-slate-700"> 653 <label className="flex items-center gap-1.5 text-sm text-slate-700">
618 <input 654 <input