@svgrid/enterprise 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +95 -81
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +22468 -10459
  3. package/dist/node/studio.js +11590 -2907
  4. package/package.json +31 -10
  5. package/src/SvAlertRuleEditor.svelte +294 -0
  6. package/src/SvAlertsManager.svelte +210 -0
  7. package/src/SvAlertsPanel.svelte +129 -0
  8. package/src/SvBoard.svelte +3 -1
  9. package/src/SvExpressionEditor.svelte +341 -0
  10. package/src/SvGridAlerts.dom.test.ts +113 -0
  11. package/src/SvGridAlerts.svelte +265 -0
  12. package/src/SvGridBoard.svelte +2355 -0
  13. package/src/SvGridEditPanel.svelte +849 -794
  14. package/src/SvGridScheduler.svelte +5334 -4410
  15. package/src/SvPivotDesigner.svelte +3 -3
  16. package/src/SvRecordDetail.svelte +6 -2
  17. package/src/SvSchedule.svelte +3 -1
  18. package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +4 -1
  19. package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +4 -1
  20. package/src/{ai-export.test.ts → ai-export.dom.test.ts} +5 -1
  21. package/src/alerts/alert-engine-attach.ts +165 -0
  22. package/src/alerts/alert-engine.test.ts +135 -0
  23. package/src/alerts/alert-engine.ts +260 -0
  24. package/src/alerts/alert-formats.test.ts +87 -0
  25. package/src/alerts/alert-formats.ts +77 -0
  26. package/src/alerts/alert-observer.test.ts +189 -0
  27. package/src/alerts/alert-observer.ts +208 -0
  28. package/src/alerts/alert-scheduler.ts +96 -0
  29. package/src/alerts/alert-storage.test.ts +54 -0
  30. package/src/alerts/alert-storage.ts +116 -0
  31. package/src/alerts/alert-store.svelte.ts +80 -0
  32. package/src/alerts/alert-types.ts +94 -0
  33. package/src/alerts.ts +28 -0
  34. package/src/board.dom.test.ts +941 -0
  35. package/src/board.ts +36 -0
  36. package/src/export.ts +6 -0
  37. package/src/expressions/evaluate.test.ts +111 -0
  38. package/src/expressions/evaluate.ts +228 -0
  39. package/src/expressions/expression-columns.ts +133 -0
  40. package/src/expressions/expression-types.ts +84 -0
  41. package/src/expressions/parse.test.ts +106 -0
  42. package/src/expressions/parse.ts +610 -0
  43. package/src/import.ts +2 -0
  44. package/src/index.ts +180 -46
  45. package/src/install.ts +12 -2
  46. package/src/pivot-enable.ts +44 -0
  47. package/src/scheduler-assignments.test.ts +97 -0
  48. package/src/scheduler-assignments.ts +134 -0
  49. package/src/scheduler-axis.test.ts +108 -0
  50. package/src/scheduler-axis.ts +238 -0
  51. package/src/scheduler-booking.test.ts +57 -0
  52. package/src/scheduler-booking.ts +63 -0
  53. package/src/scheduler-config.ts +179 -0
  54. package/src/scheduler-dependencies.test.ts +155 -0
  55. package/src/scheduler-dependencies.ts +223 -0
  56. package/src/scheduler-freebusy.test.ts +41 -0
  57. package/src/scheduler-freebusy.ts +36 -0
  58. package/src/scheduler-heatmap.test.ts +39 -0
  59. package/src/scheduler-heatmap.ts +55 -0
  60. package/src/scheduler-resource-tree.test.ts +84 -0
  61. package/src/scheduler-resource-tree.ts +107 -0
  62. package/src/scheduler-slots.test.ts +53 -0
  63. package/src/scheduler-slots.ts +94 -0
  64. package/src/scheduler-summary.test.ts +47 -0
  65. package/src/scheduler-summary.ts +81 -0
  66. package/src/sources/index.ts +1 -1
  67. package/src/sources/introspect-supabase.test.ts +13 -1
  68. package/src/sources/introspect-supabase.ts +20 -0
  69. package/src/studio/copilot-core.test.ts +45 -0
  70. package/src/studio/copilot-core.ts +65 -0
  71. package/src/studio/deploy-cli.test.ts +56 -0
  72. package/src/studio/deploy-cli.ts +100 -0
  73. package/src/studio/emit-project.test.ts +503 -18
  74. package/src/studio/emit-project.ts +829 -111
  75. package/src/studio/emit-schema.test.ts +17 -0
  76. package/src/studio/emit-schema.ts +166 -35
  77. package/src/studio/index.ts +25 -1
  78. package/src/studio/introspect-openapi.test.ts +84 -0
  79. package/src/studio/introspect-openapi.ts +252 -0
  80. package/src/studio/project.test.ts +57 -0
  81. package/src/studio/project.ts +193 -6
  82. package/src/studio/samples/crm.ts +282 -258
  83. package/src/studio/samples/fleet.ts +213 -186
  84. package/src/studio/samples/insurance.ts +223 -195
  85. package/src/studio/samples/inventory.ts +213 -182
  86. package/src/studio/samples/live-data.test.ts +99 -98
  87. package/src/studio/samples/live-data.ts +8 -10
  88. package/src/studio/samples/projects.ts +212 -190
  89. package/src/studio/samples/samples.test.ts +52 -0
  90. package/src/studio/samples/shared.ts +26 -6
  91. package/src/studio/samples/starter.ts +251 -0
  92. package/src/studio/samples/support.ts +209 -184
  93. package/src/studio/ui-components-surface.test.ts +185 -0
  94. package/src/studio/ui-components.generated.ts +5617 -0
  95. package/src/studio/ui-components.ts +641 -472
  96. package/src/sveltekit/sql-source.test.ts +13 -0
  97. package/src/sveltekit/sql-source.ts +11 -6
  98. package/src/upgrade-prompt.ts +2 -2
  99. package/src/watermark.ts +2 -2
  100. package/src/ai.test.ts +0 -522
  101. package/src/ai.ts +0 -1388
@@ -1,182 +1,213 @@
1
- import type { EntitySchema } from '../../schema.js'
2
- import type { FormatRule } from '../project.js'
3
- import { screen, formScreen, boardScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
-
5
- const suppliers: EntitySchema = {
6
- name: 'suppliers',
7
- label: 'Supplier',
8
- idField: 'id',
9
- fields: [
10
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
11
- { field: 'name', type: 'text', required: true, minLength: 2 },
12
- { field: 'country', type: 'text', label: 'Country', input: { editorType: 'country', help: 'Country of dispatch' } },
13
- { field: 'email', type: 'text', format: 'email', required: true, input: { placeholder: 'orders@supplier.com' } },
14
- { field: 'phone', type: 'text', input: { editorType: 'phone' } },
15
- { field: 'rating', type: 'number', label: 'Rating', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 quality score' } },
16
- { field: 'leadDays', type: 'number', label: 'Lead time (days)', min: 0, defaultValue: 14, input: { help: 'Typical days from order to delivery' } },
17
- { field: 'color', type: 'text', label: 'Accent', defaultValue: '#f59e0b', input: { editorType: 'color', help: 'Chart accent color' } },
18
- { field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
19
- ],
20
- }
21
-
22
- const products: EntitySchema = {
23
- name: 'products',
24
- label: 'Product',
25
- idField: 'id',
26
- fields: [
27
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
28
- { field: 'name', type: 'text', required: true, minLength: 2 },
29
- { field: 'sku', type: 'text', label: 'SKU', required: true, input: { editorType: 'mask', mask: 'AAA-####', help: 'Stock keeping unit' } },
30
- { field: 'category', type: 'enum', required: true, options: [
31
- { value: 'electronics', label: 'Electronics', color: '#6366f1' },
32
- { value: 'office', label: 'Office', color: '#0ea5e9' },
33
- { value: 'home', label: 'Home', color: '#10b981' },
34
- { value: 'tools', label: 'Tools', color: '#f59e0b' },
35
- ] },
36
- { field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Search and filter keywords' } },
37
- { field: 'price', type: 'number', label: 'Price ($)', min: 0, required: true },
38
- { field: 'discount', type: 'number', label: 'Discount (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Current promotional discount' } },
39
- { field: 'stock', type: 'number', label: 'In stock', min: 0, defaultValue: 0 },
40
- { field: 'reorderLevel', type: 'number', label: 'Reorder level', min: 0, defaultValue: 20, input: { help: 'Restock when stock drops below this' } },
41
- // Derived: value of stock on hand (display-only, never stored).
42
- { field: 'stockValue', type: 'number', label: 'Stock value ($)', readonly: true, formula: 'price * stock' },
43
- { field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
44
- ],
45
- }
46
-
47
- const purchaseOrders: EntitySchema = {
48
- name: 'purchaseOrders',
49
- label: 'Purchase order',
50
- idField: 'id',
51
- fields: [
52
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
53
- { field: 'ref', type: 'text', label: 'Ref', required: true, minLength: 2 },
54
- { field: 'supplierId', type: 'relation', label: 'Supplier', relation: { entity: 'suppliers', foreignKey: 'supplierId', labelField: 'name' } },
55
- { field: 'productId', type: 'relation', label: 'Product', relation: { entity: 'products', foreignKey: 'productId', labelField: 'name' } },
56
- { field: 'status', type: 'enum', defaultValue: 'draft', options: [
57
- { value: 'draft', label: 'Draft', color: '#94a3b8' },
58
- { value: 'ordered', label: 'Ordered', color: '#0ea5e9' },
59
- { value: 'received', label: 'Received', color: '#10b981' },
60
- { value: 'cancelled', label: 'Cancelled', color: '#ef4444' },
61
- ] },
62
- { field: 'qty', type: 'number', label: 'Quantity', min: 1, defaultValue: 1, required: true },
63
- { field: 'unitCost', type: 'number', label: 'Unit cost ($)', min: 0, defaultValue: 0 },
64
- // Derived: line total from quantity and unit cost (display-only, never stored).
65
- { field: 'total', type: 'number', label: 'Total ($)', readonly: true, formula: 'qty * unitCost' },
66
- { field: 'orderedAt', type: 'date', label: 'Ordered at', input: { editorType: 'datetime', help: 'When the order was placed' } },
67
- ],
68
- }
69
-
70
- const seed = {
71
- suppliers: [
72
- { id: 'sp1', name: 'Shenzhen Components', country: 'China', email: 'sales@shenzhencomp.com', phone: '+86 755 5550 101', rating: 5, leadDays: 21, color: '#6366f1', active: true },
73
- { id: 'sp2', name: 'Nordic Office Supply', country: 'Sweden', email: 'orders@nordicoffice.se', phone: '+46 8 5550 102', rating: 4, leadDays: 10, color: '#10b981', active: true },
74
- { id: 'sp3', name: 'Bavaria Tools', country: 'Germany', email: 'info@bavariatools.de', phone: '+49 89 5550103', rating: 5, leadDays: 14, color: '#f59e0b', active: true },
75
- { id: 'sp4', name: 'Pacific Home Goods', country: 'United States', email: 'hello@pacifichome.com', phone: '+1 (415) 555-0104', rating: 4, leadDays: 7, color: '#0ea5e9', active: true },
76
- { id: 'sp5', name: 'Kyoto Electronics', country: 'Japan', email: 'sales@kyotoelec.jp', phone: '+81 75 5550 105', rating: 5, leadDays: 18, color: '#8b5cf6', active: true },
77
- { id: 'sp6', name: 'Toronto Distributors', country: 'Canada', email: 'orders@torontodist.ca', phone: '+1 (416) 555-0106', rating: 4, leadDays: 12, color: '#ef4444', active: false },
78
- ],
79
- products: [
80
- { id: 'pr1', name: 'USB-C Hub', sku: 'ELE-0001', category: 'electronics', tags: ['usb-c', 'hub', 'charging'], price: 39, discount: 10, stock: 420, reorderLevel: 100, active: true },
81
- { id: 'pr2', name: 'Standing Desk', sku: 'OFF-0101', category: 'office', tags: ['adjustable', 'ergonomic'], price: 480, discount: 5, stock: 35, reorderLevel: 20, active: true },
82
- { id: 'pr3', name: 'Ceramic Mug Set', sku: 'HOM-0210', category: 'home', tags: ['kitchen', 'ceramic'], price: 24, discount: 0, stock: 610, reorderLevel: 150, active: true },
83
- { id: 'pr4', name: 'Cordless Drill', sku: 'TOO-0305', category: 'tools', tags: ['power', 'cordless', '18v'], price: 129, discount: 15, stock: 88, reorderLevel: 40, active: true },
84
- { id: 'pr5', name: 'Mechanical Keyboard', sku: 'ELE-0002', category: 'electronics', tags: ['mechanical', 'rgb', 'usb'], price: 95, discount: 8, stock: 240, reorderLevel: 80, active: true },
85
- { id: 'pr6', name: 'Ergonomic Chair', sku: 'OFF-0102', category: 'office', tags: ['ergonomic', 'mesh'], price: 320, discount: 12, stock: 52, reorderLevel: 25, active: true },
86
- { id: 'pr7', name: 'LED Desk Lamp', sku: 'HOM-0211', category: 'home', tags: ['lighting', 'led', 'dimmable'], price: 45, discount: 0, stock: 175, reorderLevel: 60, active: true },
87
- ],
88
- purchaseOrders: [
89
- { id: 'po1', ref: 'PO-4001', supplierId: 'sp1', productId: 'pr1', status: 'received', qty: 200, unitCost: 28, orderedAt: '2026-01-10T09:30:00.000Z' },
90
- { id: 'po2', ref: 'PO-4002', supplierId: 'sp2', productId: 'pr2', status: 'ordered', qty: 30, unitCost: 360, orderedAt: '2026-02-04T14:15:00.000Z' },
91
- { id: 'po3', ref: 'PO-4003', supplierId: 'sp3', productId: 'pr4', status: 'draft', qty: 25, unitCost: 92, orderedAt: '2026-02-18T11:00:00.000Z' },
92
- { id: 'po4', ref: 'PO-4004', supplierId: 'sp4', productId: 'pr3', status: 'cancelled', qty: 100, unitCost: 15, orderedAt: '2026-01-22T16:45:00.000Z' },
93
- { id: 'po5', ref: 'PO-4005', supplierId: 'sp5', productId: 'pr5', status: 'received', qty: 150, unitCost: 68, orderedAt: '2026-03-01T08:20:00.000Z' },
94
- { id: 'po6', ref: 'PO-4006', supplierId: 'sp6', productId: 'pr7', status: 'ordered', qty: 120, unitCost: 30, orderedAt: '2026-03-12T13:10:00.000Z' },
95
- ],
96
- }
97
-
98
- // Product grid formatting: category status pills, a LOW STOCK red-bold threshold
99
- // (stock under 10 units needs reordering now), and a green highlight on deep
100
- // promotional discounts - data-driven color, not just status pills.
101
- const productFormats: FormatRule[] = [
102
- ...statusPills(products, 'category'),
103
- { field: 'stock', op: 'lt', value: 10, color: '#dc2626', bold: true },
104
- { field: 'discount', op: 'gte', value: 15, color: '#16a34a', bold: true },
105
- ]
106
-
107
- export const inventory: SampleApp = {
108
- id: 'inventory',
109
- name: 'Inventory',
110
- description: 'Products, suppliers and purchase orders - stock and purchasing dashboards (target KPIs, an order-trend sparkline, a reorder gauge, a category pie, a supplier x status pivot, tabbed breakdowns), a low-stock red grid plus a status-pill order grid with row actions, suppliers master/detail, and rich edit forms (country, phone, supplier rating, SKU mask, tag chips, discount slider, order datetime).',
111
- emoji: '\u{1F4E6}',
112
- accent: '#f59e0b',
113
- build: () => {
114
- const supplierRows = pad(suppliers, seed.suppliers, 24)
115
- const productRows = pad(products, seed.products, 60)
116
- const poRows = pad(purchaseOrders, seed.purchaseOrders, 90, { supplierId: ids(supplierRows), productId: ids(productRows) })
117
- return project({
118
- title: 'Warehouse',
119
- brand: 'Warehouse',
120
- accent: '#f59e0b',
121
- preset: 'excel',
122
- footer: '',
123
- entities: [suppliers, products, purchaseOrders],
124
- seed: { suppliers: supplierRows, products: productRows, purchaseOrders: poRows },
125
- screens: [
126
- // Stock dashboard: KPI cards (units-in-stock card carries a 5,000-unit
127
- // target), a stock-level gauge, category breakdowns (bar + pie), a tabbed
128
- // view, and a status-pill product grid where LOW STOCK rows turn red-bold.
129
- dashScreen(products, { id: 'overview', title: 'Stock', order: 0 }, [
130
- { kpi: 'Products', reduce: 'count', span: 1 },
131
- { kpi: 'Units in stock', measure: 'stock', reduce: 'sum', target: 5000, span: 1 },
132
- { kpi: 'Avg price', measure: 'price', reduce: 'avg', format: 'currency', span: 1 },
133
- { gauge: 'Avg stock on hand', measure: 'stock', reduce: 'avg', min: 0, max: 500, span: 1 },
134
- { chart: 'category', measure: 'stock', reduce: 'sum', type: 'bar', span: 2 },
135
- { chart: 'category', reduce: 'count', type: 'pie', span: 1 },
136
- { tabs: [
137
- { label: 'Stock by category', tiles: [{ chart: 'category', measure: 'stock', reduce: 'sum', type: 'bar' }] },
138
- { label: 'Avg price by category', tiles: [{ chart: 'category', measure: 'price', reduce: 'avg', type: 'bar' }] },
139
- ], span: 3 },
140
- { grid: true, format: productFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
141
- ]),
142
- // Purchasing dashboard: order-volume + cost KPIs (units-ordered card trends
143
- // over the order date as a sparkline), an average-unit-cost gauge, status
144
- // breakdowns, an order-quantity-over-time area trend, a supplier x status
145
- // pivot, a tabbed view, and a status-pill purchase-order grid.
146
- dashScreen(purchaseOrders, { id: 'purchasing', title: 'Purchasing', order: 1 }, [
147
- { kpi: 'Purchase orders', reduce: 'count', span: 1 },
148
- { kpi: 'Units ordered', measure: 'qty', reduce: 'sum', trendField: 'orderedAt', trendReduce: 'sum', span: 1 },
149
- { kpi: 'Avg unit cost', measure: 'unitCost', reduce: 'avg', format: 'currency', span: 1 },
150
- { gauge: 'Avg unit cost', measure: 'unitCost', reduce: 'avg', min: 0, max: 400, unit: '$', span: 1 },
151
- { chart: 'status', measure: 'qty', reduce: 'sum', type: 'bar', span: 2 },
152
- { chart: 'status', reduce: 'count', type: 'pie', span: 1 },
153
- { chart: 'orderedAt', measure: 'qty', reduce: 'sum', type: 'area', span: 3 },
154
- { pivot: { rows: ['supplierId'], cols: ['status'], measure: 'qty', aggregate: 'sum' }, span: 3 },
155
- { tabs: [
156
- { label: 'Qty by status', tiles: [{ chart: 'status', measure: 'qty', reduce: 'sum', type: 'bar' }] },
157
- { label: 'Orders by status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
158
- ], span: 3 },
159
- { grid: true, format: statusPills(purchaseOrders, 'status'), summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
160
- ]),
161
- // Product detail: a full record page - header (name + SKU + category pill),
162
- // stock / price metric tiles, an Overview of pricing and reorder fields, and a
163
- // Purchase orders timeline of the orders placed for the product.
164
- detailScreen(products, { id: 'product-detail', title: 'Product detail', order: 2 }, {
165
- titleField: 'name', subtitleField: 'sku', statusField: 'category',
166
- metricFields: ['stock', 'price'],
167
- sections: [
168
- { label: 'Pricing', fields: ['discount', 'tags'] },
169
- { label: 'Stock', fields: ['reorderLevel', 'active'] },
170
- ],
171
- related: [{ entity: 'purchaseOrders', foreignKey: 'productId', label: 'Purchase orders', titleField: 'ref', subtitleField: 'qty', dateField: 'orderedAt', statusField: 'status' }],
172
- }),
173
- screen(suppliers, 'master-detail', { id: 'suppliers', title: 'Suppliers', order: 3, child: purchaseOrders, foreignKey: 'supplierId' }),
174
- formScreen(products, { id: 'products', title: 'Products', order: 4 }, undefined, { format: productFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'product-detail', sourceField: 'id', targetField: 'id' } }, ['category', 'active']),
175
- formScreen(purchaseOrders, { id: 'purchaseOrders', title: 'Purchase orders', order: 5 }, undefined, { format: statusPills(purchaseOrders, 'status'), summaries: true, rowActions: [{ kind: 'edit' }] }, ['status', 'supplierId']),
176
- // Purchasing board: purchase orders as draggable Kanban lanes by status
177
- // (Draft -> Ordered -> Received), card = ref + order total.
178
- boardScreen(purchaseOrders, { id: 'board', title: 'Orders board', order: 6 }, { groupBy: 'status', titleField: 'ref', badgeField: 'qty', subtitleField: 'supplierId', filter: ['status', 'supplierId'] }),
179
- ],
180
- })
181
- },
182
- }
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import type { FormatRule } from '../project.js'
3
+ import { screen, formScreen, boardScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
+
5
+ // Excel-style chrome: a green header band over the brand, a green command bar
6
+ // (the toolbar), square-cornered nav rows with a green left-marker on the active
7
+ // sheet. Pairs with the square-cornered `excel` preset.
8
+ const EXCEL_CSS = `.sv-app.theme-excel .sv-app__side { background: #f3f2f1; border-right: 1px solid #d0cfce; padding: 0 0 12px; }
9
+ .sv-app.theme-excel .sv-app__sidehead { background: #107c41; margin: 0 0 6px; padding: 13px 14px; }
10
+ .sv-app.theme-excel .sv-app__brandtext { color: #fff; font-weight: 700; }
11
+ .sv-app.theme-excel .sv-app__collapse { color: rgba(255, 255, 255, 0.85); }
12
+ .sv-app.theme-excel .sv-app__links { padding: 0 8px; }
13
+ .sv-app.theme-excel .sv-app__link { border-radius: 0; padding: 7px 12px; border-left: 3px solid transparent; }
14
+ .sv-app.theme-excel .sv-app__link.is-active { background: #e7f2ec; color: #0b5c31; border-left-color: #107c41; font-weight: 600; }
15
+ .sv-app.theme-excel .sv-app__foot { padding-left: 12px; }
16
+ .sv-app.theme-excel .sv-app__toolbar { background: #107c41; border-bottom: 1px solid #0b5c31; }
17
+ .sv-app.theme-excel .sv-app__search { background: rgba(255, 255, 255, 0.16); border-color: rgba(255, 255, 255, 0.3); }
18
+ .sv-app.theme-excel .sv-app__search-in { color: #fff; }
19
+ .sv-app.theme-excel .sv-app__search-in::placeholder { color: rgba(255, 255, 255, 0.78); }
20
+ .sv-app.theme-excel .sv-app__search-ic, .sv-app.theme-excel .sv-app__kbd { color: #fff; }`
21
+
22
+ const suppliers: EntitySchema = {
23
+ name: 'suppliers',
24
+ label: 'Supplier',
25
+ idField: 'id',
26
+ fields: [
27
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
28
+ { field: 'name', type: 'text', required: true, minLength: 2 },
29
+ { field: 'country', type: 'text', label: 'Country', input: { editorType: 'country', help: 'Country of dispatch' } },
30
+ { field: 'email', type: 'text', format: 'email', required: true, input: { placeholder: 'orders@supplier.com' } },
31
+ { field: 'phone', type: 'text', input: { editorType: 'phone' } },
32
+ { field: 'rating', type: 'number', label: 'Rating', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 quality score' } },
33
+ { field: 'leadDays', type: 'number', label: 'Lead time (days)', min: 0, defaultValue: 14, input: { help: 'Typical days from order to delivery' } },
34
+ { field: 'color', type: 'text', label: 'Accent', defaultValue: '#f59e0b', input: { editorType: 'color', help: 'Chart accent color' } },
35
+ { field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
36
+ ],
37
+ }
38
+
39
+ const products: EntitySchema = {
40
+ name: 'products',
41
+ label: 'Product',
42
+ idField: 'id',
43
+ fields: [
44
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
45
+ { field: 'name', type: 'text', required: true, minLength: 2 },
46
+ { field: 'sku', type: 'text', label: 'SKU', required: true, input: { editorType: 'mask', mask: 'AAA-####', help: 'Stock keeping unit' } },
47
+ { field: 'category', type: 'enum', required: true, options: [
48
+ { value: 'electronics', label: 'Electronics', color: '#6366f1' },
49
+ { value: 'office', label: 'Office', color: '#0ea5e9' },
50
+ { value: 'home', label: 'Home', color: '#10b981' },
51
+ { value: 'tools', label: 'Tools', color: '#f59e0b' },
52
+ ] },
53
+ { field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Search and filter keywords' } },
54
+ { field: 'price', type: 'number', label: 'Price ($)', min: 0, required: true },
55
+ { field: 'discount', type: 'number', label: 'Discount (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Current promotional discount' } },
56
+ { field: 'stock', type: 'number', label: 'In stock', min: 0, defaultValue: 0 },
57
+ { field: 'reorderLevel', type: 'number', label: 'Reorder level', min: 0, defaultValue: 20, input: { help: 'Restock when stock drops below this' } },
58
+ // Derived: value of stock on hand (display-only, never stored).
59
+ { field: 'stockValue', type: 'number', label: 'Stock value ($)', readonly: true, formula: 'price * stock' },
60
+ { field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
61
+ ],
62
+ }
63
+
64
+ const purchaseOrders: EntitySchema = {
65
+ name: 'purchaseOrders',
66
+ label: 'Purchase order',
67
+ idField: 'id',
68
+ fields: [
69
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
70
+ { field: 'ref', type: 'text', label: 'Ref', required: true, minLength: 2 },
71
+ { field: 'supplierId', type: 'relation', label: 'Supplier', relation: { entity: 'suppliers', foreignKey: 'supplierId', labelField: 'name' } },
72
+ { field: 'productId', type: 'relation', label: 'Product', relation: { entity: 'products', foreignKey: 'productId', labelField: 'name' } },
73
+ { field: 'status', type: 'enum', defaultValue: 'draft', options: [
74
+ { value: 'draft', label: 'Draft', color: '#94a3b8' },
75
+ { value: 'ordered', label: 'Ordered', color: '#0ea5e9' },
76
+ { value: 'received', label: 'Received', color: '#10b981' },
77
+ { value: 'cancelled', label: 'Cancelled', color: '#ef4444' },
78
+ ] },
79
+ { field: 'qty', type: 'number', label: 'Quantity', min: 1, defaultValue: 1, required: true },
80
+ { field: 'unitCost', type: 'number', label: 'Unit cost ($)', min: 0, defaultValue: 0 },
81
+ // Derived: line total from quantity and unit cost (display-only, never stored).
82
+ { field: 'total', type: 'number', label: 'Total ($)', readonly: true, formula: 'qty * unitCost' },
83
+ { field: 'orderedAt', type: 'date', label: 'Ordered at', input: { editorType: 'datetime', help: 'When the order was placed' } },
84
+ ],
85
+ }
86
+
87
+ const seed = {
88
+ suppliers: [
89
+ { id: 'sp1', name: 'Shenzhen Components', country: 'China', email: 'sales@shenzhencomp.com', phone: '+86 755 5550 101', rating: 5, leadDays: 21, color: '#6366f1', active: true },
90
+ { id: 'sp2', name: 'Nordic Office Supply', country: 'Sweden', email: 'orders@nordicoffice.se', phone: '+46 8 5550 102', rating: 4, leadDays: 10, color: '#10b981', active: true },
91
+ { id: 'sp3', name: 'Bavaria Tools', country: 'Germany', email: 'info@bavariatools.de', phone: '+49 89 5550103', rating: 5, leadDays: 14, color: '#f59e0b', active: true },
92
+ { id: 'sp4', name: 'Pacific Home Goods', country: 'United States', email: 'hello@pacifichome.com', phone: '+1 (415) 555-0104', rating: 4, leadDays: 7, color: '#0ea5e9', active: true },
93
+ { id: 'sp5', name: 'Kyoto Electronics', country: 'Japan', email: 'sales@kyotoelec.jp', phone: '+81 75 5550 105', rating: 5, leadDays: 18, color: '#8b5cf6', active: true },
94
+ { id: 'sp6', name: 'Toronto Distributors', country: 'Canada', email: 'orders@torontodist.ca', phone: '+1 (416) 555-0106', rating: 4, leadDays: 12, color: '#ef4444', active: false },
95
+ ],
96
+ products: [
97
+ { id: 'pr1', name: 'USB-C Hub', sku: 'ELE-0001', category: 'electronics', tags: ['usb-c', 'hub', 'charging'], price: 39, discount: 10, stock: 420, reorderLevel: 100, active: true },
98
+ { id: 'pr2', name: 'Standing Desk', sku: 'OFF-0101', category: 'office', tags: ['adjustable', 'ergonomic'], price: 480, discount: 5, stock: 35, reorderLevel: 20, active: true },
99
+ { id: 'pr3', name: 'Ceramic Mug Set', sku: 'HOM-0210', category: 'home', tags: ['kitchen', 'ceramic'], price: 24, discount: 0, stock: 610, reorderLevel: 150, active: true },
100
+ { id: 'pr4', name: 'Cordless Drill', sku: 'TOO-0305', category: 'tools', tags: ['power', 'cordless', '18v'], price: 129, discount: 15, stock: 88, reorderLevel: 40, active: true },
101
+ { id: 'pr5', name: 'Mechanical Keyboard', sku: 'ELE-0002', category: 'electronics', tags: ['mechanical', 'rgb', 'usb'], price: 95, discount: 8, stock: 240, reorderLevel: 80, active: true },
102
+ { id: 'pr6', name: 'Ergonomic Chair', sku: 'OFF-0102', category: 'office', tags: ['ergonomic', 'mesh'], price: 320, discount: 12, stock: 52, reorderLevel: 25, active: true },
103
+ { id: 'pr7', name: 'LED Desk Lamp', sku: 'HOM-0211', category: 'home', tags: ['lighting', 'led', 'dimmable'], price: 45, discount: 0, stock: 175, reorderLevel: 60, active: true },
104
+ ],
105
+ purchaseOrders: [
106
+ { id: 'po1', ref: 'PO-4001', supplierId: 'sp1', productId: 'pr1', status: 'received', qty: 200, unitCost: 28, orderedAt: '2026-01-10T09:30:00.000Z' },
107
+ { id: 'po2', ref: 'PO-4002', supplierId: 'sp2', productId: 'pr2', status: 'ordered', qty: 30, unitCost: 360, orderedAt: '2026-02-04T14:15:00.000Z' },
108
+ { id: 'po3', ref: 'PO-4003', supplierId: 'sp3', productId: 'pr4', status: 'draft', qty: 25, unitCost: 92, orderedAt: '2026-02-18T11:00:00.000Z' },
109
+ { id: 'po4', ref: 'PO-4004', supplierId: 'sp4', productId: 'pr3', status: 'cancelled', qty: 100, unitCost: 15, orderedAt: '2026-01-22T16:45:00.000Z' },
110
+ { id: 'po5', ref: 'PO-4005', supplierId: 'sp5', productId: 'pr5', status: 'received', qty: 150, unitCost: 68, orderedAt: '2026-03-01T08:20:00.000Z' },
111
+ { id: 'po6', ref: 'PO-4006', supplierId: 'sp6', productId: 'pr7', status: 'ordered', qty: 120, unitCost: 30, orderedAt: '2026-03-12T13:10:00.000Z' },
112
+ ],
113
+ }
114
+
115
+ // Product grid formatting: category status pills, a LOW STOCK red-bold threshold
116
+ // (stock under 10 units needs reordering now), and a green highlight on deep
117
+ // promotional discounts - data-driven color, not just status pills.
118
+ const productFormats: FormatRule[] = [
119
+ ...statusPills(products, 'category'),
120
+ { field: 'stock', op: 'lt', value: 10, color: '#dc2626', bold: true },
121
+ { field: 'discount', op: 'gte', value: 15, color: '#16a34a', bold: true },
122
+ ]
123
+
124
+ export const inventory: SampleApp = {
125
+ id: 'inventory',
126
+ name: 'Inventory',
127
+ description: 'Products, suppliers and purchase orders - an Excel-style warehouse app behind a sign-in (clerk and manager roles), with stock and purchasing dashboards (target KPIs, an order-trend sparkline, a reorder gauge, a category pie, a supplier x status pivot, tabbed breakdowns), a low-stock red grid plus a status-pill order grid with row actions, suppliers master/detail, and rich edit forms (country, phone, supplier rating, SKU mask, tag chips, discount slider, order datetime).',
128
+ emoji: '\u{1F4E6}',
129
+ accent: '#107c41',
130
+ build: () => {
131
+ const supplierRows = pad(suppliers, seed.suppliers, 24)
132
+ const productRows = pad(products, seed.products, 60)
133
+ const poRows = pad(purchaseOrders, seed.purchaseOrders, 90, { supplierId: ids(supplierRows), productId: ids(productRows) })
134
+ return project({
135
+ title: 'Warehouse',
136
+ brand: 'Warehouse',
137
+ accent: '#107c41',
138
+ preset: 'excel',
139
+ footer: '',
140
+ // Excel/spreadsheet-style shell: green header + command bar, square sheets.
141
+ appClass: 'theme-excel',
142
+ customCss: EXCEL_CSS,
143
+ // Sign-in with two roles. Clerks manage stock but cannot delete; purchasing
144
+ // analytics and the supplier directory are manager-only.
145
+ auth: { enabled: true, protect: true },
146
+ access: {
147
+ enabled: true,
148
+ defaultRole: 'clerk',
149
+ roles: [
150
+ { role: 'manager', screens: '*', actions: '*' },
151
+ { role: 'clerk', screens: ['overview', 'product-detail', 'products', 'board'], actions: ['create', 'update'] },
152
+ ],
153
+ },
154
+ entities: [suppliers, products, purchaseOrders],
155
+ seed: { suppliers: supplierRows, products: productRows, purchaseOrders: poRows },
156
+ screens: [
157
+ // Stock dashboard: KPI cards (units-in-stock card carries a 5,000-unit
158
+ // target), a stock-level gauge, category breakdowns (bar + pie), a tabbed
159
+ // view, and a status-pill product grid where LOW STOCK rows turn red-bold.
160
+ dashScreen(products, { id: 'overview', title: 'Stock', order: 0 }, [
161
+ { kpi: 'Products', reduce: 'count', span: 1 },
162
+ { kpi: 'Units in stock', measure: 'stock', reduce: 'sum', target: 5000, span: 1 },
163
+ { kpi: 'Avg price', measure: 'price', reduce: 'avg', format: 'currency', span: 1 },
164
+ { gauge: 'Avg stock on hand', measure: 'stock', reduce: 'avg', min: 0, max: 500, span: 1 },
165
+ { chart: 'category', measure: 'stock', reduce: 'sum', type: 'bar', span: 2 },
166
+ { chart: 'category', reduce: 'count', type: 'pie', span: 1 },
167
+ { tabs: [
168
+ { label: 'Stock by category', tiles: [{ chart: 'category', measure: 'stock', reduce: 'sum', type: 'bar' }] },
169
+ { label: 'Avg price by category', tiles: [{ chart: 'category', measure: 'price', reduce: 'avg', type: 'bar' }] },
170
+ ], span: 3 },
171
+ { grid: true, format: productFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
172
+ ]),
173
+ // Purchasing dashboard: order-volume + cost KPIs (units-ordered card trends
174
+ // over the order date as a sparkline), an average-unit-cost gauge, status
175
+ // breakdowns, an order-quantity-over-time area trend, a supplier x status
176
+ // pivot, a tabbed view, and a status-pill purchase-order grid.
177
+ dashScreen(purchaseOrders, { id: 'purchasing', title: 'Purchasing', order: 1 }, [
178
+ { kpi: 'Purchase orders', reduce: 'count', span: 1 },
179
+ { kpi: 'Units ordered', measure: 'qty', reduce: 'sum', trendField: 'orderedAt', trendReduce: 'sum', span: 1 },
180
+ { kpi: 'Avg unit cost', measure: 'unitCost', reduce: 'avg', format: 'currency', span: 1 },
181
+ { gauge: 'Avg unit cost', measure: 'unitCost', reduce: 'avg', min: 0, max: 400, unit: '$', span: 1 },
182
+ { chart: 'status', measure: 'qty', reduce: 'sum', type: 'bar', span: 2 },
183
+ { chart: 'status', reduce: 'count', type: 'pie', span: 1 },
184
+ { chart: 'orderedAt', measure: 'qty', reduce: 'sum', type: 'area', span: 3 },
185
+ { pivot: { rows: ['supplierId'], cols: ['status'], measure: 'qty', aggregate: 'sum' }, span: 3 },
186
+ { tabs: [
187
+ { label: 'Qty by status', tiles: [{ chart: 'status', measure: 'qty', reduce: 'sum', type: 'bar' }] },
188
+ { label: 'Orders by status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
189
+ ], span: 3 },
190
+ { grid: true, format: statusPills(purchaseOrders, 'status'), summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
191
+ ]),
192
+ // Product detail: a full record page - header (name + SKU + category pill),
193
+ // stock / price metric tiles, an Overview of pricing and reorder fields, and a
194
+ // Purchase orders timeline of the orders placed for the product.
195
+ detailScreen(products, { id: 'product-detail', title: 'Product detail', order: 2 }, {
196
+ titleField: 'name', subtitleField: 'sku', statusField: 'category',
197
+ metricFields: ['stock', 'price'],
198
+ sections: [
199
+ { label: 'Pricing', fields: ['discount', 'tags'] },
200
+ { label: 'Stock', fields: ['reorderLevel', 'active'] },
201
+ ],
202
+ related: [{ entity: 'purchaseOrders', foreignKey: 'productId', label: 'Purchase orders', titleField: 'ref', subtitleField: 'qty', dateField: 'orderedAt', statusField: 'status' }],
203
+ }),
204
+ screen(suppliers, 'master-detail', { id: 'suppliers', title: 'Suppliers', order: 3, child: purchaseOrders, foreignKey: 'supplierId' }),
205
+ formScreen(products, { id: 'products', title: 'Products', order: 4 }, undefined, { format: productFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'product-detail', sourceField: 'id', targetField: 'id' } }, ['category', 'active']),
206
+ formScreen(purchaseOrders, { id: 'purchaseOrders', title: 'Purchase orders', order: 5 }, undefined, { format: statusPills(purchaseOrders, 'status'), summaries: true, rowActions: [{ kind: 'edit' }] }, ['status', 'supplierId']),
207
+ // Purchasing board: purchase orders as draggable Kanban lanes by status
208
+ // (Draft -> Ordered -> Received), card = ref + order total.
209
+ boardScreen(purchaseOrders, { id: 'board', title: 'Orders board', order: 6 }, { groupBy: 'status', titleField: 'ref', badgeField: 'qty', subtitleField: 'supplierId', filter: ['status', 'supplierId'] }),
210
+ ],
211
+ })
212
+ },
213
+ }