@svgrid/enterprise 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/svgrid-enterprise.svelte-external.js +26039 -805
- package/dist/designer/assets/index-Dp44bTid.js +939 -0
- package/dist/designer/assets/index-RJp6x8tw.css +1 -0
- package/dist/designer/assets/jszip.min-CjMo-QGg.js +2 -0
- package/dist/designer/index.html +13 -0
- package/dist/node/studio.js +22554 -0
- package/package.json +92 -83
- package/src/SvAuthGate.svelte +115 -0
- package/src/SvBoard.dom.test.ts +67 -0
- package/src/SvBoard.svelte +192 -0
- package/src/SvExportMenu.svelte +553 -0
- package/src/SvFileInput.svelte +113 -0
- package/src/SvGridEditPanel.dom.test.ts +146 -0
- package/src/SvGridEditPanel.svelte +793 -0
- package/src/SvGridMasterDetail.svelte +95 -0
- package/src/SvImportDialog.svelte +1020 -0
- package/src/SvLookupInput.svelte +180 -0
- package/src/SvRecordDetail.dom.test.ts +137 -0
- package/src/SvRecordDetail.svelte +288 -0
- package/src/SvSchedule.dom.test.ts +57 -0
- package/src/SvSchedule.svelte +156 -0
- package/src/SvSchemaChart.svelte +233 -0
- package/src/SvSchemaDashboard.svelte +130 -0
- package/src/ai-export-pdf.test.ts +74 -0
- package/src/ai-export-xlsx.test.ts +87 -0
- package/src/ai-export.test.ts +110 -0
- package/src/ai.ts +1188 -782
- package/src/edit-panel.test.ts +213 -0
- package/src/edit-panel.ts +228 -0
- package/src/export-conditional.test.ts +60 -0
- package/src/export-conditional.ts +94 -0
- package/src/export-ooxml.test.ts +152 -0
- package/src/export-ooxml.ts +485 -0
- package/src/export-pdf.test.ts +138 -0
- package/src/export-pdf.ts +245 -0
- package/src/export-print.test.ts +66 -0
- package/src/export-print.ts +132 -0
- package/src/export-serialize.test.ts +56 -0
- package/src/export-serialize.ts +24 -0
- package/src/export-xls.ts +196 -0
- package/src/export-xlsx-roundtrip.test.ts +120 -0
- package/src/export-xlsx.test.ts +150 -0
- package/src/export.test.ts +349 -0
- package/src/export.ts +1452 -549
- package/src/import-automap.test.ts +131 -0
- package/src/import-hardening.test.ts +158 -0
- package/src/import.ts +1042 -648
- package/src/index.ts +282 -0
- package/src/install.ts +134 -114
- package/src/license-core.test.ts +23 -0
- package/src/license-core.ts +35 -0
- package/src/license.ts +103 -90
- package/src/master-detail.test.ts +61 -0
- package/src/master-detail.ts +42 -0
- package/src/print.ts +107 -127
- package/src/schema-designer.test.ts +121 -0
- package/src/schema-designer.ts +142 -0
- package/src/schema.test.ts +268 -0
- package/src/schema.ts +499 -0
- package/src/smart-shim.ts +107 -105
- package/src/sources/aggregate.test.ts +79 -0
- package/src/sources/aggregate.ts +102 -0
- package/src/sources/auth-supabase.test.ts +80 -0
- package/src/sources/auth-supabase.ts +87 -0
- package/src/sources/dashboard.kpi.test.ts +50 -0
- package/src/sources/dashboard.test.ts +61 -0
- package/src/sources/dashboard.ts +136 -0
- package/src/sources/field-inference.test.ts +60 -0
- package/src/sources/field-inference.ts +63 -0
- package/src/sources/filters.test.ts +58 -0
- package/src/sources/filters.ts +64 -0
- package/src/sources/index.ts +67 -0
- package/src/sources/introspect-supabase.test.ts +100 -0
- package/src/sources/introspect-supabase.ts +119 -0
- package/src/sources/realtime-supabase.test.ts +93 -0
- package/src/sources/realtime-supabase.ts +99 -0
- package/src/sources/relation-lookup.test.ts +99 -0
- package/src/sources/relation-lookup.ts +128 -0
- package/src/sources/rest-adapters.test.ts +95 -0
- package/src/sources/rest-adapters.ts +120 -0
- package/src/sources/rest.test.ts +104 -0
- package/src/sources/rest.ts +129 -0
- package/src/sources/schema-from-columns.test.ts +106 -0
- package/src/sources/schema-from-columns.ts +88 -0
- package/src/sources/supabase.test.ts +110 -0
- package/src/sources/supabase.ts +99 -0
- package/src/sources/with-entity-rules.test.ts +104 -0
- package/src/sources/with-entity-rules.ts +78 -0
- package/src/sources/with-relation-labels.test.ts +75 -0
- package/src/sources/with-relation-labels.ts +73 -0
- package/src/studio/bug-report.test.ts +101 -0
- package/src/studio/bug-report.ts +165 -0
- package/src/studio/cli.test.ts +187 -0
- package/src/studio/cli.ts +112 -0
- package/src/studio/csv.test.ts +90 -0
- package/src/studio/csv.ts +165 -0
- package/src/studio/db-connect-string.test.ts +94 -0
- package/src/studio/db-connect-string.ts +128 -0
- package/src/studio/emit-project.test.ts +923 -0
- package/src/studio/emit-project.ts +1273 -0
- package/src/studio/emit-schema.test.ts +94 -0
- package/src/studio/emit-schema.ts +920 -0
- package/src/studio/index.ts +195 -0
- package/src/studio/introspect-db.test.ts +186 -0
- package/src/studio/introspect-db.ts +312 -0
- package/src/studio/introspect-prisma.test.ts +99 -0
- package/src/studio/introspect-prisma.ts +175 -0
- package/src/studio/introspect.test.ts +172 -0
- package/src/studio/introspect.ts +310 -0
- package/src/studio/pipeline.test.ts +42 -0
- package/src/studio/project-robust.test.ts +157 -0
- package/src/studio/project.test.ts +473 -0
- package/src/studio/project.ts +916 -0
- package/src/studio/sample-data.test.ts +58 -0
- package/src/studio/sample-data.ts +200 -0
- package/src/studio/samples/ats.ts +202 -0
- package/src/studio/samples/clinic.ts +177 -0
- package/src/studio/samples/crm.ts +250 -0
- package/src/studio/samples/ecommerce.ts +187 -0
- package/src/studio/samples/events.ts +199 -0
- package/src/studio/samples/fleet.ts +184 -0
- package/src/studio/samples/gym.ts +213 -0
- package/src/studio/samples/hr.ts +193 -0
- package/src/studio/samples/index.ts +55 -0
- package/src/studio/samples/insurance.ts +195 -0
- package/src/studio/samples/inventory.ts +182 -0
- package/src/studio/samples/invoicing.ts +162 -0
- package/src/studio/samples/library.ts +180 -0
- package/src/studio/samples/live-data.test.ts +98 -0
- package/src/studio/samples/live-data.ts +308 -0
- package/src/studio/samples/projects.ts +190 -0
- package/src/studio/samples/realestate.ts +196 -0
- package/src/studio/samples/restaurant.ts +187 -0
- package/src/studio/samples/samples.test.ts +208 -0
- package/src/studio/samples/school.ts +197 -0
- package/src/studio/samples/seed-floor.test.ts +25 -0
- package/src/studio/samples/shared.ts +305 -0
- package/src/studio/samples/subscriptions.ts +183 -0
- package/src/studio/samples/support.ts +182 -0
- package/src/studio/scaffold-app.test.ts +91 -0
- package/src/studio/scaffold-app.ts +161 -0
- package/src/studio/scaffold.test.ts +178 -0
- package/src/studio/scaffold.ts +374 -0
- package/src/studio/themes.ts +172 -0
- package/src/studio/verify.test.ts +47 -0
- package/src/studio/verify.ts +79 -0
- package/src/sveltekit/in-memory.test.ts +104 -0
- package/src/sveltekit/in-memory.ts +129 -0
- package/src/sveltekit/index.ts +23 -0
- package/src/sveltekit/query-plan.test.ts +109 -0
- package/src/sveltekit/query-plan.ts +125 -0
- package/src/sveltekit/sql-source.test.ts +185 -0
- package/src/sveltekit/sql-source.ts +166 -0
- package/src/sveltekit/sql.test.ts +80 -0
- package/src/sveltekit/sql.ts +131 -0
- package/src/sveltekit/transport.test.ts +175 -0
- package/src/sveltekit/transport.ts +254 -0
- package/src/sveltekit/types.ts +11 -0
- 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
|
+
}
|