@svgrid/enterprise 1.2.0 → 2.0.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 (159) hide show
  1. package/dist/cdn/svgrid-enterprise.svelte-external.js +26039 -805
  2. package/dist/designer/assets/index-Dp44bTid.js +939 -0
  3. package/dist/designer/assets/index-RJp6x8tw.css +1 -0
  4. package/dist/designer/assets/jszip.min-CjMo-QGg.js +2 -0
  5. package/dist/designer/index.html +13 -0
  6. package/dist/node/studio.js +22554 -0
  7. package/package.json +10 -3
  8. package/src/SvAuthGate.svelte +115 -0
  9. package/src/SvBoard.dom.test.ts +67 -0
  10. package/src/SvBoard.svelte +192 -0
  11. package/src/SvExportMenu.svelte +553 -0
  12. package/src/SvFileInput.svelte +113 -0
  13. package/src/SvGridEditPanel.dom.test.ts +146 -0
  14. package/src/SvGridEditPanel.svelte +793 -0
  15. package/src/SvGridMasterDetail.svelte +95 -0
  16. package/src/SvImportDialog.svelte +1020 -0
  17. package/src/SvLookupInput.svelte +180 -0
  18. package/src/SvRecordDetail.dom.test.ts +137 -0
  19. package/src/SvRecordDetail.svelte +288 -0
  20. package/src/SvSchedule.dom.test.ts +57 -0
  21. package/src/SvSchedule.svelte +156 -0
  22. package/src/SvSchemaChart.svelte +233 -0
  23. package/src/SvSchemaDashboard.svelte +130 -0
  24. package/src/ai-export-pdf.test.ts +74 -0
  25. package/src/ai-export-xlsx.test.ts +87 -0
  26. package/src/ai-export.test.ts +110 -0
  27. package/src/ai.ts +1188 -782
  28. package/src/edit-panel.test.ts +213 -0
  29. package/src/edit-panel.ts +228 -0
  30. package/src/export-conditional.test.ts +60 -0
  31. package/src/export-conditional.ts +94 -0
  32. package/src/export-ooxml.test.ts +152 -0
  33. package/src/export-ooxml.ts +485 -0
  34. package/src/export-pdf.test.ts +138 -0
  35. package/src/export-pdf.ts +245 -0
  36. package/src/export-print.test.ts +66 -0
  37. package/src/export-print.ts +132 -0
  38. package/src/export-serialize.test.ts +56 -0
  39. package/src/export-serialize.ts +24 -0
  40. package/src/export-xls.ts +196 -0
  41. package/src/export-xlsx-roundtrip.test.ts +120 -0
  42. package/src/export-xlsx.test.ts +150 -0
  43. package/src/export.test.ts +349 -0
  44. package/src/export.ts +1452 -549
  45. package/src/import-automap.test.ts +131 -0
  46. package/src/import-hardening.test.ts +158 -0
  47. package/src/import.ts +1042 -648
  48. package/src/index.ts +282 -0
  49. package/src/install.ts +134 -114
  50. package/src/license-core.test.ts +23 -0
  51. package/src/license-core.ts +35 -0
  52. package/src/license.ts +103 -90
  53. package/src/master-detail.test.ts +61 -0
  54. package/src/master-detail.ts +42 -0
  55. package/src/print.ts +107 -127
  56. package/src/schema-designer.test.ts +121 -0
  57. package/src/schema-designer.ts +142 -0
  58. package/src/schema.test.ts +268 -0
  59. package/src/schema.ts +499 -0
  60. package/src/smart-shim.ts +107 -105
  61. package/src/sources/aggregate.test.ts +79 -0
  62. package/src/sources/aggregate.ts +102 -0
  63. package/src/sources/auth-supabase.test.ts +80 -0
  64. package/src/sources/auth-supabase.ts +87 -0
  65. package/src/sources/dashboard.kpi.test.ts +50 -0
  66. package/src/sources/dashboard.test.ts +61 -0
  67. package/src/sources/dashboard.ts +136 -0
  68. package/src/sources/field-inference.test.ts +60 -0
  69. package/src/sources/field-inference.ts +63 -0
  70. package/src/sources/filters.test.ts +58 -0
  71. package/src/sources/filters.ts +64 -0
  72. package/src/sources/index.ts +67 -0
  73. package/src/sources/introspect-supabase.test.ts +100 -0
  74. package/src/sources/introspect-supabase.ts +119 -0
  75. package/src/sources/realtime-supabase.test.ts +93 -0
  76. package/src/sources/realtime-supabase.ts +99 -0
  77. package/src/sources/relation-lookup.test.ts +99 -0
  78. package/src/sources/relation-lookup.ts +128 -0
  79. package/src/sources/rest-adapters.test.ts +95 -0
  80. package/src/sources/rest-adapters.ts +120 -0
  81. package/src/sources/rest.test.ts +104 -0
  82. package/src/sources/rest.ts +129 -0
  83. package/src/sources/schema-from-columns.test.ts +106 -0
  84. package/src/sources/schema-from-columns.ts +88 -0
  85. package/src/sources/supabase.test.ts +110 -0
  86. package/src/sources/supabase.ts +99 -0
  87. package/src/sources/with-entity-rules.test.ts +104 -0
  88. package/src/sources/with-entity-rules.ts +78 -0
  89. package/src/sources/with-relation-labels.test.ts +75 -0
  90. package/src/sources/with-relation-labels.ts +73 -0
  91. package/src/studio/bug-report.test.ts +101 -0
  92. package/src/studio/bug-report.ts +165 -0
  93. package/src/studio/cli.test.ts +187 -0
  94. package/src/studio/cli.ts +112 -0
  95. package/src/studio/csv.test.ts +90 -0
  96. package/src/studio/csv.ts +165 -0
  97. package/src/studio/db-connect-string.test.ts +94 -0
  98. package/src/studio/db-connect-string.ts +128 -0
  99. package/src/studio/emit-project.test.ts +923 -0
  100. package/src/studio/emit-project.ts +1273 -0
  101. package/src/studio/emit-schema.test.ts +94 -0
  102. package/src/studio/emit-schema.ts +920 -0
  103. package/src/studio/index.ts +195 -0
  104. package/src/studio/introspect-db.test.ts +186 -0
  105. package/src/studio/introspect-db.ts +312 -0
  106. package/src/studio/introspect-prisma.test.ts +99 -0
  107. package/src/studio/introspect-prisma.ts +175 -0
  108. package/src/studio/introspect.test.ts +172 -0
  109. package/src/studio/introspect.ts +310 -0
  110. package/src/studio/pipeline.test.ts +42 -0
  111. package/src/studio/project-robust.test.ts +157 -0
  112. package/src/studio/project.test.ts +473 -0
  113. package/src/studio/project.ts +916 -0
  114. package/src/studio/sample-data.test.ts +58 -0
  115. package/src/studio/sample-data.ts +200 -0
  116. package/src/studio/samples/ats.ts +202 -0
  117. package/src/studio/samples/clinic.ts +177 -0
  118. package/src/studio/samples/crm.ts +250 -0
  119. package/src/studio/samples/ecommerce.ts +187 -0
  120. package/src/studio/samples/events.ts +199 -0
  121. package/src/studio/samples/fleet.ts +184 -0
  122. package/src/studio/samples/gym.ts +213 -0
  123. package/src/studio/samples/hr.ts +193 -0
  124. package/src/studio/samples/index.ts +55 -0
  125. package/src/studio/samples/insurance.ts +195 -0
  126. package/src/studio/samples/inventory.ts +182 -0
  127. package/src/studio/samples/invoicing.ts +162 -0
  128. package/src/studio/samples/library.ts +180 -0
  129. package/src/studio/samples/live-data.test.ts +98 -0
  130. package/src/studio/samples/live-data.ts +308 -0
  131. package/src/studio/samples/projects.ts +190 -0
  132. package/src/studio/samples/realestate.ts +196 -0
  133. package/src/studio/samples/restaurant.ts +187 -0
  134. package/src/studio/samples/samples.test.ts +208 -0
  135. package/src/studio/samples/school.ts +197 -0
  136. package/src/studio/samples/seed-floor.test.ts +25 -0
  137. package/src/studio/samples/shared.ts +305 -0
  138. package/src/studio/samples/subscriptions.ts +183 -0
  139. package/src/studio/samples/support.ts +182 -0
  140. package/src/studio/scaffold-app.test.ts +91 -0
  141. package/src/studio/scaffold-app.ts +161 -0
  142. package/src/studio/scaffold.test.ts +178 -0
  143. package/src/studio/scaffold.ts +374 -0
  144. package/src/studio/themes.ts +172 -0
  145. package/src/studio/verify.test.ts +47 -0
  146. package/src/studio/verify.ts +79 -0
  147. package/src/sveltekit/in-memory.test.ts +104 -0
  148. package/src/sveltekit/in-memory.ts +129 -0
  149. package/src/sveltekit/index.ts +23 -0
  150. package/src/sveltekit/query-plan.test.ts +109 -0
  151. package/src/sveltekit/query-plan.ts +125 -0
  152. package/src/sveltekit/sql-source.test.ts +185 -0
  153. package/src/sveltekit/sql-source.ts +166 -0
  154. package/src/sveltekit/sql.test.ts +80 -0
  155. package/src/sveltekit/sql.ts +131 -0
  156. package/src/sveltekit/transport.test.ts +175 -0
  157. package/src/sveltekit/transport.ts +254 -0
  158. package/src/sveltekit/types.ts +11 -0
  159. package/src/upgrade-prompt.ts +149 -148
@@ -0,0 +1,177 @@
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import type { FormatRule } from '../project.js'
3
+ import { screen, formScreen, calendarScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
+
5
+ const doctors: EntitySchema = {
6
+ name: 'doctors',
7
+ label: 'Doctor',
8
+ idField: 'id',
9
+ fields: [
10
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
11
+ { field: 'name', type: 'text', required: true, minLength: 2 },
12
+ { field: 'email', type: 'text', format: 'email', required: true },
13
+ { field: 'phone', type: 'text', input: { editorType: 'phone' } },
14
+ { field: 'specialty', type: 'enum', label: 'Specialty', required: true, options: [
15
+ { value: 'gp', label: 'General Practice', color: '#6366f1' },
16
+ { value: 'cardiology', label: 'Cardiology', color: '#ef4444' },
17
+ { value: 'dermatology', label: 'Dermatology', color: '#f59e0b' },
18
+ { value: 'pediatrics', label: 'Pediatrics', color: '#10b981' },
19
+ ] },
20
+ { field: 'rating', type: 'number', label: 'Patient rating', min: 1, max: 5, defaultValue: 4, input: { editorType: 'rating', help: '1-5 average patient rating' } },
21
+ { field: 'calendarColor', type: 'text', label: 'Calendar color', input: { editorType: 'color', help: 'Color used on the schedule' } },
22
+ ],
23
+ }
24
+
25
+ const patients: EntitySchema = {
26
+ name: 'patients',
27
+ label: 'Patient',
28
+ idField: 'id',
29
+ fields: [
30
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
31
+ { field: 'name', type: 'text', required: true, minLength: 2 },
32
+ { field: 'email', type: 'text', format: 'email', required: true },
33
+ { field: 'phone', type: 'text', input: { editorType: 'phone' } },
34
+ { field: 'dob', type: 'date', label: 'Date of birth', required: true },
35
+ { field: 'bloodType', type: 'enum', label: 'Blood type', options: [
36
+ { value: 'O+', label: 'O+', color: '#ef4444' },
37
+ { value: 'O-', label: 'O-', color: '#f97316' },
38
+ { value: 'A+', label: 'A+', color: '#6366f1' },
39
+ { value: 'A-', label: 'A-', color: '#8b5cf6' },
40
+ { value: 'B+', label: 'B+', color: '#10b981' },
41
+ { value: 'B-', label: 'B-', color: '#14b8a6' },
42
+ { value: 'AB+', label: 'AB+', color: '#f59e0b' },
43
+ { value: 'AB-', label: 'AB-', color: '#eab308' },
44
+ ] },
45
+ { field: 'insuranceNumber', type: 'text', label: 'Insurance #', input: { editorType: 'mask', mask: 'AAA-######', placeholder: 'AAA-000000' } },
46
+ { field: 'allergies', type: 'text', label: 'Allergies', input: { editorType: 'chips', help: 'Known allergies' } },
47
+ ],
48
+ }
49
+
50
+ const appointments: EntitySchema = {
51
+ name: 'appointments',
52
+ label: 'Appointment',
53
+ idField: 'id',
54
+ fields: [
55
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
56
+ { field: 'patientId', type: 'relation', label: 'Patient', required: true, relation: { entity: 'patients', foreignKey: 'patientId', labelField: 'name' } },
57
+ { field: 'doctorId', type: 'relation', label: 'Doctor', required: true, relation: { entity: 'doctors', foreignKey: 'doctorId', labelField: 'name' } },
58
+ { field: 'status', type: 'enum', label: 'Status', defaultValue: 'scheduled', options: [
59
+ { value: 'scheduled', label: 'Scheduled', color: '#6366f1' },
60
+ { value: 'completed', label: 'Completed', color: '#10b981' },
61
+ { value: 'cancelled', label: 'Cancelled', color: '#ef4444' },
62
+ { value: 'no_show', label: 'No show', color: '#94a3b8' },
63
+ ] },
64
+ { field: 'scheduledAt', type: 'datetime', label: 'Scheduled at', required: true },
65
+ { field: 'durationMins', type: 'number', label: 'Duration (min)', min: 15, max: 120, defaultValue: 30, input: { editorType: 'slider', step: 15, help: 'Visit length in minutes' } },
66
+ { field: 'fee', type: 'number', label: 'Fee ($)', min: 0 },
67
+ // Derived: fee plus 8% tax (display-only, never seeded).
68
+ { field: 'total', type: 'number', label: 'Total w/ tax ($)', readonly: true, formula: 'fee * 1.08' },
69
+ { field: 'satisfaction', type: 'number', label: 'Satisfaction', min: 1, max: 5, input: { editorType: 'rating', help: '1-5 visit rating' } },
70
+ ],
71
+ }
72
+
73
+ // Appointment grid formatting: status pills plus numeric thresholds (low
74
+ // satisfaction red, perfect scores green + bold, high-value fees bold) - data-driven
75
+ // color, not just status pills.
76
+ const apptFormats: FormatRule[] = [
77
+ ...statusPills(appointments, 'status'),
78
+ { field: 'satisfaction', op: 'lt', value: 3, color: '#dc2626' },
79
+ { field: 'satisfaction', op: 'gte', value: 5, color: '#16a34a', bold: true },
80
+ { field: 'fee', op: 'gte', value: 250, bold: true },
81
+ ]
82
+
83
+ const seed = {
84
+ doctors: [
85
+ { id: 'dr1', name: 'Dr. Sarah Mendes', email: 'sarah.mendes@healthclinic.com', phone: '+1 (415) 555-0301', specialty: 'gp', rating: 5, calendarColor: '#6366f1' },
86
+ { id: 'dr2', name: 'Dr. James Whitfield', email: 'james.whitfield@healthclinic.com', phone: '+1 (415) 555-0302', specialty: 'cardiology', rating: 4, calendarColor: '#ef4444' },
87
+ { id: 'dr3', name: 'Dr. Aisha Rahman', email: 'aisha.rahman@healthclinic.com', phone: '+1 (650) 555-0303', specialty: 'dermatology', rating: 5, calendarColor: '#f59e0b' },
88
+ { id: 'dr4', name: 'Dr. Marcus Liang', email: 'marcus.liang@healthclinic.com', phone: '+1 (212) 555-0304', specialty: 'pediatrics', rating: 4, calendarColor: '#10b981' },
89
+ { id: 'dr5', name: 'Dr. Elena Popova', email: 'elena.popova@healthclinic.com', phone: '+1 (312) 555-0305', specialty: 'gp', rating: 3, calendarColor: '#3b82f6' },
90
+ { id: 'dr6', name: 'Dr. Thomas Becker', email: 'thomas.becker@healthclinic.com', phone: '+1 (206) 555-0306', specialty: 'cardiology', rating: 4, calendarColor: '#8b5cf6' },
91
+ ],
92
+ patients: [
93
+ { id: 'pt1', name: 'Oliver Grant', email: 'oliver.grant@example.com', phone: '+1 (415) 555-0201', dob: '1984-03-12', bloodType: 'O+', insuranceNumber: 'AET-100482', allergies: ['Penicillin'] },
94
+ { id: 'pt2', name: 'Maya Fernandez', email: 'maya.fernandez@example.com', phone: '+1 (415) 555-0202', dob: '1991-11-04', bloodType: 'A-', insuranceNumber: 'BCX-204817', allergies: ['Peanuts', 'Latex'] },
95
+ { id: 'pt3', name: 'Noah Kim', email: 'noah.kim@example.com', phone: '+1 (650) 555-0203', dob: '2015-06-22', bloodType: 'B+', insuranceNumber: 'CDN-330298', allergies: [] },
96
+ { id: 'pt4', name: 'Sophia Rossi', email: 'sophia.rossi@example.com', phone: '+1 (212) 555-0204', dob: '1978-09-30', bloodType: 'AB+', insuranceNumber: 'DEF-419926', allergies: ['Aspirin'] },
97
+ { id: 'pt5', name: 'Ethan Walsh', email: 'ethan.walsh@example.com', phone: '+1 (312) 555-0205', dob: '2001-01-18', bloodType: 'O-', insuranceNumber: 'EGH-528140', allergies: ['Pollen', 'Dust'] },
98
+ { id: 'pt6', name: 'Amara Okafor', email: 'amara.okafor@example.com', phone: '+1 (206) 555-0206', dob: '1996-07-08', bloodType: 'A+', insuranceNumber: 'FIJ-610355', allergies: ['Shellfish'] },
99
+ ],
100
+ appointments: [
101
+ { id: 'ap1', patientId: 'pt1', doctorId: 'dr1', status: 'completed', scheduledAt: '2026-06-02T09:30:00Z', durationMins: 30, fee: 120, satisfaction: 5 },
102
+ { id: 'ap2', patientId: 'pt2', doctorId: 'dr2', status: 'scheduled', scheduledAt: '2026-07-18T14:00:00Z', durationMins: 45, fee: 260, satisfaction: 4 },
103
+ { id: 'ap3', patientId: 'pt3', doctorId: 'dr4', status: 'completed', scheduledAt: '2026-06-14T10:15:00Z', durationMins: 20, fee: 90, satisfaction: 5 },
104
+ { id: 'ap4', patientId: 'pt4', doctorId: 'dr3', status: 'cancelled', scheduledAt: '2026-05-27T11:00:00Z', durationMins: 30, fee: 150, satisfaction: 3 },
105
+ { id: 'ap5', patientId: 'pt5', doctorId: 'dr1', status: 'no_show', scheduledAt: '2026-06-09T15:30:00Z', durationMins: 30, fee: 120, satisfaction: 2 },
106
+ { id: 'ap6', patientId: 'pt6', doctorId: 'dr6', status: 'scheduled', scheduledAt: '2026-07-21T08:45:00Z', durationMins: 60, fee: 280, satisfaction: 4 },
107
+ { id: 'ap7', patientId: 'pt1', doctorId: 'dr5', status: 'completed', scheduledAt: '2026-06-30T13:00:00Z', durationMins: 25, fee: 110, satisfaction: 5 },
108
+ { id: 'ap8', patientId: 'pt4', doctorId: 'dr2', status: 'completed', scheduledAt: '2026-07-03T16:20:00Z', durationMins: 45, fee: 260, satisfaction: 4 },
109
+ ],
110
+ }
111
+
112
+ export const clinic: SampleApp = {
113
+ id: 'clinic',
114
+ name: 'Clinic',
115
+ description: 'Patients, doctors and appointments - visits, revenue and satisfaction dashboards (KPI sparkline + target, satisfaction gauge, status/specialty charts, a scheduled-at revenue trend, a pivot, and a tabbed breakdown), filtered status-pill grids with row actions, patient master/detail, and rich edit forms (phone, blood-type, insurance mask, allergy chips, duration slider, ratings).',
116
+ emoji: '\u{1FA7A}',
117
+ accent: '#ef4444',
118
+ build: () => {
119
+ const doctorRows = pad(doctors, seed.doctors, 18)
120
+ const patientRows = pad(patients, seed.patients, 60)
121
+ const appointmentRows = pad(appointments, seed.appointments, 90, { patientId: ids(patientRows), doctorId: ids(doctorRows) })
122
+ return project({
123
+ title: 'HealthClinic',
124
+ brand: 'HealthClinic',
125
+ accent: '#ef4444',
126
+ preset: 'material',
127
+ footer: '',
128
+ entities: [doctors, patients, appointments],
129
+ seed: { doctors: doctorRows, patients: patientRows, appointments: appointmentRows },
130
+ screens: [
131
+ // Appointments dashboard: visit + revenue KPIs (a revenue card that trends
132
+ // over scheduledAt with a $2k target), a satisfaction gauge (0-5), status
133
+ // revenue/volume charts, a scheduled-at revenue area trend, a fee-by-status
134
+ // pivot, a tabbed breakdown, a status filter, and a status-pill grid.
135
+ dashScreen(appointments, { id: 'overview', title: 'Overview', order: 0 }, [
136
+ { kpi: 'Appointments', reduce: 'count', span: 1 },
137
+ { kpi: 'Total revenue', measure: 'fee', reduce: 'sum', format: 'currency', trendField: 'scheduledAt', trendReduce: 'sum', target: 2000, span: 1 },
138
+ { kpi: 'Avg fee', measure: 'fee', reduce: 'avg', format: 'currency', span: 1 },
139
+ { gauge: 'Avg satisfaction', measure: 'satisfaction', reduce: 'avg', min: 0, max: 5, span: 1 },
140
+ { chart: 'status', measure: 'fee', reduce: 'sum', type: 'bar', span: 2 },
141
+ { chart: 'status', reduce: 'count', type: 'pie', span: 1 },
142
+ { chart: 'scheduledAt', measure: 'fee', reduce: 'sum', type: 'area', span: 3 },
143
+ { pivot: { rows: ['status'], cols: [], measure: 'fee', aggregate: 'sum' }, span: 3 },
144
+ { tabs: [
145
+ { label: 'Revenue by status', tiles: [{ chart: 'status', measure: 'fee', reduce: 'sum', type: 'bar' }] },
146
+ { label: 'Visits by status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
147
+ ], span: 3 },
148
+ { filter: ['status'], span: 3 },
149
+ { grid: true, format: statusPills(appointments, 'status'), summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
150
+ ]),
151
+ // Doctors dashboard: roster + rating KPIs, an average-rating gauge (0-5),
152
+ // specialty breakdowns (pie + avg-rating bar), a specialty filter, and a
153
+ // specialty-pill roster grid.
154
+ dashScreen(doctors, { id: 'specialties', title: 'Specialties', order: 1 }, [
155
+ { kpi: 'Doctors', reduce: 'count', span: 1 },
156
+ { kpi: 'Avg rating', measure: 'rating', reduce: 'avg', span: 1 },
157
+ { kpi: 'Top rating', measure: 'rating', reduce: 'max', span: 1 },
158
+ { gauge: 'Avg patient rating', measure: 'rating', reduce: 'avg', min: 0, max: 5, span: 1 },
159
+ { chart: 'specialty', reduce: 'count', type: 'pie', span: 1 },
160
+ { chart: 'specialty', measure: 'rating', reduce: 'avg', type: 'bar', span: 2 },
161
+ { filter: ['specialty'], span: 3 },
162
+ { grid: true, format: statusPills(doctors, 'specialty'), summaries: true, span: 3 },
163
+ ]),
164
+ calendarScreen(appointments, { id: 'schedule', title: 'Schedule', order: 2 }, { dateField: 'scheduledAt', titleField: 'patientId', colorField: 'status', filter: ['status', 'doctorId'] }),
165
+ // Patient 360: chart header (blood-type pill) with a visit-history timeline -
166
+ // each appointment titled by its doctor (relation resolved), colored by status.
167
+ detailScreen(patients, { id: 'patient-360', title: 'Patient 360', order: 3 }, {
168
+ titleField: 'name', subtitleField: 'email', statusField: 'bloodType',
169
+ related: [{ entity: 'appointments', foreignKey: 'patientId', label: 'Visits', titleField: 'doctorId', subtitleField: 'fee', dateField: 'scheduledAt', statusField: 'status' }],
170
+ }),
171
+ screen(patients, 'master-detail', { id: 'patients', title: 'Patients', order: 4, child: appointments, foreignKey: 'patientId', linkScreen: 'patient-360' }),
172
+ formScreen(appointments, { id: 'appointments', title: 'Appointments', order: 5 }, undefined, { format: apptFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['status']),
173
+ formScreen(doctors, { id: 'doctors', title: 'Doctors', order: 6 }, undefined, {}, ['specialty']),
174
+ ],
175
+ })
176
+ },
177
+ }
@@ -0,0 +1,250 @@
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import type { FormatRule } from '../project.js'
3
+ import { screen, formScreen, boardScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
+
5
+ // 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 360: a full record page - header (title + company + stage pill), value /
199
+ // probability metric tiles, an Overview of deal + account fields, and an
200
+ // Activities timeline of the tasks logged against the deal.
201
+ detailScreen(deals, { id: 'deal-360', title: 'Deal 360', order: 2 }, {
202
+ titleField: 'title', subtitleField: 'companyId', statusField: 'stage',
203
+ metricFields: ['value', 'probability'],
204
+ sections: [
205
+ { label: 'Deal', fields: ['owner', 'closeDate'] },
206
+ { label: 'Account', fields: ['companyId'] },
207
+ ],
208
+ related: [{ entity: 'activities', foreignKey: 'dealId', label: 'Activities', titleField: 'subject', subtitleField: 'owner', dateField: 'dueDate', statusField: 'priority' }],
209
+ }),
210
+ // Forecast: value KPIs, a value-over-time area trend, and a pivot of value by
211
+ // owner (rows) x stage (columns).
212
+ dashScreen(deals, { id: 'forecast', title: 'Forecast', order: 3 }, [
213
+ { kpi: 'Pipeline value', measure: 'value', reduce: 'sum', format: 'currency', span: 1 },
214
+ { kpi: 'Avg deal size', measure: 'value', reduce: 'avg', format: 'currency', span: 1 },
215
+ { kpi: 'Largest deal', measure: 'value', reduce: 'max', format: 'currency', span: 1 },
216
+ { chart: 'closeDate', measure: 'value', reduce: 'sum', type: 'area', span: 3 },
217
+ { pivot: { rows: ['owner'], cols: ['stage'], measure: 'value', aggregate: 'sum' }, span: 3 },
218
+ ]),
219
+ // Accounts dashboard: headcount KPIs (compact-formatted), an account-health
220
+ // gauge (avg rating 0-5), industry breakdowns, and a company grid that drills
221
+ // into the account's deals.
222
+ dashScreen(companies, { id: 'accounts', title: 'Accounts', order: 4 }, [
223
+ { kpi: 'Companies', reduce: 'count', span: 1 },
224
+ { kpi: 'Total headcount', measure: 'employees', reduce: 'sum', format: 'compact', span: 1 },
225
+ { kpi: 'Avg headcount', measure: 'employees', reduce: 'avg', span: 1 },
226
+ { gauge: 'Avg account health', measure: 'health', reduce: 'avg', min: 0, max: 5, span: 1 },
227
+ { chart: 'industry', measure: 'employees', reduce: 'sum', type: 'bar', span: 2 },
228
+ { chart: 'industry', reduce: 'count', type: 'pie', span: 1 },
229
+ { filter: ['industry', 'country', 'active'], span: 3 },
230
+ { grid: true, format: statusPills(companies, 'industry'), summaries: true, rowLink: { screen: 'deals', sourceField: 'id', targetField: 'companyId' }, span: 3 },
231
+ ]),
232
+ // Activity board: task KPIs, a duration gauge, type/owner breakdowns, a filter,
233
+ // and a grid with type + priority pills and inline edit actions.
234
+ dashScreen(activities, { id: 'activities', title: 'Activities', order: 5 }, [
235
+ { kpi: 'Activities', reduce: 'count', span: 1 },
236
+ { kpi: 'Logged minutes', measure: 'durationMins', reduce: 'sum', format: 'compact', span: 1 },
237
+ { kpi: 'Avg duration', measure: 'durationMins', reduce: 'avg', span: 1 },
238
+ { gauge: 'Avg duration (min)', measure: 'durationMins', reduce: 'avg', min: 0, max: 120, unit: 'm', span: 1 },
239
+ { chart: 'type', reduce: 'count', type: 'pie', span: 1 },
240
+ { chart: 'owner', reduce: 'count', type: 'bar', span: 2 },
241
+ { filter: ['type', 'priority', 'owner', 'completed'], span: 3 },
242
+ { grid: true, format: [...statusPills(activities, 'type'), ...statusPills(activities, 'priority'), ...statusPills(activities, 'owner')], summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
243
+ ]),
244
+ screen(companies, 'master-detail', { id: 'companies', title: 'Companies', order: 6, child: contacts, foreignKey: 'companyId' }),
245
+ formScreen(contacts, { id: 'contacts', title: 'Contacts', order: 7 }, undefined, {}, ['companyId', 'tags', 'active']),
246
+ formScreen(deals, { id: 'deals', title: 'Deals', order: 8 }, undefined, { format: dealFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'deal-360', sourceField: 'id', targetField: 'id' } }, ['stage', 'owner', 'companyId']),
247
+ ],
248
+ })
249
+ },
250
+ }