@svgrid/enterprise 2.2.0 → 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 +22479 -10466
  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 +13 -1
  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,195 +1,223 @@
1
- import type { EntitySchema } from '../../schema.js'
2
- import type { FormatRule } from '../project.js'
3
- import { screen, formScreen, boardScreen, calendarScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
-
5
- const policies: EntitySchema = {
6
- name: 'policies',
7
- label: 'Policy',
8
- idField: 'id',
9
- fields: [
10
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
11
- { field: 'policyNumber', type: 'text', label: 'Policy #', required: true, input: { editorType: 'mask', mask: 'POL-######', help: 'Policy identifier' } },
12
- { field: 'holder', type: 'text', label: 'Policy holder', required: true, minLength: 2 },
13
- { field: 'email', type: 'text', label: 'Email', format: 'email', required: true, input: { placeholder: 'holder@example.com' } },
14
- { field: 'type', type: 'enum', label: 'Line', required: true, options: [
15
- { value: 'auto', label: 'Auto', color: '#6366f1' },
16
- { value: 'home', label: 'Home', color: '#10b981' },
17
- { value: 'life', label: 'Life', color: '#f59e0b' },
18
- { value: 'health', label: 'Health', color: '#ef4444' },
19
- ] },
20
- { field: 'region', type: 'text', label: 'Region', input: { editorType: 'country', help: 'Underwriting region' } },
21
- { field: 'premium', type: 'number', label: 'Premium ($)', min: 0, required: true },
22
- { field: 'coverage', type: 'number', label: 'Coverage ($)', min: 0, required: true },
23
- { field: 'riskScore', type: 'number', label: 'Risk', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1 (low) - 5 (high) underwriting risk' } },
24
- { field: 'status', type: 'enum', label: 'Status', defaultValue: 'active', options: [
25
- { value: 'active', label: 'Active', color: '#10b981' },
26
- { value: 'lapsed', label: 'Lapsed', color: '#f59e0b' },
27
- { value: 'cancelled', label: 'Cancelled', color: '#ef4444' },
28
- ] },
29
- { field: 'startDate', type: 'date', label: 'Start date' },
30
- ],
31
- }
32
-
33
- const adjusters: EntitySchema = {
34
- name: 'adjusters',
35
- label: 'Adjuster',
36
- idField: 'id',
37
- fields: [
38
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
39
- { field: 'name', type: 'text', required: true, minLength: 2 },
40
- { field: 'email', type: 'text', format: 'email', required: true },
41
- { field: 'phone', type: 'text', input: { editorType: 'phone' } },
42
- { field: 'region', type: 'text', label: 'Region', input: { editorType: 'country' } },
43
- { field: 'specialty', type: 'enum', label: 'Specialty', options: [
44
- { value: 'auto', label: 'Auto', color: '#6366f1' },
45
- { value: 'property', label: 'Property', color: '#10b981' },
46
- { value: 'medical', label: 'Medical', color: '#ef4444' },
47
- { value: 'liability', label: 'Liability', color: '#f59e0b' },
48
- ] },
49
- { field: 'rating', type: 'number', label: 'Rating', min: 1, max: 5, defaultValue: 4, input: { editorType: 'rating', help: 'Resolution quality' } },
50
- { field: 'color', type: 'text', label: 'Accent', defaultValue: '#0ea5e9', input: { editorType: 'color', help: 'Chart accent color' } },
51
- { field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
52
- ],
53
- }
54
-
55
- const claims: EntitySchema = {
56
- name: 'claims',
57
- label: 'Claim',
58
- idField: 'id',
59
- fields: [
60
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
61
- { field: 'claimNumber', type: 'text', label: 'Claim #', required: true, input: { editorType: 'mask', mask: 'CLM-######', help: 'Claim reference' } },
62
- { field: 'policyId', type: 'relation', label: 'Policy', required: true, relation: { entity: 'policies', foreignKey: 'policyId', labelField: 'policyNumber' } },
63
- { field: 'adjusterId', type: 'relation', label: 'Adjuster', relation: { entity: 'adjusters', foreignKey: 'adjusterId', labelField: 'name' } },
64
- { field: 'type', type: 'enum', label: 'Type', required: true, options: [
65
- { value: 'collision', label: 'Collision', color: '#6366f1' },
66
- { value: 'theft', label: 'Theft', color: '#8b5cf6' },
67
- { value: 'fire', label: 'Fire', color: '#ef4444' },
68
- { value: 'water', label: 'Water', color: '#0ea5e9' },
69
- { value: 'medical', label: 'Medical', color: '#10b981' },
70
- ] },
71
- { field: 'status', type: 'enum', label: 'Status', required: true, defaultValue: 'filed', options: [
72
- { value: 'filed', label: 'Filed', color: '#94a3b8' },
73
- { value: 'review', label: 'In review', color: '#0ea5e9' },
74
- { value: 'approved', label: 'Approved', color: '#6366f1' },
75
- { value: 'paid', label: 'Paid', color: '#10b981' },
76
- { value: 'denied', label: 'Denied', color: '#ef4444' },
77
- ] },
78
- { field: 'amount', type: 'number', label: 'Amount ($)', min: 0, required: true },
79
- { field: 'deductible', type: 'number', label: 'Deductible ($)', min: 0, defaultValue: 500 },
80
- // Derived: net payout after the deductible (display-only, never stored).
81
- { field: 'net', type: 'number', label: 'Net payout ($)', readonly: true, formula: 'amount - deductible' },
82
- { field: 'severity', type: 'number', label: 'Severity', min: 0, max: 100, defaultValue: 40, input: { editorType: 'slider', suffix: '%', help: 'Loss severity 0-100' } },
83
- { field: 'fraudRisk', type: 'number', label: 'Fraud risk', min: 1, max: 5, defaultValue: 1, input: { editorType: 'rating', help: 'Fraud indicators 1-5' } },
84
- { field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Triage keywords' } },
85
- { field: 'incidentDate', type: 'date', label: 'Incident date' },
86
- { field: 'filedAt', type: 'datetime', label: 'Filed at', input: { editorType: 'datetime', help: 'When the claim was filed' } },
87
- ],
88
- }
89
-
90
- const seed = {
91
- policies: [
92
- { id: 'pl1', policyNumber: 'POL-100001', holder: 'Oliver Grant', email: 'oliver.grant@example.com', type: 'auto', region: 'United States', premium: 1200, coverage: 45000, riskScore: 2, status: 'active', startDate: '2024-03-01' },
93
- { id: 'pl2', policyNumber: 'POL-100002', holder: 'Maya Fernandez', email: 'maya.fernandez@example.com', type: 'home', region: 'Spain', premium: 980, coverage: 320000, riskScore: 3, status: 'active', startDate: '2023-11-15' },
94
- { id: 'pl3', policyNumber: 'POL-100003', holder: 'Noah Kim', email: 'noah.kim@example.com', type: 'health', region: 'South Korea', premium: 2100, coverage: 150000, riskScore: 4, status: 'active', startDate: '2025-01-08' },
95
- { id: 'pl4', policyNumber: 'POL-100004', holder: 'Sophia Rossi', email: 'sophia.rossi@example.com', type: 'life', region: 'Italy', premium: 1650, coverage: 500000, riskScore: 2, status: 'lapsed', startDate: '2022-06-20' },
96
- { id: 'pl5', policyNumber: 'POL-100005', holder: 'Ethan Walsh', email: 'ethan.walsh@example.com', type: 'auto', region: 'United States', premium: 1420, coverage: 52000, riskScore: 5, status: 'active', startDate: '2024-09-12' },
97
- { id: 'pl6', policyNumber: 'POL-100006', holder: 'Amara Okafor', email: 'amara.okafor@example.com', type: 'home', region: 'Nigeria', premium: 760, coverage: 210000, riskScore: 3, status: 'cancelled', startDate: '2023-02-28' },
98
- { id: 'pl7', policyNumber: 'POL-100007', holder: 'Liam Murphy', email: 'liam.murphy@example.com', type: 'health', region: 'Ireland', premium: 1890, coverage: 180000, riskScore: 4, status: 'active', startDate: '2024-12-01' },
99
- { id: 'pl8', policyNumber: 'POL-100008', holder: 'Hana Sato', email: 'hana.sato@example.com', type: 'auto', region: 'Japan', premium: 1310, coverage: 48000, riskScore: 2, status: 'active', startDate: '2025-02-19' },
100
- ],
101
- adjusters: [
102
- { id: 'ad1', name: 'Grace Okonkwo', email: 'grace.okonkwo@insureco.com', phone: '+1 (415) 555-0301', region: 'United States', specialty: 'auto', rating: 5, color: '#6366f1', active: true },
103
- { id: 'ad2', name: 'Marcus Bauer', email: 'marcus.bauer@insureco.com', phone: '+49 30 5550302', region: 'Germany', specialty: 'property', rating: 4, color: '#10b981', active: true },
104
- { id: 'ad3', name: 'Priya Nair', email: 'priya.nair@insureco.com', phone: '+91 22 5550 303', region: 'India', specialty: 'medical', rating: 5, color: '#ef4444', active: true },
105
- { id: 'ad4', name: 'Diego Santos', email: 'diego.santos@insureco.com', phone: '+55 11 95555-0304', region: 'Brazil', specialty: 'liability', rating: 3, color: '#f59e0b', active: true },
106
- { id: 'ad5', name: 'Elena Popova', email: 'elena.popova@insureco.com', phone: '+7 495 555 0305', region: 'Russia', specialty: 'property', rating: 4, color: '#8b5cf6', active: false },
107
- { id: 'ad6', name: 'Thomas Becker', email: 'thomas.becker@insureco.com', phone: '+1 (206) 555-0306', region: 'United States', specialty: 'auto', rating: 4, color: '#0ea5e9', active: true },
108
- ],
109
- claims: [
110
- { id: 'cl1', claimNumber: 'CLM-500001', policyId: 'pl1', adjusterId: 'ad1', type: 'collision', status: 'paid', amount: 8400, deductible: 500, severity: 55, fraudRisk: 1, tags: ['rear-end', 'no-injury'], incidentDate: '2026-01-04', filedAt: '2026-01-06T09:30:00.000Z' },
111
- { id: 'cl2', claimNumber: 'CLM-500002', policyId: 'pl2', adjusterId: 'ad2', type: 'water', status: 'review', amount: 15200, deductible: 1000, severity: 70, fraudRisk: 2, tags: ['burst-pipe'], incidentDate: '2026-02-01', filedAt: '2026-02-03T14:15:00.000Z' },
112
- { id: 'cl3', claimNumber: 'CLM-500003', policyId: 'pl3', adjusterId: 'ad3', type: 'medical', status: 'approved', amount: 6200, deductible: 750, severity: 40, fraudRisk: 1, tags: ['surgery'], incidentDate: '2026-02-10', filedAt: '2026-02-12T11:00:00.000Z' },
113
- { id: 'cl4', claimNumber: 'CLM-500004', policyId: 'pl5', adjusterId: 'ad1', type: 'theft', status: 'denied', amount: 22000, deductible: 500, severity: 90, fraudRisk: 5, tags: ['suspicious', 'late-report'], incidentDate: '2026-01-20', filedAt: '2026-01-28T16:45:00.000Z' },
114
- { id: 'cl5', claimNumber: 'CLM-500005', policyId: 'pl6', adjusterId: 'ad4', type: 'fire', status: 'paid', amount: 41000, deductible: 2000, severity: 95, fraudRisk: 2, tags: ['kitchen', 'total-loss'], incidentDate: '2026-03-01', filedAt: '2026-03-02T08:20:00.000Z' },
115
- { id: 'cl6', claimNumber: 'CLM-500006', policyId: 'pl7', adjusterId: 'ad3', type: 'medical', status: 'filed', amount: 3100, deductible: 750, severity: 25, fraudRisk: 1, tags: ['ER-visit'], incidentDate: '2026-03-11', filedAt: '2026-03-12T13:10:00.000Z' },
116
- { id: 'cl7', claimNumber: 'CLM-500007', policyId: 'pl8', adjusterId: 'ad6', type: 'collision', status: 'review', amount: 9700, deductible: 500, severity: 60, fraudRisk: 3, tags: ['intersection'], incidentDate: '2026-03-15', filedAt: '2026-03-17T10:05:00.000Z' },
117
- { id: 'cl8', claimNumber: 'CLM-500008', policyId: 'pl1', adjusterId: 'ad6', type: 'theft', status: 'approved', amount: 5400, deductible: 500, severity: 50, fraudRisk: 2, tags: ['catalytic-converter'], incidentDate: '2026-02-22', filedAt: '2026-02-24T15:30:00.000Z' },
118
- ],
119
- }
120
-
121
- // Claim grid formatting: status pills + a red-bold high-fraud threshold and a
122
- // green highlight on large approved payouts - data-driven, not just pills.
123
- const claimFormats: FormatRule[] = [
124
- ...statusPills(claims, 'status'),
125
- { field: 'fraudRisk', op: 'gte', value: 4, color: '#dc2626', bold: true },
126
- { field: 'amount', op: 'gte', value: 20000, color: '#b45309', bold: true },
127
- ]
128
-
129
- export const insurance: SampleApp = {
130
- id: 'insurance',
131
- name: 'Insurance Claims',
132
- description: 'Policies, adjusters and claims - a claims pipeline board (Filed -> Paid), payout + approval dashboards (target KPIs, a filings sparkline, a severity gauge, type/status breakdowns, an adjuster x status pivot, tabbed views), a fraud-risk red grid, policy master/detail with a claims timeline, and rich edit forms (policy/claim number masks, region, adjuster phone, risk + fraud ratings, severity slider, triage chips, filing datetime).',
133
- emoji: '\u{1F6E1}\u{FE0F}',
134
- accent: '#1e5eff',
135
- build: () => {
136
- const policyRows = pad(policies, seed.policies, 40)
137
- const adjusterRows = pad(adjusters, seed.adjusters, 16)
138
- const claimRows = pad(claims, seed.claims, 90, { policyId: ids(policyRows), adjusterId: ids(adjusterRows) })
139
- return project({
140
- title: 'Claims',
141
- brand: 'Assurance',
142
- accent: '#1e5eff',
143
- preset: 'sap',
144
- footer: '',
145
- entities: [policies, adjusters, claims],
146
- seed: { policies: policyRows, adjusters: adjusterRows, claims: claimRows },
147
- screens: [
148
- // Claims dashboard: payout + approval KPIs (total payout carries a target,
149
- // filings trend as a sparkline), a severity gauge, type/status breakdowns,
150
- // an adjuster x status pivot, tabs, and a fraud-risk status-pill grid that
151
- // drills into the claim detail.
152
- dashScreen(claims, { id: 'overview', title: 'Claims', order: 0 }, [
153
- { kpi: 'Claims', reduce: 'count', span: 1 },
154
- { kpi: 'Total payout', measure: 'net', reduce: 'sum', format: 'currency', target: 200000, span: 1 },
155
- { kpi: 'Filings', measure: 'amount', reduce: 'count', trendField: 'filedAt', trendReduce: 'count', span: 1 },
156
- { gauge: 'Avg severity', measure: 'severity', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
157
- { chart: 'status', measure: 'amount', reduce: 'sum', type: 'bar', span: 2 },
158
- { chart: 'type', reduce: 'count', type: 'pie', span: 1 },
159
- { chart: 'filedAt', measure: 'amount', reduce: 'sum', type: 'area', span: 3 },
160
- { pivot: { rows: ['adjusterId'], cols: ['status'], measure: 'amount', aggregate: 'sum' }, span: 3 },
161
- { tabs: [
162
- { label: 'Payout by status', tiles: [{ chart: 'status', measure: 'net', reduce: 'sum', type: 'bar' }] },
163
- { label: 'Claims by type', tiles: [{ chart: 'type', reduce: 'count', type: 'bar' }] },
164
- ], span: 3 },
165
- { grid: true, format: claimFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'claim-detail', sourceField: 'id', targetField: 'id' }, span: 3 },
166
- ]),
167
- // The signature view: the claims pipeline as draggable Kanban columns by
168
- // status, each card the claim number + policy + payout; opens the detail.
169
- boardScreen(claims, { id: 'pipeline', title: 'Pipeline', order: 1 }, { groupBy: 'status', titleField: 'claimNumber', subtitleField: 'policyId', badgeField: 'amount', filter: ['status', 'type', 'adjusterId'], openScreen: 'claim-detail' }),
170
- // Claim detail: header (claim # + policy + status pill), payout metric
171
- // tiles, and Overview sections.
172
- detailScreen(claims, { id: 'claim-detail', title: 'Claim detail', order: 2 }, {
173
- titleField: 'claimNumber', subtitleField: 'policyId', statusField: 'status',
174
- metricFields: ['amount', 'net'],
175
- sections: [
176
- { label: 'Assessment', fields: ['type', 'severity', 'fraudRisk', 'tags'] },
177
- { label: 'Handling', fields: ['adjusterId', 'deductible', 'incidentDate', 'filedAt'] },
178
- ],
179
- }),
180
- // Policy 360: a full policy record with a timeline of its claims.
181
- detailScreen(policies, { id: 'policy-360', title: 'Policy 360', order: 3 }, {
182
- titleField: 'policyNumber', subtitleField: 'holder', statusField: 'status',
183
- metricFields: ['premium', 'coverage'],
184
- sections: [{ label: 'Underwriting', fields: ['type', 'region', 'riskScore', 'startDate'] }],
185
- related: [{ entity: 'claims', foreignKey: 'policyId', label: 'Claims', titleField: 'claimNumber', subtitleField: 'amount', dateField: 'filedAt', statusField: 'status' }],
186
- }),
187
- screen(policies, 'master-detail', { id: 'policies', title: 'Policies', order: 4, child: claims, foreignKey: 'policyId', linkScreen: 'policy-360' }),
188
- formScreen(claims, { id: 'claims', title: 'Claims list', order: 5 }, undefined, { format: claimFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'claim-detail', sourceField: 'id', targetField: 'id' } }, ['status', 'type', 'policyId']),
189
- formScreen(adjusters, { id: 'adjusters', title: 'Adjusters', order: 6 }, undefined, {}, ['specialty', 'region', 'active']),
190
- // Incident calendar: claims placed on a month calendar by incident date.
191
- calendarScreen(claims, { id: 'calendar', title: 'Incidents', order: 7 }, { dateField: 'incidentDate', titleField: 'claimNumber', filter: ['status', 'type', 'adjusterId'] }),
192
- ],
193
- })
194
- },
195
- }
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import type { FormatRule } from '../project.js'
3
+ import { screen, formScreen, boardScreen, calendarScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
+
5
+ // SAP Fiori-style chrome: a dark shell bar across the top (the toolbar), a white
6
+ // launchpad-style rail, and a blue left-bar on the active item (square corners).
7
+ const FIORI_CSS = `.sv-app.theme-fiori .sv-app__toolbar { background: #354a5f; border-bottom: 0; }
8
+ .sv-app.theme-fiori .sv-app__search { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.24); }
9
+ .sv-app.theme-fiori .sv-app__search-in { color: #fff; }
10
+ .sv-app.theme-fiori .sv-app__search-in::placeholder { color: rgba(255, 255, 255, 0.72); }
11
+ .sv-app.theme-fiori .sv-app__search-ic, .sv-app.theme-fiori .sv-app__kbd { color: #fff; }
12
+ .sv-app.theme-fiori .sv-app__side { background: #fff; border-right: 1px solid #e5e5e5; }
13
+ .sv-app.theme-fiori .sv-app__brandtext { color: #0a6ed1; font-weight: 700; }
14
+ .sv-app.theme-fiori .sv-app__link { border-radius: 0; padding: 9px 12px; border-left: 3px solid transparent; color: #32363a; }
15
+ .sv-app.theme-fiori .sv-app__link:hover { background: #f5f6f7; }
16
+ .sv-app.theme-fiori .sv-app__link.is-active { background: #ebf5ff; color: #0a6ed1; border-left-color: #0a6ed1; font-weight: 600; }`
17
+
18
+ const policies: EntitySchema = {
19
+ name: 'policies',
20
+ label: 'Policy',
21
+ idField: 'id',
22
+ fields: [
23
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
24
+ { field: 'policyNumber', type: 'text', label: 'Policy #', required: true, input: { editorType: 'mask', mask: 'POL-######', help: 'Policy identifier' } },
25
+ { field: 'holder', type: 'text', label: 'Policy holder', required: true, minLength: 2 },
26
+ { field: 'email', type: 'text', label: 'Email', format: 'email', required: true, input: { placeholder: 'holder@example.com' } },
27
+ { field: 'type', type: 'enum', label: 'Line', required: true, options: [
28
+ { value: 'auto', label: 'Auto', color: '#6366f1' },
29
+ { value: 'home', label: 'Home', color: '#10b981' },
30
+ { value: 'life', label: 'Life', color: '#f59e0b' },
31
+ { value: 'health', label: 'Health', color: '#ef4444' },
32
+ ] },
33
+ { field: 'region', type: 'text', label: 'Region', input: { editorType: 'country', help: 'Underwriting region' } },
34
+ { field: 'premium', type: 'number', label: 'Premium ($)', min: 0, required: true },
35
+ { field: 'coverage', type: 'number', label: 'Coverage ($)', min: 0, required: true },
36
+ { field: 'riskScore', type: 'number', label: 'Risk', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1 (low) - 5 (high) underwriting risk' } },
37
+ { field: 'status', type: 'enum', label: 'Status', defaultValue: 'active', options: [
38
+ { value: 'active', label: 'Active', color: '#10b981' },
39
+ { value: 'lapsed', label: 'Lapsed', color: '#f59e0b' },
40
+ { value: 'cancelled', label: 'Cancelled', color: '#ef4444' },
41
+ ] },
42
+ { field: 'startDate', type: 'date', label: 'Start date' },
43
+ ],
44
+ }
45
+
46
+ const adjusters: EntitySchema = {
47
+ name: 'adjusters',
48
+ label: 'Adjuster',
49
+ idField: 'id',
50
+ fields: [
51
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
52
+ { field: 'name', type: 'text', required: true, minLength: 2 },
53
+ { field: 'email', type: 'text', format: 'email', required: true },
54
+ { field: 'phone', type: 'text', input: { editorType: 'phone' } },
55
+ { field: 'region', type: 'text', label: 'Region', input: { editorType: 'country' } },
56
+ { field: 'specialty', type: 'enum', label: 'Specialty', options: [
57
+ { value: 'auto', label: 'Auto', color: '#6366f1' },
58
+ { value: 'property', label: 'Property', color: '#10b981' },
59
+ { value: 'medical', label: 'Medical', color: '#ef4444' },
60
+ { value: 'liability', label: 'Liability', color: '#f59e0b' },
61
+ ] },
62
+ { field: 'rating', type: 'number', label: 'Rating', min: 1, max: 5, defaultValue: 4, input: { editorType: 'rating', help: 'Resolution quality' } },
63
+ { field: 'color', type: 'text', label: 'Accent', defaultValue: '#0ea5e9', input: { editorType: 'color', help: 'Chart accent color' } },
64
+ { field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
65
+ ],
66
+ }
67
+
68
+ const claims: EntitySchema = {
69
+ name: 'claims',
70
+ label: 'Claim',
71
+ idField: 'id',
72
+ fields: [
73
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
74
+ { field: 'claimNumber', type: 'text', label: 'Claim #', required: true, input: { editorType: 'mask', mask: 'CLM-######', help: 'Claim reference' } },
75
+ { field: 'policyId', type: 'relation', label: 'Policy', required: true, relation: { entity: 'policies', foreignKey: 'policyId', labelField: 'policyNumber' } },
76
+ { field: 'adjusterId', type: 'relation', label: 'Adjuster', relation: { entity: 'adjusters', foreignKey: 'adjusterId', labelField: 'name' } },
77
+ { field: 'type', type: 'enum', label: 'Type', required: true, options: [
78
+ { value: 'collision', label: 'Collision', color: '#6366f1' },
79
+ { value: 'theft', label: 'Theft', color: '#8b5cf6' },
80
+ { value: 'fire', label: 'Fire', color: '#ef4444' },
81
+ { value: 'water', label: 'Water', color: '#0ea5e9' },
82
+ { value: 'medical', label: 'Medical', color: '#10b981' },
83
+ ] },
84
+ { field: 'status', type: 'enum', label: 'Status', required: true, defaultValue: 'filed', options: [
85
+ { value: 'filed', label: 'Filed', color: '#94a3b8' },
86
+ { value: 'review', label: 'In review', color: '#0ea5e9' },
87
+ { value: 'approved', label: 'Approved', color: '#6366f1' },
88
+ { value: 'paid', label: 'Paid', color: '#10b981' },
89
+ { value: 'denied', label: 'Denied', color: '#ef4444' },
90
+ ] },
91
+ { field: 'amount', type: 'number', label: 'Amount ($)', min: 0, required: true },
92
+ { field: 'deductible', type: 'number', label: 'Deductible ($)', min: 0, defaultValue: 500 },
93
+ // Derived: net payout after the deductible (display-only, never stored).
94
+ { field: 'net', type: 'number', label: 'Net payout ($)', readonly: true, formula: 'amount - deductible' },
95
+ { field: 'severity', type: 'number', label: 'Severity', min: 0, max: 100, defaultValue: 40, input: { editorType: 'slider', suffix: '%', help: 'Loss severity 0-100' } },
96
+ { field: 'fraudRisk', type: 'number', label: 'Fraud risk', min: 1, max: 5, defaultValue: 1, input: { editorType: 'rating', help: 'Fraud indicators 1-5' } },
97
+ { field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Triage keywords' } },
98
+ { field: 'incidentDate', type: 'date', label: 'Incident date' },
99
+ { field: 'filedAt', type: 'datetime', label: 'Filed at', input: { editorType: 'datetime', help: 'When the claim was filed' } },
100
+ ],
101
+ }
102
+
103
+ const seed = {
104
+ policies: [
105
+ { id: 'pl1', policyNumber: 'POL-100001', holder: 'Oliver Grant', email: 'oliver.grant@example.com', type: 'auto', region: 'United States', premium: 1200, coverage: 45000, riskScore: 2, status: 'active', startDate: '2024-03-01' },
106
+ { id: 'pl2', policyNumber: 'POL-100002', holder: 'Maya Fernandez', email: 'maya.fernandez@example.com', type: 'home', region: 'Spain', premium: 980, coverage: 320000, riskScore: 3, status: 'active', startDate: '2023-11-15' },
107
+ { id: 'pl3', policyNumber: 'POL-100003', holder: 'Noah Kim', email: 'noah.kim@example.com', type: 'health', region: 'South Korea', premium: 2100, coverage: 150000, riskScore: 4, status: 'active', startDate: '2025-01-08' },
108
+ { id: 'pl4', policyNumber: 'POL-100004', holder: 'Sophia Rossi', email: 'sophia.rossi@example.com', type: 'life', region: 'Italy', premium: 1650, coverage: 500000, riskScore: 2, status: 'lapsed', startDate: '2022-06-20' },
109
+ { id: 'pl5', policyNumber: 'POL-100005', holder: 'Ethan Walsh', email: 'ethan.walsh@example.com', type: 'auto', region: 'United States', premium: 1420, coverage: 52000, riskScore: 5, status: 'active', startDate: '2024-09-12' },
110
+ { id: 'pl6', policyNumber: 'POL-100006', holder: 'Amara Okafor', email: 'amara.okafor@example.com', type: 'home', region: 'Nigeria', premium: 760, coverage: 210000, riskScore: 3, status: 'cancelled', startDate: '2023-02-28' },
111
+ { id: 'pl7', policyNumber: 'POL-100007', holder: 'Liam Murphy', email: 'liam.murphy@example.com', type: 'health', region: 'Ireland', premium: 1890, coverage: 180000, riskScore: 4, status: 'active', startDate: '2024-12-01' },
112
+ { id: 'pl8', policyNumber: 'POL-100008', holder: 'Hana Sato', email: 'hana.sato@example.com', type: 'auto', region: 'Japan', premium: 1310, coverage: 48000, riskScore: 2, status: 'active', startDate: '2025-02-19' },
113
+ ],
114
+ adjusters: [
115
+ { id: 'ad1', name: 'Grace Okonkwo', email: 'grace.okonkwo@insureco.com', phone: '+1 (415) 555-0301', region: 'United States', specialty: 'auto', rating: 5, color: '#6366f1', active: true },
116
+ { id: 'ad2', name: 'Marcus Bauer', email: 'marcus.bauer@insureco.com', phone: '+49 30 5550302', region: 'Germany', specialty: 'property', rating: 4, color: '#10b981', active: true },
117
+ { id: 'ad3', name: 'Priya Nair', email: 'priya.nair@insureco.com', phone: '+91 22 5550 303', region: 'India', specialty: 'medical', rating: 5, color: '#ef4444', active: true },
118
+ { id: 'ad4', name: 'Diego Santos', email: 'diego.santos@insureco.com', phone: '+55 11 95555-0304', region: 'Brazil', specialty: 'liability', rating: 3, color: '#f59e0b', active: true },
119
+ { id: 'ad5', name: 'Elena Popova', email: 'elena.popova@insureco.com', phone: '+7 495 555 0305', region: 'Russia', specialty: 'property', rating: 4, color: '#8b5cf6', active: false },
120
+ { id: 'ad6', name: 'Thomas Becker', email: 'thomas.becker@insureco.com', phone: '+1 (206) 555-0306', region: 'United States', specialty: 'auto', rating: 4, color: '#0ea5e9', active: true },
121
+ ],
122
+ claims: [
123
+ { id: 'cl1', claimNumber: 'CLM-500001', policyId: 'pl1', adjusterId: 'ad1', type: 'collision', status: 'paid', amount: 8400, deductible: 500, severity: 55, fraudRisk: 1, tags: ['rear-end', 'no-injury'], incidentDate: '2026-01-04', filedAt: '2026-01-06T09:30:00.000Z' },
124
+ { id: 'cl2', claimNumber: 'CLM-500002', policyId: 'pl2', adjusterId: 'ad2', type: 'water', status: 'review', amount: 15200, deductible: 1000, severity: 70, fraudRisk: 2, tags: ['burst-pipe'], incidentDate: '2026-02-01', filedAt: '2026-02-03T14:15:00.000Z' },
125
+ { id: 'cl3', claimNumber: 'CLM-500003', policyId: 'pl3', adjusterId: 'ad3', type: 'medical', status: 'approved', amount: 6200, deductible: 750, severity: 40, fraudRisk: 1, tags: ['surgery'], incidentDate: '2026-02-10', filedAt: '2026-02-12T11:00:00.000Z' },
126
+ { id: 'cl4', claimNumber: 'CLM-500004', policyId: 'pl5', adjusterId: 'ad1', type: 'theft', status: 'denied', amount: 22000, deductible: 500, severity: 90, fraudRisk: 5, tags: ['suspicious', 'late-report'], incidentDate: '2026-01-20', filedAt: '2026-01-28T16:45:00.000Z' },
127
+ { id: 'cl5', claimNumber: 'CLM-500005', policyId: 'pl6', adjusterId: 'ad4', type: 'fire', status: 'paid', amount: 41000, deductible: 2000, severity: 95, fraudRisk: 2, tags: ['kitchen', 'total-loss'], incidentDate: '2026-03-01', filedAt: '2026-03-02T08:20:00.000Z' },
128
+ { id: 'cl6', claimNumber: 'CLM-500006', policyId: 'pl7', adjusterId: 'ad3', type: 'medical', status: 'filed', amount: 3100, deductible: 750, severity: 25, fraudRisk: 1, tags: ['ER-visit'], incidentDate: '2026-03-11', filedAt: '2026-03-12T13:10:00.000Z' },
129
+ { id: 'cl7', claimNumber: 'CLM-500007', policyId: 'pl8', adjusterId: 'ad6', type: 'collision', status: 'review', amount: 9700, deductible: 500, severity: 60, fraudRisk: 3, tags: ['intersection'], incidentDate: '2026-03-15', filedAt: '2026-03-17T10:05:00.000Z' },
130
+ { id: 'cl8', claimNumber: 'CLM-500008', policyId: 'pl1', adjusterId: 'ad6', type: 'theft', status: 'approved', amount: 5400, deductible: 500, severity: 50, fraudRisk: 2, tags: ['catalytic-converter'], incidentDate: '2026-02-22', filedAt: '2026-02-24T15:30:00.000Z' },
131
+ ],
132
+ }
133
+
134
+ // Claim grid formatting: status pills + a red-bold high-fraud threshold and a
135
+ // green highlight on large approved payouts - data-driven, not just pills.
136
+ const claimFormats: FormatRule[] = [
137
+ ...statusPills(claims, 'status'),
138
+ { field: 'fraudRisk', op: 'gte', value: 4, color: '#dc2626', bold: true },
139
+ { field: 'amount', op: 'gte', value: 20000, color: '#b45309', bold: true },
140
+ ]
141
+
142
+ export const insurance: SampleApp = {
143
+ id: 'insurance',
144
+ name: 'Insurance Claims',
145
+ description: 'Policies, adjusters and claims - a SAP Fiori-style claims desk behind a sign-in (adjuster and admin roles), with a claims pipeline board (Filed -> Paid), payout + approval dashboards (target KPIs, a filings sparkline, a severity gauge, type/status breakdowns, an adjuster x status pivot, tabbed views), a fraud-risk red grid, policy master/detail with a claims timeline, and rich edit forms (policy/claim number masks, region, adjuster phone, risk + fraud ratings, severity slider, triage chips, filing datetime).',
146
+ emoji: '\u{1F6E1}\u{FE0F}',
147
+ accent: '#0070f2',
148
+ build: () => {
149
+ const policyRows = pad(policies, seed.policies, 40)
150
+ const adjusterRows = pad(adjusters, seed.adjusters, 16)
151
+ const claimRows = pad(claims, seed.claims, 90, { policyId: ids(policyRows), adjusterId: ids(adjusterRows) })
152
+ return project({
153
+ title: 'Claims',
154
+ brand: 'Assurance',
155
+ accent: '#0070f2',
156
+ preset: 'sap',
157
+ footer: '',
158
+ // SAP Fiori-style enterprise shell: a dark shell bar + a launchpad rail.
159
+ appClass: 'theme-fiori',
160
+ customCss: FIORI_CSS,
161
+ // Sign-in with two roles. Adjusters work the claims queue but cannot delete;
162
+ // the policy portfolio and the adjuster roster are admin-only, so they drop
163
+ // out of an adjuster's nav.
164
+ auth: { enabled: true, protect: true },
165
+ access: {
166
+ enabled: true,
167
+ defaultRole: 'adjuster',
168
+ roles: [
169
+ { role: 'admin', screens: '*', actions: '*' },
170
+ { role: 'adjuster', screens: ['overview', 'pipeline', 'claim-detail', 'claims', 'calendar'], actions: ['create', 'update'] },
171
+ ],
172
+ },
173
+ entities: [policies, adjusters, claims],
174
+ seed: { policies: policyRows, adjusters: adjusterRows, claims: claimRows },
175
+ screens: [
176
+ // Claims dashboard: payout + approval KPIs (total payout carries a target,
177
+ // filings trend as a sparkline), a severity gauge, type/status breakdowns,
178
+ // an adjuster x status pivot, tabs, and a fraud-risk status-pill grid that
179
+ // drills into the claim detail.
180
+ dashScreen(claims, { id: 'overview', title: 'Claims', order: 0 }, [
181
+ { kpi: 'Claims', reduce: 'count', span: 1 },
182
+ { kpi: 'Total payout', measure: 'net', reduce: 'sum', format: 'currency', target: 200000, span: 1 },
183
+ { kpi: 'Filings', measure: 'amount', reduce: 'count', trendField: 'filedAt', trendReduce: 'count', span: 1 },
184
+ { gauge: 'Avg severity', measure: 'severity', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
185
+ { chart: 'status', measure: 'amount', reduce: 'sum', type: 'bar', span: 2 },
186
+ { chart: 'type', reduce: 'count', type: 'pie', span: 1 },
187
+ { chart: 'filedAt', measure: 'amount', reduce: 'sum', type: 'area', span: 3 },
188
+ { pivot: { rows: ['adjusterId'], cols: ['status'], measure: 'amount', aggregate: 'sum' }, span: 3 },
189
+ { tabs: [
190
+ { label: 'Payout by status', tiles: [{ chart: 'status', measure: 'net', reduce: 'sum', type: 'bar' }] },
191
+ { label: 'Claims by type', tiles: [{ chart: 'type', reduce: 'count', type: 'bar' }] },
192
+ ], span: 3 },
193
+ { grid: true, format: claimFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'claim-detail', sourceField: 'id', targetField: 'id' }, span: 3 },
194
+ ]),
195
+ // The signature view: the claims pipeline as draggable Kanban columns by
196
+ // status, each card the claim number + policy + payout; opens the detail.
197
+ boardScreen(claims, { id: 'pipeline', title: 'Pipeline', order: 1 }, { groupBy: 'status', titleField: 'claimNumber', subtitleField: 'policyId', badgeField: 'amount', filter: ['status', 'type', 'adjusterId'], openScreen: 'claim-detail' }),
198
+ // Claim detail: header (claim # + policy + status pill), payout metric
199
+ // tiles, and Overview sections.
200
+ detailScreen(claims, { id: 'claim-detail', title: 'Claim detail', order: 2 }, {
201
+ titleField: 'claimNumber', subtitleField: 'policyId', statusField: 'status',
202
+ metricFields: ['amount', 'net'],
203
+ sections: [
204
+ { label: 'Assessment', fields: ['type', 'severity', 'fraudRisk', 'tags'] },
205
+ { label: 'Handling', fields: ['adjusterId', 'deductible', 'incidentDate', 'filedAt'] },
206
+ ],
207
+ }),
208
+ // Policy 360: a full policy record with a timeline of its claims.
209
+ detailScreen(policies, { id: 'policy-360', title: 'Policy 360', order: 3 }, {
210
+ titleField: 'policyNumber', subtitleField: 'holder', statusField: 'status',
211
+ metricFields: ['premium', 'coverage'],
212
+ sections: [{ label: 'Underwriting', fields: ['type', 'region', 'riskScore', 'startDate'] }],
213
+ related: [{ entity: 'claims', foreignKey: 'policyId', label: 'Claims', titleField: 'claimNumber', subtitleField: 'amount', dateField: 'filedAt', statusField: 'status' }],
214
+ }),
215
+ screen(policies, 'master-detail', { id: 'policies', title: 'Policies', order: 4, child: claims, foreignKey: 'policyId', linkScreen: 'policy-360' }),
216
+ formScreen(claims, { id: 'claims', title: 'Claims list', order: 5 }, undefined, { format: claimFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'claim-detail', sourceField: 'id', targetField: 'id' } }, ['status', 'type', 'policyId']),
217
+ formScreen(adjusters, { id: 'adjusters', title: 'Adjusters', order: 6 }, undefined, {}, ['specialty', 'region', 'active']),
218
+ // Incident calendar: claims placed on a month calendar by incident date.
219
+ calendarScreen(claims, { id: 'calendar', title: 'Incidents', order: 7 }, { dateField: 'incidentDate', titleField: 'claimNumber', filter: ['status', 'type', 'adjusterId'] }),
220
+ ],
221
+ })
222
+ },
223
+ }