@svgrid/enterprise 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +95 -81
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +22468 -10459
  3. package/dist/node/studio.js +11590 -2907
  4. package/package.json +31 -10
  5. package/src/SvAlertRuleEditor.svelte +294 -0
  6. package/src/SvAlertsManager.svelte +210 -0
  7. package/src/SvAlertsPanel.svelte +129 -0
  8. package/src/SvBoard.svelte +3 -1
  9. package/src/SvExpressionEditor.svelte +341 -0
  10. package/src/SvGridAlerts.dom.test.ts +113 -0
  11. package/src/SvGridAlerts.svelte +265 -0
  12. package/src/SvGridBoard.svelte +2355 -0
  13. package/src/SvGridEditPanel.svelte +849 -794
  14. package/src/SvGridScheduler.svelte +5334 -4410
  15. package/src/SvPivotDesigner.svelte +3 -3
  16. package/src/SvRecordDetail.svelte +6 -2
  17. package/src/SvSchedule.svelte +3 -1
  18. package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +4 -1
  19. package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +4 -1
  20. package/src/{ai-export.test.ts → ai-export.dom.test.ts} +5 -1
  21. package/src/alerts/alert-engine-attach.ts +165 -0
  22. package/src/alerts/alert-engine.test.ts +135 -0
  23. package/src/alerts/alert-engine.ts +260 -0
  24. package/src/alerts/alert-formats.test.ts +87 -0
  25. package/src/alerts/alert-formats.ts +77 -0
  26. package/src/alerts/alert-observer.test.ts +189 -0
  27. package/src/alerts/alert-observer.ts +208 -0
  28. package/src/alerts/alert-scheduler.ts +96 -0
  29. package/src/alerts/alert-storage.test.ts +54 -0
  30. package/src/alerts/alert-storage.ts +116 -0
  31. package/src/alerts/alert-store.svelte.ts +80 -0
  32. package/src/alerts/alert-types.ts +94 -0
  33. package/src/alerts.ts +28 -0
  34. package/src/board.dom.test.ts +941 -0
  35. package/src/board.ts +36 -0
  36. package/src/export.ts +6 -0
  37. package/src/expressions/evaluate.test.ts +111 -0
  38. package/src/expressions/evaluate.ts +228 -0
  39. package/src/expressions/expression-columns.ts +133 -0
  40. package/src/expressions/expression-types.ts +84 -0
  41. package/src/expressions/parse.test.ts +106 -0
  42. package/src/expressions/parse.ts +610 -0
  43. package/src/import.ts +2 -0
  44. package/src/index.ts +180 -46
  45. package/src/install.ts +12 -2
  46. package/src/pivot-enable.ts +44 -0
  47. package/src/scheduler-assignments.test.ts +97 -0
  48. package/src/scheduler-assignments.ts +134 -0
  49. package/src/scheduler-axis.test.ts +108 -0
  50. package/src/scheduler-axis.ts +238 -0
  51. package/src/scheduler-booking.test.ts +57 -0
  52. package/src/scheduler-booking.ts +63 -0
  53. package/src/scheduler-config.ts +179 -0
  54. package/src/scheduler-dependencies.test.ts +155 -0
  55. package/src/scheduler-dependencies.ts +223 -0
  56. package/src/scheduler-freebusy.test.ts +41 -0
  57. package/src/scheduler-freebusy.ts +36 -0
  58. package/src/scheduler-heatmap.test.ts +39 -0
  59. package/src/scheduler-heatmap.ts +55 -0
  60. package/src/scheduler-resource-tree.test.ts +84 -0
  61. package/src/scheduler-resource-tree.ts +107 -0
  62. package/src/scheduler-slots.test.ts +53 -0
  63. package/src/scheduler-slots.ts +94 -0
  64. package/src/scheduler-summary.test.ts +47 -0
  65. package/src/scheduler-summary.ts +81 -0
  66. package/src/sources/index.ts +1 -1
  67. package/src/sources/introspect-supabase.test.ts +13 -1
  68. package/src/sources/introspect-supabase.ts +20 -0
  69. package/src/studio/copilot-core.test.ts +45 -0
  70. package/src/studio/copilot-core.ts +65 -0
  71. package/src/studio/deploy-cli.test.ts +56 -0
  72. package/src/studio/deploy-cli.ts +100 -0
  73. package/src/studio/emit-project.test.ts +503 -18
  74. package/src/studio/emit-project.ts +829 -111
  75. package/src/studio/emit-schema.test.ts +17 -0
  76. package/src/studio/emit-schema.ts +166 -35
  77. package/src/studio/index.ts +25 -1
  78. package/src/studio/introspect-openapi.test.ts +84 -0
  79. package/src/studio/introspect-openapi.ts +252 -0
  80. package/src/studio/project.test.ts +57 -0
  81. package/src/studio/project.ts +193 -6
  82. package/src/studio/samples/crm.ts +282 -258
  83. package/src/studio/samples/fleet.ts +213 -186
  84. package/src/studio/samples/insurance.ts +223 -195
  85. package/src/studio/samples/inventory.ts +213 -182
  86. package/src/studio/samples/live-data.test.ts +99 -98
  87. package/src/studio/samples/live-data.ts +8 -10
  88. package/src/studio/samples/projects.ts +212 -190
  89. package/src/studio/samples/samples.test.ts +52 -0
  90. package/src/studio/samples/shared.ts +26 -6
  91. package/src/studio/samples/starter.ts +251 -0
  92. package/src/studio/samples/support.ts +209 -184
  93. package/src/studio/ui-components-surface.test.ts +185 -0
  94. package/src/studio/ui-components.generated.ts +5617 -0
  95. package/src/studio/ui-components.ts +641 -472
  96. package/src/sveltekit/sql-source.test.ts +13 -0
  97. package/src/sveltekit/sql-source.ts +11 -6
  98. package/src/upgrade-prompt.ts +2 -2
  99. package/src/watermark.ts +2 -2
  100. package/src/ai.test.ts +0 -522
  101. package/src/ai.ts +0 -1388
@@ -1,258 +1,282 @@
1
- import type { EntitySchema } from '../../schema.js'
2
- import type { FormatRule } from '../project.js'
3
- import { screen, formScreen, boardScreen, schedulerScreen, workspaceScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
-
5
- // A fixed sales-rep pool, shared by deals + activities so leaderboards, pivots
6
- // and owner pills stay clean (padded rows cycle these instead of random names).
7
- const OWNERS = [
8
- { value: 'Sam Rivera', label: 'Sam Rivera', color: '#6366f1' },
9
- { value: 'Jamie Chen', label: 'Jamie Chen', color: '#10b981' },
10
- { value: 'Priya Patel', label: 'Priya Patel', color: '#f59e0b' },
11
- { value: 'Dana Lee', label: 'Dana Lee', color: '#ec4899' },
12
- ]
13
-
14
- const companies: EntitySchema = {
15
- name: 'companies',
16
- label: 'Company',
17
- idField: 'id',
18
- fields: [
19
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
20
- { field: 'name', type: 'text', required: true, minLength: 2 },
21
- { field: 'industry', type: 'enum', required: true, options: [
22
- { value: 'saas', label: 'SaaS', color: '#6366f1' },
23
- { value: 'fintech', label: 'Fintech', color: '#10b981' },
24
- { value: 'retail', label: 'Retail', color: '#f59e0b' },
25
- { value: 'health', label: 'Health', color: '#ef4444' },
26
- ] },
27
- { field: 'country', type: 'text', label: 'Country', input: { editorType: 'country', help: 'Headquarters country' } },
28
- { field: 'website', type: 'text', format: 'url', input: { placeholder: 'https://…' } },
29
- { field: 'employees', type: 'number', label: 'Employees', min: 0 },
30
- { field: 'health', type: 'number', label: 'Account health', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 relationship score' } },
31
- { field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
32
- ],
33
- }
34
-
35
- const contacts: EntitySchema = {
36
- name: 'contacts',
37
- label: 'Contact',
38
- idField: 'id',
39
- fields: [
40
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
41
- { field: 'name', type: 'text', required: true, minLength: 2 },
42
- { field: 'email', type: 'text', format: 'email', required: true },
43
- { field: 'phone', type: 'text', input: { editorType: 'phone' } },
44
- { field: 'title', type: 'text', label: 'Job title' },
45
- { field: 'companyId', type: 'relation', label: 'Company', relation: { entity: 'companies', foreignKey: 'companyId', labelField: 'name' } },
46
- { field: 'tags', type: 'text', label: 'Segments', input: { editorType: 'chips', help: 'Interests / segments' } },
47
- { field: 'active', type: 'boolean', defaultValue: true },
48
- ],
49
- }
50
-
51
- const deals: EntitySchema = {
52
- name: 'deals',
53
- label: 'Deal',
54
- idField: 'id',
55
- fields: [
56
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
57
- { field: 'title', type: 'text', required: true, minLength: 2 },
58
- { field: 'companyId', type: 'relation', label: 'Company', relation: { entity: 'companies', foreignKey: 'companyId', labelField: 'name' } },
59
- { field: 'stage', type: 'enum', required: true, options: [
60
- { value: 'lead', label: 'Lead', color: '#94a3b8' },
61
- { value: 'qualified', label: 'Qualified', color: '#6366f1' },
62
- { value: 'proposal', label: 'Proposal', color: '#f59e0b' },
63
- { value: 'won', label: 'Won', color: '#10b981' },
64
- { value: 'lost', label: 'Lost', color: '#ef4444' },
65
- ] },
66
- { field: 'value', type: 'number', label: 'Value ($)', min: 0 },
67
- { field: 'probability', type: 'number', label: 'Probability (%)', min: 0, max: 100, defaultValue: 50, input: { editorType: 'slider', help: 'Win likelihood' } },
68
- // Derived: expected value weighted by win probability (display-only).
69
- { field: 'weighted', type: 'number', label: 'Weighted ($)', readonly: true, formula: 'value * probability / 100' },
70
- { field: 'owner', type: 'enum', label: 'Owner', options: OWNERS },
71
- { field: 'closeDate', type: 'date', label: 'Close date' },
72
- ],
73
- }
74
-
75
- const activities: EntitySchema = {
76
- name: 'activities',
77
- label: 'Activity',
78
- idField: 'id',
79
- fields: [
80
- { field: 'id', type: 'text', primaryKey: true, readonly: true },
81
- { field: 'subject', type: 'text', required: true, minLength: 3 },
82
- { field: 'type', type: 'enum', required: true, options: [
83
- { value: 'call', label: 'Call', color: '#6366f1' },
84
- { value: 'email', label: 'Email', color: '#10b981' },
85
- { value: 'meeting', label: 'Meeting', color: '#f59e0b' },
86
- { value: 'task', label: 'Task', color: '#8b5cf6' },
87
- ] },
88
- { field: 'dealId', type: 'relation', label: 'Deal', relation: { entity: 'deals', foreignKey: 'dealId', labelField: 'title' } },
89
- { field: 'owner', type: 'enum', label: 'Owner', options: OWNERS },
90
- { field: 'priority', type: 'enum', defaultValue: 'medium', options: [
91
- { value: 'low', label: 'Low', color: '#94a3b8' },
92
- { value: 'medium', label: 'Medium', color: '#6366f1' },
93
- { value: 'high', label: 'High', color: '#ef4444' },
94
- ] },
95
- { field: 'dueDate', type: 'date', label: 'Due date' },
96
- { field: 'durationMins', type: 'number', label: 'Duration (min)', min: 0 },
97
- { field: 'completed', type: 'boolean', label: 'Done', defaultValue: false },
98
- { field: 'notes', type: 'text', label: 'Notes' },
99
- ],
100
- }
101
-
102
- // Deal grid formatting: stage + owner pills, plus numeric thresholds (weak deals
103
- // red, strong deals green + bold, big-ticket values bold) - data-driven color, not
104
- // just status pills.
105
- const dealFormats: FormatRule[] = [
106
- ...statusPills(deals, 'stage'),
107
- ...statusPills(deals, 'owner'),
108
- { field: 'probability', op: 'lt', value: 30, color: '#dc2626' },
109
- { field: 'probability', op: 'gte', value: 70, color: '#16a34a', bold: true },
110
- { field: 'value', op: 'gte', value: 100000, bold: true },
111
- ]
112
-
113
- const seed = {
114
- companies: [
115
- { id: 'co1', name: 'Globex', industry: 'saas', country: 'United States', website: 'https://globex.com', employees: 320, health: 4, active: true },
116
- { id: 'co2', name: 'Initech', industry: 'fintech', country: 'United Kingdom', website: 'https://initech.co', employees: 140, health: 3, active: true },
117
- { id: 'co3', name: 'Umbrella', industry: 'health', country: 'Germany', website: 'https://umbrella.de', employees: 900, health: 5, active: true },
118
- { id: 'co4', name: 'Hooli', industry: 'saas', country: 'United States', website: 'https://hooli.com', employees: 2100, health: 2, active: false },
119
- { id: 'co5', name: 'Soylent', industry: 'retail', country: 'Canada', website: 'https://soylent.ca', employees: 75, health: 3, active: true },
120
- { id: 'co6', name: 'Stark Industries', industry: 'fintech', country: 'United States', website: 'https://stark.com', employees: 5400, health: 5, active: true },
121
- ],
122
- contacts: [
123
- { id: 'ct1', name: 'Ada Lovelace', email: 'ada@globex.com', phone: '+1 (415) 555-0101', title: 'CTO', companyId: 'co1', tags: ['Champion', 'Technical'], active: true },
124
- { id: 'ct2', name: 'Alan Turing', email: 'alan@initech.co', phone: '+44 20 7946 0102', title: 'Head of Data', companyId: 'co2', tags: ['Economic buyer'], active: true },
125
- { id: 'ct3', name: 'Grace Hopper', email: 'grace@umbrella.de', phone: '+49 30 5550103', title: 'VP Engineering', companyId: 'co3', tags: ['Champion'], active: true },
126
- { id: 'ct4', name: 'Linus Torvalds', email: 'linus@hooli.com', phone: '+1 (650) 555-0104', title: 'Architect', companyId: 'co4', tags: ['Technical', 'Blocker'], active: false },
127
- { id: 'ct5', name: 'Barbara Liskov', email: 'barbara@soylent.ca', phone: '+1 (416) 555-0105', title: 'Founder', companyId: 'co5', tags: ['Decision maker'], active: true },
128
- { id: 'ct6', name: 'Katherine Johnson', email: 'kj@stark.com', phone: '+1 (212) 555-0106', title: 'Analyst', companyId: 'co6', tags: ['Influencer'], active: true },
129
- { id: 'ct7', name: 'Donald Knuth', email: 'don@globex.com', phone: '+1 (415) 555-0107', title: 'Advisor', companyId: 'co1', tags: ['Influencer', 'Technical'], active: true },
130
- { id: 'ct8', name: 'Margaret Hamilton', email: 'mh@stark.com', phone: '+1 (212) 555-0108', title: 'Director', companyId: 'co6', tags: ['Decision maker'], active: true },
131
- ],
132
- deals: [
133
- { id: 'dl1', title: 'Globex platform rollout', companyId: 'co1', stage: 'proposal', value: 48000, probability: 60, owner: 'Sam Rivera', closeDate: '2026-08-15' },
134
- { id: 'dl2', title: 'Initech data pipeline', companyId: 'co2', stage: 'won', value: 26000, probability: 100, owner: 'Jamie Chen', closeDate: '2026-06-30' },
135
- { id: 'dl3', title: 'Umbrella compliance suite', companyId: 'co3', stage: 'qualified', value: 91000, probability: 40, owner: 'Sam Rivera', closeDate: '2026-09-20' },
136
- { id: 'dl4', title: 'Hooli migration', companyId: 'co4', stage: 'lost', value: 15000, probability: 0, owner: 'Priya Patel', closeDate: '2026-05-10' },
137
- { id: 'dl5', title: 'Soylent storefront', companyId: 'co5', stage: 'lead', value: 8000, probability: 20, owner: 'Jamie Chen', closeDate: '2026-10-01' },
138
- { id: 'dl6', title: 'Stark analytics', companyId: 'co6', stage: 'proposal', value: 132000, probability: 65, owner: 'Priya Patel', closeDate: '2026-08-28' },
139
- { id: 'dl7', title: 'Globex add-on seats', companyId: 'co1', stage: 'won', value: 12000, probability: 100, owner: 'Sam Rivera', closeDate: '2026-07-05' },
140
- { id: 'dl8', title: 'Stark expansion', companyId: 'co6', stage: 'qualified', value: 54000, probability: 45, owner: 'Jamie Chen', closeDate: '2026-09-12' },
141
- ],
142
- activities: [
143
- { id: 'ac1', subject: 'Discovery call with Ada', type: 'call', dealId: 'dl1', owner: 'Sam Rivera', priority: 'high', dueDate: '2026-07-16', durationMins: 30, completed: true, notes: 'Mapped the rollout scope.' },
144
- { id: 'ac2', subject: 'Send pricing proposal', type: 'email', dealId: 'dl1', owner: 'Sam Rivera', priority: 'high', dueDate: '2026-07-18', durationMins: 15, completed: false, notes: 'Awaiting legal sign-off.' },
145
- { id: 'ac3', subject: 'Data pipeline demo', type: 'meeting', dealId: 'dl2', owner: 'Jamie Chen', priority: 'medium', dueDate: '2026-07-12', durationMins: 45, completed: true, notes: 'Great fit, moving to close.' },
146
- { id: 'ac4', subject: 'Compliance review follow-up', type: 'task', dealId: 'dl3', owner: 'Sam Rivera', priority: 'medium', dueDate: '2026-07-20', durationMins: 20, completed: false, notes: 'Collect SOC2 docs.' },
147
- { id: 'ac5', subject: 'Re-engage Hooli champion', type: 'call', dealId: 'dl4', owner: 'Priya Patel', priority: 'low', dueDate: '2026-07-22', durationMins: 25, completed: false, notes: 'Deal went cold - revisit Q4.' },
148
- { id: 'ac6', subject: 'Storefront scoping', type: 'meeting', dealId: 'dl5', owner: 'Jamie Chen', priority: 'medium', dueDate: '2026-07-19', durationMins: 60, completed: false, notes: 'Needs design mockups.' },
149
- { id: 'ac7', subject: 'Analytics exec briefing', type: 'meeting', dealId: 'dl6', owner: 'Priya Patel', priority: 'high', dueDate: '2026-07-24', durationMins: 40, completed: false, notes: 'CFO joining the call.' },
150
- { id: 'ac8', subject: 'Renewal paperwork', type: 'task', dealId: 'dl7', owner: 'Sam Rivera', priority: 'low', dueDate: '2026-07-14', durationMins: 10, completed: true, notes: 'Signed and filed.' },
151
- { id: 'ac9', subject: 'Expansion seats quote', type: 'email', dealId: 'dl8', owner: 'Jamie Chen', priority: 'medium', dueDate: '2026-07-21', durationMins: 15, completed: false, notes: 'Quote 40 extra seats.' },
152
- { id: 'ac10', subject: 'Quarterly business review', type: 'meeting', dealId: 'dl6', owner: 'Priya Patel', priority: 'high', dueDate: '2026-07-28', durationMins: 90, completed: false, notes: 'Prep the QBR deck.' },
153
- ],
154
- }
155
-
156
- export const crm: SampleApp = {
157
- id: 'crm',
158
- name: 'CRM',
159
- description: 'Companies, contacts, deals and activities - sales + forecast + activity dashboards (KPI sparklines, target gauge, value-over-time trend, pivot, tabs), filtered status-pill grids with drill-through and row actions, master/detail, and rich edit forms.',
160
- emoji: '\u{1F91D}',
161
- accent: '#6366f1',
162
- build: () => {
163
- const companyRows = pad(companies, seed.companies, 12)
164
- const pool = ids(companyRows)
165
- const contactRows = pad(contacts, seed.contacts, 22, { companyId: pool })
166
- const dealRows = pad(deals, seed.deals, 30, { companyId: pool })
167
- const dealPool = ids(dealRows)
168
- const activityRows = pad(activities, seed.activities, 24, { dealId: dealPool })
169
- return project({
170
- title: 'Acme CRM',
171
- brand: 'Acme CRM',
172
- accent: '#6366f1',
173
- preset: 'salesforce',
174
- footer: Acme Inc.',
175
- entities: [companies, contacts, deals, activities],
176
- seed: { companies: companyRows, contacts: contactRows, deals: dealRows, activities: activityRows },
177
- screens: [
178
- // Sales dashboard: KPI cards (a pipeline-value card with a sparkline over
179
- // close dates + a $400k target), a target gauge, a stage funnel, a pie, an
180
- // owner-leaderboard, a tabbed breakdown, and a status-pill deal grid.
181
- dashScreen(deals, { id: 'overview', title: 'Overview', order: 0 }, [
182
- { kpi: 'Pipeline value', measure: 'value', reduce: 'sum', format: 'currency', trendField: 'closeDate', trendReduce: 'sum', target: 400000, span: 1 },
183
- { kpi: 'Open deals', reduce: 'count', span: 1 },
184
- { kpi: 'Avg deal size', measure: 'value', reduce: 'avg', format: 'currency', span: 1 },
185
- { gauge: 'Pipeline vs $500k target', measure: 'value', reduce: 'sum', min: 0, max: 500000, unit: '$', span: 1 },
186
- { chart: 'stage', measure: 'value', reduce: 'sum', type: 'bar', span: 2 },
187
- { chart: 'stage', reduce: 'count', type: 'pie', span: 1 },
188
- { chart: 'owner', measure: 'value', reduce: 'sum', type: 'bar', span: 2 },
189
- { tabs: [
190
- { label: 'Value by stage', tiles: [{ chart: 'stage', measure: 'value', reduce: 'sum', type: 'bar' }] },
191
- { label: 'Deals by owner', tiles: [{ chart: 'owner', reduce: 'count', type: 'bar' }] },
192
- ], span: 3 },
193
- { grid: true, format: dealFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
194
- ]),
195
- // Pipeline board: deals as draggable cards in stage columns (drag a card to
196
- // change its stage). The signature "real app" view.
197
- boardScreen(deals, { id: 'pipeline', title: 'Pipeline', order: 1 }, { groupBy: 'stage', titleField: 'title', badgeField: 'value', subtitleField: 'owner', filter: ['owner', 'companyId'], openScreen: 'deal-360' }),
198
- // Deal desk: a docking-manager console (split panes) - filter deals on the
199
- // left, the pipeline grid centre, and the selected deal's record on the right.
200
- // Resize the panes; click a row to load it. Showcases screen layout: 'split'.
201
- workspaceScreen(deals, { id: 'deal-desk', title: 'Deal desk', order: 2 }, { filter: ['stage', 'owner'], mode: 'split' }),
202
- // Deal 360: a full record page - header (title + company + stage pill), value /
203
- // probability metric tiles, an Overview of deal + account fields, and an
204
- // Activities timeline of the tasks logged against the deal.
205
- detailScreen(deals, { id: 'deal-360', title: 'Deal 360', order: 3 }, {
206
- titleField: 'title', subtitleField: 'companyId', statusField: 'stage',
207
- metricFields: ['value', 'probability'],
208
- sections: [
209
- { label: 'Deal', fields: ['owner', 'closeDate'] },
210
- { label: 'Account', fields: ['companyId'] },
211
- ],
212
- related: [{ entity: 'activities', foreignKey: 'dealId', label: 'Activities', titleField: 'subject', subtitleField: 'owner', dateField: 'dueDate', statusField: 'priority' }],
213
- }),
214
- // Forecast: value KPIs, a value-over-time area trend, and a pivot of value by
215
- // owner (rows) x stage (columns).
216
- dashScreen(deals, { id: 'forecast', title: 'Forecast', order: 4 }, [
217
- { kpi: 'Pipeline value', measure: 'value', reduce: 'sum', format: 'currency', span: 1 },
218
- { kpi: 'Avg deal size', measure: 'value', reduce: 'avg', format: 'currency', span: 1 },
219
- { kpi: 'Largest deal', measure: 'value', reduce: 'max', format: 'currency', span: 1 },
220
- { chart: 'closeDate', measure: 'value', reduce: 'sum', type: 'area', span: 3 },
221
- { pivot: { rows: ['owner'], cols: ['stage'], measure: 'value', aggregate: 'sum' }, span: 3 },
222
- ]),
223
- // Accounts dashboard: headcount KPIs (compact-formatted), an account-health
224
- // gauge (avg rating 0-5), industry breakdowns, and a company grid that drills
225
- // into the account's deals.
226
- dashScreen(companies, { id: 'accounts', title: 'Accounts', order: 5 }, [
227
- { kpi: 'Companies', reduce: 'count', span: 1 },
228
- { kpi: 'Total headcount', measure: 'employees', reduce: 'sum', format: 'compact', span: 1 },
229
- { kpi: 'Avg headcount', measure: 'employees', reduce: 'avg', span: 1 },
230
- { gauge: 'Avg account health', measure: 'health', reduce: 'avg', min: 0, max: 5, span: 1 },
231
- { chart: 'industry', measure: 'employees', reduce: 'sum', type: 'bar', span: 2 },
232
- { chart: 'industry', reduce: 'count', type: 'pie', span: 1 },
233
- { filter: ['industry', 'country', 'active'], span: 3 },
234
- { grid: true, format: statusPills(companies, 'industry'), summaries: true, rowLink: { screen: 'deals', sourceField: 'id', targetField: 'companyId' }, span: 3 },
235
- ]),
236
- // Activity board: task KPIs, a duration gauge, type/owner breakdowns, a filter,
237
- // and a grid with type + priority pills and inline edit actions.
238
- dashScreen(activities, { id: 'activities', title: 'Activities', order: 6 }, [
239
- { kpi: 'Activities', reduce: 'count', span: 1 },
240
- { kpi: 'Logged minutes', measure: 'durationMins', reduce: 'sum', format: 'compact', span: 1 },
241
- { kpi: 'Avg duration', measure: 'durationMins', reduce: 'avg', span: 1 },
242
- { gauge: 'Avg duration (min)', measure: 'durationMins', reduce: 'avg', min: 0, max: 120, unit: 'm', span: 1 },
243
- { chart: 'type', reduce: 'count', type: 'pie', span: 1 },
244
- { chart: 'owner', reduce: 'count', type: 'bar', span: 2 },
245
- { filter: ['type', 'priority', 'owner', 'completed'], span: 3 },
246
- { grid: true, format: [...statusPills(activities, 'type'), ...statusPills(activities, 'priority'), ...statusPills(activities, 'owner')], summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
247
- ]),
248
- // Activity schedule: the activities grid rendered as a resource scheduler -
249
- // a column per owner (rep), tasks placed by due date and tinted by type, with
250
- // drag-to-reschedule + a detail drawer. Showcases the grid's scheduler view.
251
- schedulerScreen(activities, { id: 'activity-cal', title: 'Schedule', order: 7 }, { startField: 'dueDate', titleField: 'subject', colorField: 'type', resourceField: 'owner', initialView: 'week' }),
252
- screen(companies, 'master-detail', { id: 'companies', title: 'Companies', order: 8, child: contacts, foreignKey: 'companyId' }),
253
- formScreen(contacts, { id: 'contacts', title: 'Contacts', order: 9 }, undefined, {}, ['companyId', 'tags', 'active']),
254
- formScreen(deals, { id: 'deals', title: 'Deals', order: 10 }, undefined, { format: dealFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'deal-360', sourceField: 'id', targetField: 'id' } }, ['stage', 'owner', 'companyId']),
255
- ],
256
- })
257
- },
258
- }
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import type { FormatRule } from '../project.js'
3
+ import { screen, formScreen, boardScreen, schedulerScreen, workspaceScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
+
5
+ // Salesforce Lightning-style chrome: a gradient blue brand rail, white rounded-pill
6
+ // nav with a solid-white active state, and a Lightning-blue rule under the toolbar.
7
+ const LIGHTNING_CSS = `.sv-app.theme-lightning .sv-app__side { background: linear-gradient(180deg, #032d60, #0176d3); border-right: 0; }
8
+ .sv-app.theme-lightning .sv-app__brandtext { color: #fff; font-weight: 800; letter-spacing: -0.01em; }
9
+ .sv-app.theme-lightning .sv-app__link { color: #d9edff; font-weight: 600; border-radius: 9999px; padding: 8px 14px; }
10
+ .sv-app.theme-lightning .sv-app__link:hover { background: rgba(255, 255, 255, 0.14); color: #fff; }
11
+ .sv-app.theme-lightning .sv-app__link.is-active { background: #fff; color: #032d60; }
12
+ .sv-app.theme-lightning .sv-app__collapse, .sv-app.theme-lightning .sv-app__foot { color: rgba(255, 255, 255, 0.72); }
13
+ .sv-app.theme-lightning .sv-app__toolbar { border-bottom: 2px solid #0176d3; }`
14
+
15
+ // A fixed sales-rep pool, shared by deals + activities so leaderboards, pivots
16
+ // and owner pills stay clean (padded rows cycle these instead of random names).
17
+ const OWNERS = [
18
+ { value: 'Sam Rivera', label: 'Sam Rivera', color: '#6366f1' },
19
+ { value: 'Jamie Chen', label: 'Jamie Chen', color: '#10b981' },
20
+ { value: 'Priya Patel', label: 'Priya Patel', color: '#f59e0b' },
21
+ { value: 'Dana Lee', label: 'Dana Lee', color: '#ec4899' },
22
+ ]
23
+
24
+ const companies: EntitySchema = {
25
+ name: 'companies',
26
+ label: 'Company',
27
+ idField: 'id',
28
+ fields: [
29
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
30
+ { field: 'name', type: 'text', required: true, minLength: 2 },
31
+ { field: 'industry', type: 'enum', required: true, options: [
32
+ { value: 'saas', label: 'SaaS', color: '#6366f1' },
33
+ { value: 'fintech', label: 'Fintech', color: '#10b981' },
34
+ { value: 'retail', label: 'Retail', color: '#f59e0b' },
35
+ { value: 'health', label: 'Health', color: '#ef4444' },
36
+ ] },
37
+ { field: 'country', type: 'text', label: 'Country', input: { editorType: 'country', help: 'Headquarters country' } },
38
+ { field: 'website', type: 'text', format: 'url', input: { placeholder: 'https://…' } },
39
+ { field: 'employees', type: 'number', label: 'Employees', min: 0 },
40
+ { field: 'health', type: 'number', label: 'Account health', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 relationship score' } },
41
+ { field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
42
+ ],
43
+ }
44
+
45
+ const contacts: EntitySchema = {
46
+ name: 'contacts',
47
+ label: 'Contact',
48
+ idField: 'id',
49
+ fields: [
50
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
51
+ { field: 'name', type: 'text', required: true, minLength: 2 },
52
+ { field: 'email', type: 'text', format: 'email', required: true },
53
+ { field: 'phone', type: 'text', input: { editorType: 'phone' } },
54
+ { field: 'title', type: 'text', label: 'Job title' },
55
+ { field: 'companyId', type: 'relation', label: 'Company', relation: { entity: 'companies', foreignKey: 'companyId', labelField: 'name' } },
56
+ { field: 'tags', type: 'text', label: 'Segments', input: { editorType: 'chips', help: 'Interests / segments' } },
57
+ { field: 'active', type: 'boolean', defaultValue: true },
58
+ ],
59
+ }
60
+
61
+ const deals: EntitySchema = {
62
+ name: 'deals',
63
+ label: 'Deal',
64
+ idField: 'id',
65
+ fields: [
66
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
67
+ { field: 'title', type: 'text', required: true, minLength: 2 },
68
+ { field: 'companyId', type: 'relation', label: 'Company', relation: { entity: 'companies', foreignKey: 'companyId', labelField: 'name' } },
69
+ { field: 'stage', type: 'enum', required: true, options: [
70
+ { value: 'lead', label: 'Lead', color: '#94a3b8' },
71
+ { value: 'qualified', label: 'Qualified', color: '#6366f1' },
72
+ { value: 'proposal', label: 'Proposal', color: '#f59e0b' },
73
+ { value: 'won', label: 'Won', color: '#10b981' },
74
+ { value: 'lost', label: 'Lost', color: '#ef4444' },
75
+ ] },
76
+ { field: 'value', type: 'number', label: 'Value ($)', min: 0 },
77
+ { field: 'probability', type: 'number', label: 'Probability (%)', min: 0, max: 100, defaultValue: 50, input: { editorType: 'slider', help: 'Win likelihood' } },
78
+ // Derived: expected value weighted by win probability (display-only).
79
+ { field: 'weighted', type: 'number', label: 'Weighted ($)', readonly: true, formula: 'value * probability / 100' },
80
+ { field: 'owner', type: 'enum', label: 'Owner', options: OWNERS },
81
+ { field: 'closeDate', type: 'date', label: 'Close date' },
82
+ ],
83
+ }
84
+
85
+ const activities: EntitySchema = {
86
+ name: 'activities',
87
+ label: 'Activity',
88
+ idField: 'id',
89
+ fields: [
90
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
91
+ { field: 'subject', type: 'text', required: true, minLength: 3 },
92
+ { field: 'type', type: 'enum', required: true, options: [
93
+ { value: 'call', label: 'Call', color: '#6366f1' },
94
+ { value: 'email', label: 'Email', color: '#10b981' },
95
+ { value: 'meeting', label: 'Meeting', color: '#f59e0b' },
96
+ { value: 'task', label: 'Task', color: '#8b5cf6' },
97
+ ] },
98
+ { field: 'dealId', type: 'relation', label: 'Deal', relation: { entity: 'deals', foreignKey: 'dealId', labelField: 'title' } },
99
+ { field: 'owner', type: 'enum', label: 'Owner', options: OWNERS },
100
+ { field: 'priority', type: 'enum', defaultValue: 'medium', options: [
101
+ { value: 'low', label: 'Low', color: '#94a3b8' },
102
+ { value: 'medium', label: 'Medium', color: '#6366f1' },
103
+ { value: 'high', label: 'High', color: '#ef4444' },
104
+ ] },
105
+ { field: 'dueDate', type: 'date', label: 'Due date' },
106
+ { field: 'durationMins', type: 'number', label: 'Duration (min)', min: 0 },
107
+ { field: 'completed', type: 'boolean', label: 'Done', defaultValue: false },
108
+ { field: 'notes', type: 'text', label: 'Notes' },
109
+ ],
110
+ }
111
+
112
+ // Deal grid formatting: stage + owner pills, plus numeric thresholds (weak deals
113
+ // red, strong deals green + bold, big-ticket values bold) - data-driven color, not
114
+ // just status pills.
115
+ const dealFormats: FormatRule[] = [
116
+ ...statusPills(deals, 'stage'),
117
+ ...statusPills(deals, 'owner'),
118
+ { field: 'probability', op: 'lt', value: 30, color: '#dc2626' },
119
+ { field: 'probability', op: 'gte', value: 70, color: '#16a34a', bold: true },
120
+ { field: 'value', op: 'gte', value: 100000, bold: true },
121
+ ]
122
+
123
+ const seed = {
124
+ companies: [
125
+ { id: 'co1', name: 'Globex', industry: 'saas', country: 'United States', website: 'https://globex.com', employees: 320, health: 4, active: true },
126
+ { id: 'co2', name: 'Initech', industry: 'fintech', country: 'United Kingdom', website: 'https://initech.co', employees: 140, health: 3, active: true },
127
+ { id: 'co3', name: 'Umbrella', industry: 'health', country: 'Germany', website: 'https://umbrella.de', employees: 900, health: 5, active: true },
128
+ { id: 'co4', name: 'Hooli', industry: 'saas', country: 'United States', website: 'https://hooli.com', employees: 2100, health: 2, active: false },
129
+ { id: 'co5', name: 'Soylent', industry: 'retail', country: 'Canada', website: 'https://soylent.ca', employees: 75, health: 3, active: true },
130
+ { id: 'co6', name: 'Stark Industries', industry: 'fintech', country: 'United States', website: 'https://stark.com', employees: 5400, health: 5, active: true },
131
+ ],
132
+ contacts: [
133
+ { id: 'ct1', name: 'Ada Lovelace', email: 'ada@globex.com', phone: '+1 (415) 555-0101', title: 'CTO', companyId: 'co1', tags: ['Champion', 'Technical'], active: true },
134
+ { id: 'ct2', name: 'Alan Turing', email: 'alan@initech.co', phone: '+44 20 7946 0102', title: 'Head of Data', companyId: 'co2', tags: ['Economic buyer'], active: true },
135
+ { id: 'ct3', name: 'Grace Hopper', email: 'grace@umbrella.de', phone: '+49 30 5550103', title: 'VP Engineering', companyId: 'co3', tags: ['Champion'], active: true },
136
+ { id: 'ct4', name: 'Linus Torvalds', email: 'linus@hooli.com', phone: '+1 (650) 555-0104', title: 'Architect', companyId: 'co4', tags: ['Technical', 'Blocker'], active: false },
137
+ { id: 'ct5', name: 'Barbara Liskov', email: 'barbara@soylent.ca', phone: '+1 (416) 555-0105', title: 'Founder', companyId: 'co5', tags: ['Decision maker'], active: true },
138
+ { id: 'ct6', name: 'Katherine Johnson', email: 'kj@stark.com', phone: '+1 (212) 555-0106', title: 'Analyst', companyId: 'co6', tags: ['Influencer'], active: true },
139
+ { id: 'ct7', name: 'Donald Knuth', email: 'don@globex.com', phone: '+1 (415) 555-0107', title: 'Advisor', companyId: 'co1', tags: ['Influencer', 'Technical'], active: true },
140
+ { id: 'ct8', name: 'Margaret Hamilton', email: 'mh@stark.com', phone: '+1 (212) 555-0108', title: 'Director', companyId: 'co6', tags: ['Decision maker'], active: true },
141
+ ],
142
+ deals: [
143
+ { id: 'dl1', title: 'Globex platform rollout', companyId: 'co1', stage: 'proposal', value: 48000, probability: 60, owner: 'Sam Rivera', closeDate: '2026-08-15' },
144
+ { id: 'dl2', title: 'Initech data pipeline', companyId: 'co2', stage: 'won', value: 26000, probability: 100, owner: 'Jamie Chen', closeDate: '2026-06-30' },
145
+ { id: 'dl3', title: 'Umbrella compliance suite', companyId: 'co3', stage: 'qualified', value: 91000, probability: 40, owner: 'Sam Rivera', closeDate: '2026-09-20' },
146
+ { id: 'dl4', title: 'Hooli migration', companyId: 'co4', stage: 'lost', value: 15000, probability: 0, owner: 'Priya Patel', closeDate: '2026-05-10' },
147
+ { id: 'dl5', title: 'Soylent storefront', companyId: 'co5', stage: 'lead', value: 8000, probability: 20, owner: 'Jamie Chen', closeDate: '2026-10-01' },
148
+ { id: 'dl6', title: 'Stark analytics', companyId: 'co6', stage: 'proposal', value: 132000, probability: 65, owner: 'Priya Patel', closeDate: '2026-08-28' },
149
+ { id: 'dl7', title: 'Globex add-on seats', companyId: 'co1', stage: 'won', value: 12000, probability: 100, owner: 'Sam Rivera', closeDate: '2026-07-05' },
150
+ { id: 'dl8', title: 'Stark expansion', companyId: 'co6', stage: 'qualified', value: 54000, probability: 45, owner: 'Jamie Chen', closeDate: '2026-09-12' },
151
+ ],
152
+ activities: [
153
+ { id: 'ac1', subject: 'Discovery call with Ada', type: 'call', dealId: 'dl1', owner: 'Sam Rivera', priority: 'high', dueDate: '2026-07-16', durationMins: 30, completed: true, notes: 'Mapped the rollout scope.' },
154
+ { id: 'ac2', subject: 'Send pricing proposal', type: 'email', dealId: 'dl1', owner: 'Sam Rivera', priority: 'high', dueDate: '2026-07-18', durationMins: 15, completed: false, notes: 'Awaiting legal sign-off.' },
155
+ { id: 'ac3', subject: 'Data pipeline demo', type: 'meeting', dealId: 'dl2', owner: 'Jamie Chen', priority: 'medium', dueDate: '2026-07-12', durationMins: 45, completed: true, notes: 'Great fit, moving to close.' },
156
+ { id: 'ac4', subject: 'Compliance review follow-up', type: 'task', dealId: 'dl3', owner: 'Sam Rivera', priority: 'medium', dueDate: '2026-07-20', durationMins: 20, completed: false, notes: 'Collect SOC2 docs.' },
157
+ { id: 'ac5', subject: 'Re-engage Hooli champion', type: 'call', dealId: 'dl4', owner: 'Priya Patel', priority: 'low', dueDate: '2026-07-22', durationMins: 25, completed: false, notes: 'Deal went cold - revisit Q4.' },
158
+ { id: 'ac6', subject: 'Storefront scoping', type: 'meeting', dealId: 'dl5', owner: 'Jamie Chen', priority: 'medium', dueDate: '2026-07-19', durationMins: 60, completed: false, notes: 'Needs design mockups.' },
159
+ { id: 'ac7', subject: 'Analytics exec briefing', type: 'meeting', dealId: 'dl6', owner: 'Priya Patel', priority: 'high', dueDate: '2026-07-24', durationMins: 40, completed: false, notes: 'CFO joining the call.' },
160
+ { id: 'ac8', subject: 'Renewal paperwork', type: 'task', dealId: 'dl7', owner: 'Sam Rivera', priority: 'low', dueDate: '2026-07-14', durationMins: 10, completed: true, notes: 'Signed and filed.' },
161
+ { id: 'ac9', subject: 'Expansion seats quote', type: 'email', dealId: 'dl8', owner: 'Jamie Chen', priority: 'medium', dueDate: '2026-07-21', durationMins: 15, completed: false, notes: 'Quote 40 extra seats.' },
162
+ { id: 'ac10', subject: 'Quarterly business review', type: 'meeting', dealId: 'dl6', owner: 'Priya Patel', priority: 'high', dueDate: '2026-07-28', durationMins: 90, completed: false, notes: 'Prep the QBR deck.' },
163
+ ],
164
+ }
165
+
166
+ export const crm: SampleApp = {
167
+ id: 'crm',
168
+ name: 'CRM',
169
+ description: 'Companies, contacts, deals and activities - a Salesforce Lightning-style CRM behind a sign-in, with sales + forecast + activity dashboards (KPI sparklines, target gauge, value-over-time trend, pivot, tabs), filtered status-pill grids with drill-through and row actions, master/detail, and rich edit forms.',
170
+ emoji: '\u{1F91D}',
171
+ accent: '#0176d3',
172
+ build: () => {
173
+ const companyRows = pad(companies, seed.companies, 12)
174
+ const pool = ids(companyRows)
175
+ const contactRows = pad(contacts, seed.contacts, 22, { companyId: pool })
176
+ const dealRows = pad(deals, seed.deals, 30, { companyId: pool })
177
+ const dealPool = ids(dealRows)
178
+ const activityRows = pad(activities, seed.activities, 24, { dealId: dealPool })
179
+ return project({
180
+ title: 'Acme CRM',
181
+ brand: 'Acme CRM',
182
+ accent: '#0176d3',
183
+ preset: 'salesforce',
184
+ footer: '© Acme Inc.',
185
+ // Salesforce Lightning look: a deep-blue brand rail with white pill nav, and a
186
+ // Lightning-blue accent bar under the app toolbar.
187
+ appClass: 'theme-lightning',
188
+ customCss: LIGHTNING_CSS,
189
+ // A sign-in wall with two sales roles (reps can create + edit, not delete).
190
+ auth: { enabled: true, protect: true },
191
+ access: {
192
+ enabled: true,
193
+ defaultRole: 'rep',
194
+ roles: [
195
+ { role: 'admin', screens: '*', actions: '*' },
196
+ { role: 'rep', screens: '*', actions: ['create', 'update'] },
197
+ ],
198
+ },
199
+ entities: [companies, contacts, deals, activities],
200
+ seed: { companies: companyRows, contacts: contactRows, deals: dealRows, activities: activityRows },
201
+ screens: [
202
+ // Sales dashboard: KPI cards (a pipeline-value card with a sparkline over
203
+ // close dates + a $400k target), a target gauge, a stage funnel, a pie, an
204
+ // owner-leaderboard, a tabbed breakdown, and a status-pill deal grid.
205
+ dashScreen(deals, { id: 'overview', title: 'Overview', order: 0 }, [
206
+ { kpi: 'Pipeline value', measure: 'value', reduce: 'sum', format: 'currency', trendField: 'closeDate', trendReduce: 'sum', target: 400000, span: 1 },
207
+ { kpi: 'Open deals', reduce: 'count', span: 1 },
208
+ { kpi: 'Avg deal size', measure: 'value', reduce: 'avg', format: 'currency', span: 1 },
209
+ { gauge: 'Pipeline vs $500k target', measure: 'value', reduce: 'sum', min: 0, max: 500000, unit: '$', span: 1 },
210
+ { chart: 'stage', measure: 'value', reduce: 'sum', type: 'bar', span: 2 },
211
+ { chart: 'stage', reduce: 'count', type: 'pie', span: 1 },
212
+ { chart: 'owner', measure: 'value', reduce: 'sum', type: 'bar', span: 2 },
213
+ { tabs: [
214
+ { label: 'Value by stage', tiles: [{ chart: 'stage', measure: 'value', reduce: 'sum', type: 'bar' }] },
215
+ { label: 'Deals by owner', tiles: [{ chart: 'owner', reduce: 'count', type: 'bar' }] },
216
+ ], span: 3 },
217
+ { grid: true, format: dealFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
218
+ ]),
219
+ // Pipeline board: deals as draggable cards in stage columns (drag a card to
220
+ // change its stage). The signature "real app" view.
221
+ boardScreen(deals, { id: 'pipeline', title: 'Pipeline', order: 1 }, { groupBy: 'stage', titleField: 'title', badgeField: 'value', subtitleField: 'owner', filter: ['owner', 'companyId'], openScreen: 'deal-360' }),
222
+ // Deal desk: a docking-manager console (split panes) - filter deals on the
223
+ // left, the pipeline grid centre, and the selected deal's record on the right.
224
+ // Resize the panes; click a row to load it. Showcases screen layout: 'split'.
225
+ workspaceScreen(deals, { id: 'deal-desk', title: 'Deal desk', order: 2 }, { filter: ['stage', 'owner'], mode: 'split' }),
226
+ // Deal 360: a full record page - header (title + company + stage pill), value /
227
+ // probability metric tiles, an Overview of deal + account fields, and an
228
+ // Activities timeline of the tasks logged against the deal.
229
+ detailScreen(deals, { id: 'deal-360', title: 'Deal 360', order: 3 }, {
230
+ titleField: 'title', subtitleField: 'companyId', statusField: 'stage',
231
+ metricFields: ['value', 'probability'],
232
+ sections: [
233
+ { label: 'Deal', fields: ['owner', 'closeDate'] },
234
+ { label: 'Account', fields: ['companyId'] },
235
+ ],
236
+ related: [{ entity: 'activities', foreignKey: 'dealId', label: 'Activities', titleField: 'subject', subtitleField: 'owner', dateField: 'dueDate', statusField: 'priority' }],
237
+ }),
238
+ // Forecast: value KPIs, a value-over-time area trend, and a pivot of value by
239
+ // owner (rows) x stage (columns).
240
+ dashScreen(deals, { id: 'forecast', title: 'Forecast', order: 4 }, [
241
+ { kpi: 'Pipeline value', measure: 'value', reduce: 'sum', format: 'currency', span: 1 },
242
+ { kpi: 'Avg deal size', measure: 'value', reduce: 'avg', format: 'currency', span: 1 },
243
+ { kpi: 'Largest deal', measure: 'value', reduce: 'max', format: 'currency', span: 1 },
244
+ { chart: 'closeDate', measure: 'value', reduce: 'sum', type: 'area', span: 3 },
245
+ { pivot: { rows: ['owner'], cols: ['stage'], measure: 'value', aggregate: 'sum' }, span: 3 },
246
+ ]),
247
+ // Accounts dashboard: headcount KPIs (compact-formatted), an account-health
248
+ // gauge (avg rating 0-5), industry breakdowns, and a company grid that drills
249
+ // into the account's deals.
250
+ dashScreen(companies, { id: 'accounts', title: 'Accounts', order: 5 }, [
251
+ { kpi: 'Companies', reduce: 'count', span: 1 },
252
+ { kpi: 'Total headcount', measure: 'employees', reduce: 'sum', format: 'compact', span: 1 },
253
+ { kpi: 'Avg headcount', measure: 'employees', reduce: 'avg', span: 1 },
254
+ { gauge: 'Avg account health', measure: 'health', reduce: 'avg', min: 0, max: 5, span: 1 },
255
+ { chart: 'industry', measure: 'employees', reduce: 'sum', type: 'bar', span: 2 },
256
+ { chart: 'industry', reduce: 'count', type: 'pie', span: 1 },
257
+ { filter: ['industry', 'country', 'active'], span: 3 },
258
+ { grid: true, format: statusPills(companies, 'industry'), summaries: true, rowLink: { screen: 'deals', sourceField: 'id', targetField: 'companyId' }, span: 3 },
259
+ ]),
260
+ // Activity board: task KPIs, a duration gauge, type/owner breakdowns, a filter,
261
+ // and a grid with type + priority pills and inline edit actions.
262
+ dashScreen(activities, { id: 'activities', title: 'Activities', order: 6 }, [
263
+ { kpi: 'Activities', reduce: 'count', span: 1 },
264
+ { kpi: 'Logged minutes', measure: 'durationMins', reduce: 'sum', format: 'compact', span: 1 },
265
+ { kpi: 'Avg duration', measure: 'durationMins', reduce: 'avg', span: 1 },
266
+ { gauge: 'Avg duration (min)', measure: 'durationMins', reduce: 'avg', min: 0, max: 120, unit: 'm', span: 1 },
267
+ { chart: 'type', reduce: 'count', type: 'pie', span: 1 },
268
+ { chart: 'owner', reduce: 'count', type: 'bar', span: 2 },
269
+ { filter: ['type', 'priority', 'owner', 'completed'], span: 3 },
270
+ { grid: true, format: [...statusPills(activities, 'type'), ...statusPills(activities, 'priority'), ...statusPills(activities, 'owner')], summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
271
+ ]),
272
+ // Activity schedule: the activities grid rendered as a resource scheduler -
273
+ // a column per owner (rep), tasks placed by due date and tinted by type, with
274
+ // drag-to-reschedule + a detail drawer. Showcases the grid's scheduler view.
275
+ schedulerScreen(activities, { id: 'activity-cal', title: 'Schedule', order: 7 }, { startField: 'dueDate', titleField: 'subject', colorField: 'type', resourceField: 'owner', initialView: 'week' }),
276
+ screen(companies, 'master-detail', { id: 'companies', title: 'Companies', order: 8, child: contacts, foreignKey: 'companyId' }),
277
+ formScreen(contacts, { id: 'contacts', title: 'Contacts', order: 9 }, undefined, {}, ['companyId', 'tags', 'active']),
278
+ formScreen(deals, { id: 'deals', title: 'Deals', order: 10 }, undefined, { format: dealFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'deal-360', sourceField: 'id', targetField: 'id' } }, ['stage', 'owner', 'companyId']),
279
+ ],
280
+ })
281
+ },
282
+ }