@svgrid/enterprise 2.2.1 → 2.3.1

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 (126) hide show
  1. package/README.md +95 -81
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +26642 -11252
  3. package/dist/designer/assets/GridMenus-BtWVk9Ab.js +7 -0
  4. package/dist/designer/assets/SvGridChartPanel-DWlBO2SD.js +10 -0
  5. package/dist/designer/assets/SvGridChartView-8c8Mb4j8.js +1 -0
  6. package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +1 -0
  7. package/dist/designer/assets/index-DsDgp9Xq.js +78758 -0
  8. package/dist/designer/assets/index-tTY_Dx4P.css +1 -0
  9. package/dist/designer/assets/jszip.min-fkJdmAmj.js +2 -0
  10. package/dist/designer/assets/pdfmake-DeCsnyl9.js +242 -0
  11. package/dist/designer/assets/smart.export-BZlSCE8T.js +35 -0
  12. package/dist/designer/assets/vfs_fonts-eX2NpmfX.js +1 -0
  13. package/dist/designer/index.html +13 -0
  14. package/dist/node/studio.js +15304 -2708
  15. package/package.json +35 -11
  16. package/src/SvAlertRuleEditor.svelte +294 -0
  17. package/src/SvAlertsManager.svelte +210 -0
  18. package/src/SvAlertsPanel.svelte +129 -0
  19. package/src/SvBoard.svelte +3 -1
  20. package/src/SvExpressionEditor.svelte +341 -0
  21. package/src/SvGridAlerts.dom.test.ts +113 -0
  22. package/src/SvGridAlerts.svelte +265 -0
  23. package/src/SvGridBoard.svelte +2358 -0
  24. package/src/SvGridEditPanel.svelte +849 -794
  25. package/src/SvGridScheduler.svelte +5358 -4410
  26. package/src/SvPivotDesigner.svelte +3 -3
  27. package/src/SvRecordDetail.svelte +6 -2
  28. package/src/SvSchedule.svelte +3 -1
  29. package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +77 -74
  30. package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +90 -87
  31. package/src/{ai-export.test.ts → ai-export.dom.test.ts} +114 -110
  32. package/src/alerts/alert-engine-attach.ts +165 -0
  33. package/src/alerts/alert-engine.test.ts +135 -0
  34. package/src/alerts/alert-engine.ts +260 -0
  35. package/src/alerts/alert-formats.test.ts +87 -0
  36. package/src/alerts/alert-formats.ts +77 -0
  37. package/src/alerts/alert-observer.test.ts +189 -0
  38. package/src/alerts/alert-observer.ts +208 -0
  39. package/src/alerts/alert-scheduler.ts +96 -0
  40. package/src/alerts/alert-storage.test.ts +54 -0
  41. package/src/alerts/alert-storage.ts +116 -0
  42. package/src/alerts/alert-store.svelte.ts +80 -0
  43. package/src/alerts/alert-types.ts +94 -0
  44. package/src/alerts.ts +28 -0
  45. package/src/board.dom.test.ts +941 -0
  46. package/src/board.ts +36 -0
  47. package/src/export-ooxml.ts +4 -0
  48. package/src/export-xls.ts +3 -0
  49. package/src/export.ts +6 -0
  50. package/src/expressions/evaluate.test.ts +111 -0
  51. package/src/expressions/evaluate.ts +237 -0
  52. package/src/expressions/expression-columns.ts +133 -0
  53. package/src/expressions/expression-types.ts +84 -0
  54. package/src/expressions/parse.test.ts +106 -0
  55. package/src/expressions/parse.ts +610 -0
  56. package/src/import.test.ts +1 -1
  57. package/src/import.ts +3 -1
  58. package/src/index.ts +192 -46
  59. package/src/install.ts +12 -2
  60. package/src/pivot-enable.ts +44 -0
  61. package/src/pivot.test.ts +0 -1
  62. package/src/scheduler-assignments.test.ts +97 -0
  63. package/src/scheduler-assignments.ts +134 -0
  64. package/src/scheduler-axis.test.ts +108 -0
  65. package/src/scheduler-axis.ts +238 -0
  66. package/src/scheduler-booking.test.ts +57 -0
  67. package/src/scheduler-booking.ts +63 -0
  68. package/src/scheduler-config.ts +179 -0
  69. package/src/scheduler-dependencies.test.ts +155 -0
  70. package/src/scheduler-dependencies.ts +223 -0
  71. package/src/scheduler-freebusy.test.ts +41 -0
  72. package/src/scheduler-freebusy.ts +36 -0
  73. package/src/scheduler-heatmap.test.ts +39 -0
  74. package/src/scheduler-heatmap.ts +55 -0
  75. package/src/scheduler-resource-tree.test.ts +84 -0
  76. package/src/scheduler-resource-tree.ts +107 -0
  77. package/src/scheduler-slots.test.ts +53 -0
  78. package/src/scheduler-slots.ts +94 -0
  79. package/src/scheduler-summary.test.ts +47 -0
  80. package/src/scheduler-summary.ts +81 -0
  81. package/src/schema-designer.ts +1 -1
  82. package/src/sources/index.ts +1 -1
  83. package/src/sources/introspect-supabase.test.ts +13 -1
  84. package/src/sources/introspect-supabase.ts +20 -0
  85. package/src/studio/copilot-core.test.ts +45 -0
  86. package/src/studio/copilot-core.ts +65 -0
  87. package/src/studio/deploy-cli.test.ts +56 -0
  88. package/src/studio/deploy-cli.ts +100 -0
  89. package/src/studio/emit-project.test.ts +800 -24
  90. package/src/studio/emit-project.ts +1307 -135
  91. package/src/studio/emit-schema.test.ts +17 -0
  92. package/src/studio/emit-schema.ts +182 -41
  93. package/src/studio/index.ts +59 -1
  94. package/src/studio/init-flow.test.ts +239 -0
  95. package/src/studio/init-flow.ts +358 -0
  96. package/src/studio/introspect-openapi.test.ts +84 -0
  97. package/src/studio/introspect-openapi.ts +252 -0
  98. package/src/studio/project.test.ts +57 -1
  99. package/src/studio/project.ts +302 -8
  100. package/src/studio/samples/crm.ts +282 -258
  101. package/src/studio/samples/datasets.test.ts +84 -0
  102. package/src/studio/samples/datasets.ts +340 -0
  103. package/src/studio/samples/fleet.ts +213 -186
  104. package/src/studio/samples/insurance.ts +223 -195
  105. package/src/studio/samples/inventory.ts +213 -182
  106. package/src/studio/samples/live-data.test.ts +99 -98
  107. package/src/studio/samples/live-data.ts +8 -10
  108. package/src/studio/samples/projects.ts +212 -190
  109. package/src/studio/samples/samples.test.ts +268 -216
  110. package/src/studio/samples/shared.ts +22 -145
  111. package/src/studio/samples/starter.ts +251 -0
  112. package/src/studio/samples/support.ts +209 -184
  113. package/src/studio/screen-suites.test.ts +226 -0
  114. package/src/studio/screen-suites.ts +445 -0
  115. package/src/studio/ui-components-surface.test.ts +185 -0
  116. package/src/studio/ui-components.generated.ts +7154 -0
  117. package/src/studio/ui-components.ts +273 -3
  118. package/src/sveltekit/index.ts +1 -0
  119. package/src/sveltekit/sql-source.test.ts +13 -0
  120. package/src/sveltekit/sql-source.ts +12 -7
  121. package/src/sveltekit/transport-scope.test.ts +125 -0
  122. package/src/sveltekit/transport.ts +76 -2
  123. package/src/upgrade-prompt.ts +2 -2
  124. package/src/watermark.ts +2 -2
  125. package/src/ai.test.ts +0 -522
  126. package/src/ai.ts +0 -1388
@@ -1,184 +1,209 @@
1
- import type { EntitySchema } from '../../schema.js'
2
- import { screen, formScreen, boardScreen, workspaceScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
3
-
4
- const customers: EntitySchema = {
5
- name: 'customers',
6
- label: 'Customer',
7
- idField: 'id',
8
- fields: [
9
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
10
- { field: 'name', type: 'text', required: true, minLength: 2 },
11
- { field: 'email', type: 'text', format: 'email', required: true, input: { help: 'Primary contact address' } },
12
- { field: 'phone', type: 'text', input: { editorType: 'phone', help: 'Support callback number' } },
13
- { field: 'country', type: 'text', label: 'Country', input: { editorType: 'country', help: 'Billing country' } },
14
- { field: 'accountNo', type: 'text', label: 'Account #', input: { editorType: 'mask', mask: '####-####', help: 'Internal billing account number' } },
15
- { field: 'plan', type: 'enum', required: true, defaultValue: 'free', options: [
16
- { value: 'free', label: 'Free', color: '#94a3b8' },
17
- { value: 'pro', label: 'Pro', color: '#6366f1' },
18
- { value: 'business', label: 'Business', color: '#f59e0b' },
19
- ] },
20
- { field: 'signupAt', type: 'datetime', label: 'Signed up', input: { editorType: 'datetime', help: 'When the account was created' } },
21
- { field: 'health', type: 'number', label: 'Account health', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 relationship score' } },
22
- { field: 'active', type: 'boolean', defaultValue: true },
23
- ],
24
- }
25
-
26
- const agents: EntitySchema = {
27
- name: 'agents',
28
- label: 'Agent',
29
- idField: 'id',
30
- fields: [
31
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
32
- { field: 'name', type: 'text', required: true, minLength: 2 },
33
- { field: 'email', type: 'text', format: 'email', required: true, input: { help: 'Work address' } },
34
- { field: 'team', type: 'enum', required: true, defaultValue: 'technical', options: [
35
- { value: 'billing', label: 'Billing', color: '#10b981' },
36
- { value: 'technical', label: 'Technical', color: '#6366f1' },
37
- { value: 'onboarding', label: 'Onboarding', color: '#f59e0b' },
38
- ] },
39
- { field: 'phone', type: 'text', input: { editorType: 'phone', help: 'Desk extension' } },
40
- { field: 'skills', type: 'text', label: 'Skills', input: { editorType: 'chips', help: 'Tools and specialities' } },
41
- { field: 'capacity', type: 'number', label: 'Capacity (%)', min: 0, max: 100, defaultValue: 80, input: { editorType: 'slider', help: 'Share of a full ticket load' } },
42
- { field: 'csat', type: 'number', label: 'CSAT', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: 'Rolling satisfaction score' } },
43
- { field: 'calendarColor', type: 'text', label: 'Calendar color', defaultValue: '#6366f1', input: { editorType: 'color', help: 'Shown on the shift calendar' } },
44
- { field: 'active', type: 'boolean', defaultValue: true },
45
- ],
46
- }
47
-
48
- const tickets: EntitySchema = {
49
- name: 'tickets',
50
- label: 'Ticket',
51
- idField: 'id',
52
- fields: [
53
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
54
- { field: 'subject', type: 'text', required: true, minLength: 3 },
55
- { field: 'customerId', type: 'relation', label: 'Customer', relation: { entity: 'customers', foreignKey: 'customerId', labelField: 'name' } },
56
- { field: 'agentId', type: 'relation', label: 'Agent', relation: { entity: 'agents', foreignKey: 'agentId', labelField: 'name' } },
57
- { field: 'priority', type: 'enum', required: true, defaultValue: 'normal', options: [
58
- { value: 'low', label: 'Low', color: '#94a3b8' },
59
- { value: 'normal', label: 'Normal', color: '#6366f1' },
60
- { value: 'high', label: 'High', color: '#f59e0b' },
61
- { value: 'urgent', label: 'Urgent', color: '#ef4444' },
62
- ] },
63
- { field: 'status', type: 'enum', required: true, defaultValue: 'open', options: [
64
- { value: 'open', label: 'Open', color: '#6366f1' },
65
- { value: 'pending', label: 'Pending', color: '#f59e0b' },
66
- { value: 'resolved', label: 'Resolved', color: '#10b981' },
67
- { value: 'closed', label: 'Closed', color: '#94a3b8' },
68
- ] },
69
- { field: 'channel', type: 'enum', options: [
70
- { value: 'email', label: 'Email', color: '#6366f1' },
71
- { value: 'chat', label: 'Chat', color: '#10b981' },
72
- { value: 'phone', label: 'Phone', color: '#f59e0b' },
73
- ] },
74
- { field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Topic labels for routing' } },
75
- { field: 'createdAt', type: 'datetime', label: 'Created', input: { editorType: 'datetime', help: 'When the ticket was opened' } },
76
- { field: 'firstResponseMins', type: 'number', label: 'First response (min)', min: 0, input: { help: 'Time to the first agent reply' } },
77
- { field: 'satisfaction', type: 'number', label: 'CSAT', min: 1, max: 5, input: { editorType: 'rating', help: 'Customer rating on close' } },
78
- { field: 'estimateHrs', type: 'number', label: 'Estimate (hrs)', min: 0, defaultValue: 1, input: { help: 'Estimated effort to resolve' } },
79
- { field: 'hourlyRate', type: 'number', label: 'Rate ($/hr)', min: 0, defaultValue: 90, input: { help: 'Billed support rate' } },
80
- // Derived: estimated support cost for the ticket (display-only, never seeded).
81
- { field: 'cost', type: 'number', label: 'Cost ($)', readonly: true, formula: 'estimateHrs * hourlyRate' },
82
- ],
83
- }
84
-
85
- const seed = {
86
- customers: [
87
- { id: 'c1', name: 'Ada Lovelace', email: 'ada@analytic.io', phone: '+1 (415) 555-0101', country: 'United States', accountNo: '4021-7788', plan: 'business', signupAt: '2025-09-12T09:15:00Z', health: 5, active: true },
88
- { id: 'c2', name: 'Alan Turing', email: 'alan@bletchley.uk', phone: '+44 20 7946 0102', country: 'United Kingdom', accountNo: '5590-2143', plan: 'pro', signupAt: '2025-11-03T14:20:00Z', health: 4, active: true },
89
- { id: 'c3', name: 'Grace Hopper', email: 'grace@navy.mil', phone: '+1 (202) 555-0103', country: 'United States', accountNo: '7781-6620', plan: 'business', signupAt: '2026-01-21T10:05:00Z', health: 5, active: true },
90
- { id: 'c4', name: 'Linus Torvalds', email: 'linus@kernel.org', phone: '+1 (503) 555-0104', country: 'United States', accountNo: '3312-9087', plan: 'pro', signupAt: '2026-03-08T08:45:00Z', health: 3, active: true },
91
- { id: 'c5', name: 'Barbara Liskov', email: 'barbara@sub.dev', phone: '+1 (617) 555-0105', country: 'United States', accountNo: '9001-4455', plan: 'free', signupAt: '2026-05-30T16:30:00Z', health: 2, active: false },
92
- ],
93
- agents: [
94
- { id: 'a1', name: 'Sam Rivera', email: 'sam@support.io', team: 'technical', phone: '+1 (415) 555-0201', skills: ['TypeScript', 'AWS', 'GraphQL'], capacity: 85, csat: 5, calendarColor: '#6366f1', active: true },
95
- { id: 'a2', name: 'Jamie Chen', email: 'jamie@support.io', team: 'billing', phone: '+1 (312) 555-0202', skills: ['SQL', 'Leadership'], capacity: 70, csat: 4, calendarColor: '#10b981', active: true },
96
- { id: 'a3', name: 'Priya Patel', email: 'priya@support.io', team: 'onboarding', phone: '+1 (206) 555-0203', skills: ['Product', 'UX', 'Figma'], capacity: 60, csat: 4, calendarColor: '#f59e0b', active: true },
97
- ],
98
- tickets: [
99
- { id: 'tk1', subject: 'Cannot reset password', customerId: 'c1', agentId: 'a1', priority: 'high', status: 'open', channel: 'email', tags: ['auth', 'login'], createdAt: '2026-07-10T08:30:00Z', firstResponseMins: 12, satisfaction: 4, estimateHrs: 2, hourlyRate: 90 },
100
- { id: 'tk2', subject: 'Invoice discrepancy', customerId: 'c2', agentId: 'a2', priority: 'normal', status: 'pending', channel: 'email', tags: ['billing'], createdAt: '2026-07-08T11:05:00Z', firstResponseMins: 45, satisfaction: 3, estimateHrs: 1, hourlyRate: 75 },
101
- { id: 'tk3', subject: 'API returns 500', customerId: 'c3', agentId: 'a1', priority: 'urgent', status: 'open', channel: 'chat', tags: ['api', 'outage'], createdAt: '2026-07-12T15:40:00Z', firstResponseMins: 4, satisfaction: 5, estimateHrs: 4, hourlyRate: 120 },
102
- { id: 'tk4', subject: 'How do I export data?', customerId: 'c4', agentId: 'a3', priority: 'low', status: 'resolved', channel: 'chat', tags: ['export'], createdAt: '2026-07-02T09:20:00Z', firstResponseMins: 20, satisfaction: 5, estimateHrs: 1, hourlyRate: 60 },
103
- { id: 'tk5', subject: 'Upgrade to Business', customerId: 'c5', agentId: 'a3', priority: 'normal', status: 'closed', channel: 'phone', tags: ['upgrade', 'billing'], createdAt: '2026-06-28T13:10:00Z', firstResponseMins: 8, satisfaction: 4, estimateHrs: 1, hourlyRate: 75 },
104
- { id: 'tk6', subject: 'SSO configuration', customerId: 'c1', agentId: 'a1', priority: 'high', status: 'pending', channel: 'email', tags: ['sso', 'security'], createdAt: '2026-07-11T10:00:00Z', firstResponseMins: 60, satisfaction: 2, estimateHrs: 3, hourlyRate: 120 },
105
- { id: 'tk7', subject: 'Double charge refund', customerId: 'c2', agentId: 'a2', priority: 'urgent', status: 'resolved', channel: 'phone', tags: ['refund', 'billing'], createdAt: '2026-07-05T16:25:00Z', firstResponseMins: 15, satisfaction: 4, estimateHrs: 2, hourlyRate: 90 },
106
- { id: 'tk8', subject: 'Feature request: dark mode', customerId: 'c3', agentId: 'a3', priority: 'low', status: 'open', channel: 'chat', tags: ['feature'], createdAt: '2026-07-13T12:50:00Z', firstResponseMins: 90, satisfaction: 3, estimateHrs: 5, hourlyRate: 100 },
107
- ],
108
- }
109
-
110
- export const support: SampleApp = {
111
- id: 'support',
112
- name: 'Support desk',
113
- description: 'Tickets, customers and agents - triage + SLA dashboards (CSAT sparkline + gauge, pivot, tabbed breakdown), a team-capacity dashboard, status-pill grids, master/detail, and rich edit forms.',
114
- emoji: '\u{1F3AF}',
115
- accent: '#0ea5e9',
116
- build: () => {
117
- const customerRows = pad(customers, seed.customers, 16)
118
- const agentRows = pad(agents, seed.agents, 6)
119
- const ticketRows = pad(tickets, seed.tickets, 36, { customerId: ids(customerRows), agentId: ids(agentRows) })
120
- return project({
121
- title: 'Support Desk',
122
- brand: 'Support Desk',
123
- accent: '#0ea5e9',
124
- preset: 'atlassian',
125
- footer: '',
126
- entities: [customers, agents, tickets],
127
- seed: { customers: customerRows, agents: agentRows, tickets: ticketRows },
128
- screens: [
129
- // Triage dashboard: volume + CSAT KPIs (CSAT card trends over created date),
130
- // a CSAT gauge, status/priority/channel breakdowns, a tabbed view, and a
131
- // ticket grid with status AND priority pills.
132
- dashScreen(tickets, { id: 'overview', title: 'Overview', order: 0 }, [
133
- { kpi: 'Open tickets', reduce: 'count', span: 1 },
134
- { kpi: 'Avg first response', measure: 'firstResponseMins', reduce: 'avg', span: 1 },
135
- { kpi: 'Avg CSAT', measure: 'satisfaction', reduce: 'avg', trendField: 'createdAt', trendReduce: 'avg', span: 1 },
136
- { gauge: 'Avg CSAT', measure: 'satisfaction', reduce: 'avg', min: 0, max: 5, span: 1 },
137
- { chart: 'status', reduce: 'count', type: 'bar', span: 2 },
138
- { chart: 'priority', reduce: 'count', type: 'pie', span: 1 },
139
- { chart: 'channel', reduce: 'count', type: 'bar', span: 2 },
140
- { tabs: [
141
- { label: 'By status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
142
- { label: 'By priority', tiles: [{ chart: 'priority', reduce: 'count', type: 'bar' }] },
143
- { label: 'By channel', tiles: [{ chart: 'channel', reduce: 'count', type: 'bar' }] },
144
- ], span: 3 },
145
- { grid: true, format: [...statusPills(tickets, 'status'), ...statusPills(tickets, 'priority')], summaries: true, span: 3 },
146
- ]),
147
- // SLA / quality: response + CSAT KPIs, a CSAT gauge, and a pivot of CSAT by
148
- // priority (rows) x status (columns).
149
- // Ticket board: tickets as cards in status columns (drag to change status).
150
- boardScreen(tickets, { id: 'board', title: 'Board', order: 1 }, { groupBy: 'status', titleField: 'subject', subtitleField: 'priority', filter: ['priority', 'channel', 'agentId'] }),
151
- // A docking console: filter | ticket queue | selected-ticket panel (drag / float / pin).
152
- workspaceScreen(tickets, { id: 'console', title: 'Console', order: 2 }, { filter: ['priority', 'channel', 'agentId'], record: true, grid: { format: [...statusPills(tickets, 'status'), ...statusPills(tickets, 'priority')] } }),
153
- // Customer 360: account header (plan pill + health tile) with a timeline of
154
- // the customer's tickets (newest first, colored by status).
155
- detailScreen(customers, { id: 'customer-360', title: 'Customer 360', order: 2 }, {
156
- titleField: 'name', subtitleField: 'email', statusField: 'plan', metricFields: ['health'],
157
- related: [{ entity: 'tickets', foreignKey: 'customerId', label: 'Tickets', titleField: 'subject', subtitleField: 'priority', dateField: 'createdAt', statusField: 'status' }],
158
- }),
159
- dashScreen(tickets, { id: 'performance', title: 'Performance', order: 3 }, [
160
- { kpi: 'Avg first response', measure: 'firstResponseMins', reduce: 'avg', span: 1 },
161
- { kpi: 'Avg CSAT', measure: 'satisfaction', reduce: 'avg', span: 1 },
162
- { kpi: 'Est. hours', measure: 'estimateHrs', reduce: 'sum', format: 'number', span: 1 },
163
- { gauge: 'Avg CSAT', measure: 'satisfaction', reduce: 'avg', min: 0, max: 5, span: 1 },
164
- { chart: 'channel', measure: 'firstResponseMins', reduce: 'avg', type: 'bar', span: 2 },
165
- { chart: 'priority', measure: 'satisfaction', reduce: 'avg', type: 'bar', span: 1 },
166
- { pivot: { rows: ['priority'], cols: ['status'], measure: 'satisfaction', aggregate: 'avg' }, span: 3 },
167
- ]),
168
- // Team load: capacity gauge + a per-agent capacity leaderboard.
169
- dashScreen(agents, { id: 'team', title: 'Team', order: 4 }, [
170
- { kpi: 'Agents', reduce: 'count', span: 1 },
171
- { kpi: 'Avg capacity', measure: 'capacity', reduce: 'avg', format: 'percent', span: 1 },
172
- { kpi: 'Avg CSAT', measure: 'csat', reduce: 'avg', span: 1 },
173
- { gauge: 'Avg team capacity', measure: 'capacity', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
174
- { chart: 'name', measure: 'capacity', reduce: 'avg', type: 'bar', span: 2 },
175
- { chart: 'team', reduce: 'count', type: 'pie', span: 1 },
176
- { grid: true, format: statusPills(agents, 'team'), summaries: true, span: 3 },
177
- ]),
178
- formScreen(tickets, { id: 'tickets', title: 'Tickets', order: 5 }, undefined, { format: [...statusPills(tickets, 'status'), ...statusPills(tickets, 'priority')], summaries: true }),
179
- screen(customers, 'master-detail', { id: 'customers', title: 'Customers', order: 6, child: tickets, foreignKey: 'customerId', linkScreen: 'customer-360' }),
180
- formScreen(agents, { id: 'agents', title: 'Agents', order: 7 }),
181
- ],
182
- })
183
- },
184
- }
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import { screen, formScreen, boardScreen, workspaceScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
3
+
4
+ // Outlook-style chrome: a solid blue command ribbon (the toolbar), a light "folder"
5
+ // rail with compact rows, and an accented left bar on the active folder.
6
+ const OUTLOOK_CSS = `.sv-app.theme-outlook .sv-app__toolbar { background: #0f6cbd; border-bottom: 0; }
7
+ .sv-app.theme-outlook .sv-app__toolbar, .sv-app.theme-outlook .sv-app__search-ic, .sv-app.theme-outlook .sv-app__kbd { color: #fff; }
8
+ .sv-app.theme-outlook .sv-app__search { background: rgba(255, 255, 255, 0.16); border-color: rgba(255, 255, 255, 0.28); }
9
+ .sv-app.theme-outlook .sv-app__search-in { color: #fff; }
10
+ .sv-app.theme-outlook .sv-app__search-in::placeholder { color: rgba(255, 255, 255, 0.75); }
11
+ .sv-app.theme-outlook .sv-app__side { background: #faf9f8; padding: 12px 8px; }
12
+ .sv-app.theme-outlook .sv-app__brandtext { color: #0f6cbd; font-weight: 700; }
13
+ .sv-app.theme-outlook .sv-app__link { border-radius: 4px; padding: 6px 12px; font-size: 13.5px; }
14
+ .sv-app.theme-outlook .sv-app__link.is-active { background: color-mix(in srgb, #0f6cbd 12%, #fff); color: #0f6cbd; box-shadow: inset 3px 0 0 #0f6cbd; }`
15
+
16
+ const customers: EntitySchema = {
17
+ name: 'customers',
18
+ label: 'Customer',
19
+ idField: 'id',
20
+ fields: [
21
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
22
+ { field: 'name', type: 'text', required: true, minLength: 2 },
23
+ { field: 'email', type: 'text', format: 'email', required: true, input: { help: 'Primary contact address' } },
24
+ { field: 'phone', type: 'text', input: { editorType: 'phone', help: 'Support callback number' } },
25
+ { field: 'country', type: 'text', label: 'Country', input: { editorType: 'country', help: 'Billing country' } },
26
+ { field: 'accountNo', type: 'text', label: 'Account #', input: { editorType: 'mask', mask: '####-####', help: 'Internal billing account number' } },
27
+ { field: 'plan', type: 'enum', required: true, defaultValue: 'free', options: [
28
+ { value: 'free', label: 'Free', color: '#94a3b8' },
29
+ { value: 'pro', label: 'Pro', color: '#6366f1' },
30
+ { value: 'business', label: 'Business', color: '#f59e0b' },
31
+ ] },
32
+ { field: 'signupAt', type: 'datetime', label: 'Signed up', input: { editorType: 'datetime', help: 'When the account was created' } },
33
+ { field: 'health', type: 'number', label: 'Account health', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 relationship score' } },
34
+ { field: 'active', type: 'boolean', defaultValue: true },
35
+ ],
36
+ }
37
+
38
+ const agents: EntitySchema = {
39
+ name: 'agents',
40
+ label: 'Agent',
41
+ idField: 'id',
42
+ fields: [
43
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
44
+ { field: 'name', type: 'text', required: true, minLength: 2 },
45
+ { field: 'email', type: 'text', format: 'email', required: true, input: { help: 'Work address' } },
46
+ { field: 'team', type: 'enum', required: true, defaultValue: 'technical', options: [
47
+ { value: 'billing', label: 'Billing', color: '#10b981' },
48
+ { value: 'technical', label: 'Technical', color: '#6366f1' },
49
+ { value: 'onboarding', label: 'Onboarding', color: '#f59e0b' },
50
+ ] },
51
+ { field: 'phone', type: 'text', input: { editorType: 'phone', help: 'Desk extension' } },
52
+ { field: 'skills', type: 'text', label: 'Skills', input: { editorType: 'chips', help: 'Tools and specialities' } },
53
+ { field: 'capacity', type: 'number', label: 'Capacity (%)', min: 0, max: 100, defaultValue: 80, input: { editorType: 'slider', help: 'Share of a full ticket load' } },
54
+ { field: 'csat', type: 'number', label: 'CSAT', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: 'Rolling satisfaction score' } },
55
+ { field: 'calendarColor', type: 'text', label: 'Calendar color', defaultValue: '#6366f1', input: { editorType: 'color', help: 'Shown on the shift calendar' } },
56
+ { field: 'active', type: 'boolean', defaultValue: true },
57
+ ],
58
+ }
59
+
60
+ const tickets: EntitySchema = {
61
+ name: 'tickets',
62
+ label: 'Ticket',
63
+ idField: 'id',
64
+ fields: [
65
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
66
+ { field: 'subject', type: 'text', required: true, minLength: 3 },
67
+ { field: 'customerId', type: 'relation', label: 'Customer', relation: { entity: 'customers', foreignKey: 'customerId', labelField: 'name' } },
68
+ { field: 'agentId', type: 'relation', label: 'Agent', relation: { entity: 'agents', foreignKey: 'agentId', labelField: 'name' } },
69
+ { field: 'priority', type: 'enum', required: true, defaultValue: 'normal', options: [
70
+ { value: 'low', label: 'Low', color: '#94a3b8' },
71
+ { value: 'normal', label: 'Normal', color: '#6366f1' },
72
+ { value: 'high', label: 'High', color: '#f59e0b' },
73
+ { value: 'urgent', label: 'Urgent', color: '#ef4444' },
74
+ ] },
75
+ { field: 'status', type: 'enum', required: true, defaultValue: 'open', options: [
76
+ { value: 'open', label: 'Open', color: '#6366f1' },
77
+ { value: 'pending', label: 'Pending', color: '#f59e0b' },
78
+ { value: 'resolved', label: 'Resolved', color: '#10b981' },
79
+ { value: 'closed', label: 'Closed', color: '#94a3b8' },
80
+ ] },
81
+ { field: 'channel', type: 'enum', options: [
82
+ { value: 'email', label: 'Email', color: '#6366f1' },
83
+ { value: 'chat', label: 'Chat', color: '#10b981' },
84
+ { value: 'phone', label: 'Phone', color: '#f59e0b' },
85
+ ] },
86
+ { field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Topic labels for routing' } },
87
+ { field: 'createdAt', type: 'datetime', label: 'Created', input: { editorType: 'datetime', help: 'When the ticket was opened' } },
88
+ { field: 'firstResponseMins', type: 'number', label: 'First response (min)', min: 0, input: { help: 'Time to the first agent reply' } },
89
+ { field: 'satisfaction', type: 'number', label: 'CSAT', min: 1, max: 5, input: { editorType: 'rating', help: 'Customer rating on close' } },
90
+ { field: 'estimateHrs', type: 'number', label: 'Estimate (hrs)', min: 0, defaultValue: 1, input: { help: 'Estimated effort to resolve' } },
91
+ { field: 'hourlyRate', type: 'number', label: 'Rate ($/hr)', min: 0, defaultValue: 90, input: { help: 'Billed support rate' } },
92
+ // Derived: estimated support cost for the ticket (display-only, never seeded).
93
+ { field: 'cost', type: 'number', label: 'Cost ($)', readonly: true, formula: 'estimateHrs * hourlyRate' },
94
+ ],
95
+ }
96
+
97
+ const seed = {
98
+ customers: [
99
+ { id: 'c1', name: 'Ada Lovelace', email: 'ada@analytic.io', phone: '+1 (415) 555-0101', country: 'United States', accountNo: '4021-7788', plan: 'business', signupAt: '2025-09-12T09:15:00Z', health: 5, active: true },
100
+ { id: 'c2', name: 'Alan Turing', email: 'alan@bletchley.uk', phone: '+44 20 7946 0102', country: 'United Kingdom', accountNo: '5590-2143', plan: 'pro', signupAt: '2025-11-03T14:20:00Z', health: 4, active: true },
101
+ { id: 'c3', name: 'Grace Hopper', email: 'grace@navy.mil', phone: '+1 (202) 555-0103', country: 'United States', accountNo: '7781-6620', plan: 'business', signupAt: '2026-01-21T10:05:00Z', health: 5, active: true },
102
+ { id: 'c4', name: 'Linus Torvalds', email: 'linus@kernel.org', phone: '+1 (503) 555-0104', country: 'United States', accountNo: '3312-9087', plan: 'pro', signupAt: '2026-03-08T08:45:00Z', health: 3, active: true },
103
+ { id: 'c5', name: 'Barbara Liskov', email: 'barbara@sub.dev', phone: '+1 (617) 555-0105', country: 'United States', accountNo: '9001-4455', plan: 'free', signupAt: '2026-05-30T16:30:00Z', health: 2, active: false },
104
+ ],
105
+ agents: [
106
+ { id: 'a1', name: 'Sam Rivera', email: 'sam@support.io', team: 'technical', phone: '+1 (415) 555-0201', skills: ['TypeScript', 'AWS', 'GraphQL'], capacity: 85, csat: 5, calendarColor: '#6366f1', active: true },
107
+ { id: 'a2', name: 'Jamie Chen', email: 'jamie@support.io', team: 'billing', phone: '+1 (312) 555-0202', skills: ['SQL', 'Leadership'], capacity: 70, csat: 4, calendarColor: '#10b981', active: true },
108
+ { id: 'a3', name: 'Priya Patel', email: 'priya@support.io', team: 'onboarding', phone: '+1 (206) 555-0203', skills: ['Product', 'UX', 'Figma'], capacity: 60, csat: 4, calendarColor: '#f59e0b', active: true },
109
+ ],
110
+ tickets: [
111
+ { id: 'tk1', subject: 'Cannot reset password', customerId: 'c1', agentId: 'a1', priority: 'high', status: 'open', channel: 'email', tags: ['auth', 'login'], createdAt: '2026-07-10T08:30:00Z', firstResponseMins: 12, satisfaction: 4, estimateHrs: 2, hourlyRate: 90 },
112
+ { id: 'tk2', subject: 'Invoice discrepancy', customerId: 'c2', agentId: 'a2', priority: 'normal', status: 'pending', channel: 'email', tags: ['billing'], createdAt: '2026-07-08T11:05:00Z', firstResponseMins: 45, satisfaction: 3, estimateHrs: 1, hourlyRate: 75 },
113
+ { id: 'tk3', subject: 'API returns 500', customerId: 'c3', agentId: 'a1', priority: 'urgent', status: 'open', channel: 'chat', tags: ['api', 'outage'], createdAt: '2026-07-12T15:40:00Z', firstResponseMins: 4, satisfaction: 5, estimateHrs: 4, hourlyRate: 120 },
114
+ { id: 'tk4', subject: 'How do I export data?', customerId: 'c4', agentId: 'a3', priority: 'low', status: 'resolved', channel: 'chat', tags: ['export'], createdAt: '2026-07-02T09:20:00Z', firstResponseMins: 20, satisfaction: 5, estimateHrs: 1, hourlyRate: 60 },
115
+ { id: 'tk5', subject: 'Upgrade to Business', customerId: 'c5', agentId: 'a3', priority: 'normal', status: 'closed', channel: 'phone', tags: ['upgrade', 'billing'], createdAt: '2026-06-28T13:10:00Z', firstResponseMins: 8, satisfaction: 4, estimateHrs: 1, hourlyRate: 75 },
116
+ { id: 'tk6', subject: 'SSO configuration', customerId: 'c1', agentId: 'a1', priority: 'high', status: 'pending', channel: 'email', tags: ['sso', 'security'], createdAt: '2026-07-11T10:00:00Z', firstResponseMins: 60, satisfaction: 2, estimateHrs: 3, hourlyRate: 120 },
117
+ { id: 'tk7', subject: 'Double charge refund', customerId: 'c2', agentId: 'a2', priority: 'urgent', status: 'resolved', channel: 'phone', tags: ['refund', 'billing'], createdAt: '2026-07-05T16:25:00Z', firstResponseMins: 15, satisfaction: 4, estimateHrs: 2, hourlyRate: 90 },
118
+ { id: 'tk8', subject: 'Feature request: dark mode', customerId: 'c3', agentId: 'a3', priority: 'low', status: 'open', channel: 'chat', tags: ['feature'], createdAt: '2026-07-13T12:50:00Z', firstResponseMins: 90, satisfaction: 3, estimateHrs: 5, hourlyRate: 100 },
119
+ ],
120
+ }
121
+
122
+ export const support: SampleApp = {
123
+ id: 'support',
124
+ name: 'Support desk',
125
+ description: 'Tickets, customers and agents - an Outlook-style help desk behind a sign-in, with triage + SLA dashboards (CSAT sparkline + gauge, pivot, tabbed breakdown), a team-capacity dashboard, status-pill grids, master/detail, and rich edit forms.',
126
+ emoji: '\u{1F3AF}',
127
+ accent: '#0f6cbd',
128
+ build: () => {
129
+ const customerRows = pad(customers, seed.customers, 16)
130
+ const agentRows = pad(agents, seed.agents, 6)
131
+ const ticketRows = pad(tickets, seed.tickets, 36, { customerId: ids(customerRows), agentId: ids(agentRows) })
132
+ return project({
133
+ title: 'Support Desk',
134
+ brand: 'Support Desk',
135
+ accent: '#0f6cbd',
136
+ preset: 'atlassian',
137
+ footer: '',
138
+ // Outlook-style mail/triage client: blue command ribbon + a light folder rail.
139
+ appClass: 'theme-outlook',
140
+ customCss: OUTLOOK_CSS,
141
+ // Sign-in with two roles: agents work tickets but cannot delete records.
142
+ auth: { enabled: true, protect: true },
143
+ access: {
144
+ enabled: true,
145
+ defaultRole: 'agent',
146
+ roles: [
147
+ { role: 'admin', screens: '*', actions: '*' },
148
+ { role: 'agent', screens: '*', actions: ['create', 'update'] },
149
+ ],
150
+ },
151
+ entities: [customers, agents, tickets],
152
+ seed: { customers: customerRows, agents: agentRows, tickets: ticketRows },
153
+ screens: [
154
+ // Triage dashboard: volume + CSAT KPIs (CSAT card trends over created date),
155
+ // a CSAT gauge, status/priority/channel breakdowns, a tabbed view, and a
156
+ // ticket grid with status AND priority pills.
157
+ dashScreen(tickets, { id: 'overview', title: 'Overview', order: 0 }, [
158
+ { kpi: 'Open tickets', reduce: 'count', span: 1 },
159
+ { kpi: 'Avg first response', measure: 'firstResponseMins', reduce: 'avg', span: 1 },
160
+ { kpi: 'Avg CSAT', measure: 'satisfaction', reduce: 'avg', trendField: 'createdAt', trendReduce: 'avg', span: 1 },
161
+ { gauge: 'Avg CSAT', measure: 'satisfaction', reduce: 'avg', min: 0, max: 5, span: 1 },
162
+ { chart: 'status', reduce: 'count', type: 'bar', span: 2 },
163
+ { chart: 'priority', reduce: 'count', type: 'pie', span: 1 },
164
+ { chart: 'channel', reduce: 'count', type: 'bar', span: 2 },
165
+ { tabs: [
166
+ { label: 'By status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
167
+ { label: 'By priority', tiles: [{ chart: 'priority', reduce: 'count', type: 'bar' }] },
168
+ { label: 'By channel', tiles: [{ chart: 'channel', reduce: 'count', type: 'bar' }] },
169
+ ], span: 3 },
170
+ { grid: true, format: [...statusPills(tickets, 'status'), ...statusPills(tickets, 'priority')], summaries: true, span: 3 },
171
+ ]),
172
+ // SLA / quality: response + CSAT KPIs, a CSAT gauge, and a pivot of CSAT by
173
+ // priority (rows) x status (columns).
174
+ // Ticket board: tickets as cards in status columns (drag to change status).
175
+ boardScreen(tickets, { id: 'board', title: 'Board', order: 1 }, { groupBy: 'status', titleField: 'subject', subtitleField: 'priority', filter: ['priority', 'channel', 'agentId'] }),
176
+ // A docking console: filter | ticket queue | selected-ticket panel (drag / float / pin).
177
+ workspaceScreen(tickets, { id: 'console', title: 'Console', order: 2 }, { filter: ['priority', 'channel', 'agentId'], record: true, grid: { format: [...statusPills(tickets, 'status'), ...statusPills(tickets, 'priority')] } }),
178
+ // Customer 360: account header (plan pill + health tile) with a timeline of
179
+ // the customer's tickets (newest first, colored by status).
180
+ detailScreen(customers, { id: 'customer-360', title: 'Customer 360', order: 2 }, {
181
+ titleField: 'name', subtitleField: 'email', statusField: 'plan', metricFields: ['health'],
182
+ related: [{ entity: 'tickets', foreignKey: 'customerId', label: 'Tickets', titleField: 'subject', subtitleField: 'priority', dateField: 'createdAt', statusField: 'status' }],
183
+ }),
184
+ dashScreen(tickets, { id: 'performance', title: 'Performance', order: 3 }, [
185
+ { kpi: 'Avg first response', measure: 'firstResponseMins', reduce: 'avg', span: 1 },
186
+ { kpi: 'Avg CSAT', measure: 'satisfaction', reduce: 'avg', span: 1 },
187
+ { kpi: 'Est. hours', measure: 'estimateHrs', reduce: 'sum', format: 'number', span: 1 },
188
+ { gauge: 'Avg CSAT', measure: 'satisfaction', reduce: 'avg', min: 0, max: 5, span: 1 },
189
+ { chart: 'channel', measure: 'firstResponseMins', reduce: 'avg', type: 'bar', span: 2 },
190
+ { chart: 'priority', measure: 'satisfaction', reduce: 'avg', type: 'bar', span: 1 },
191
+ { pivot: { rows: ['priority'], cols: ['status'], measure: 'satisfaction', aggregate: 'avg' }, span: 3 },
192
+ ]),
193
+ // Team load: capacity gauge + a per-agent capacity leaderboard.
194
+ dashScreen(agents, { id: 'team', title: 'Team', order: 4 }, [
195
+ { kpi: 'Agents', reduce: 'count', span: 1 },
196
+ { kpi: 'Avg capacity', measure: 'capacity', reduce: 'avg', format: 'percent', span: 1 },
197
+ { kpi: 'Avg CSAT', measure: 'csat', reduce: 'avg', span: 1 },
198
+ { gauge: 'Avg team capacity', measure: 'capacity', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
199
+ { chart: 'name', measure: 'capacity', reduce: 'avg', type: 'bar', span: 2 },
200
+ { chart: 'team', reduce: 'count', type: 'pie', span: 1 },
201
+ { grid: true, format: statusPills(agents, 'team'), summaries: true, span: 3 },
202
+ ]),
203
+ formScreen(tickets, { id: 'tickets', title: 'Tickets', order: 5 }, undefined, { format: [...statusPills(tickets, 'status'), ...statusPills(tickets, 'priority')], summaries: true }),
204
+ screen(customers, 'master-detail', { id: 'customers', title: 'Customers', order: 6, child: tickets, foreignKey: 'customerId', linkScreen: 'customer-360' }),
205
+ formScreen(agents, { id: 'agents', title: 'Agents', order: 7 }),
206
+ ],
207
+ })
208
+ },
209
+ }