@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,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
+ }