@svgrid/enterprise 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -81
- package/dist/cdn/svgrid-enterprise.svelte-external.js +26642 -11252
- package/dist/designer/assets/GridMenus-BtWVk9Ab.js +7 -0
- package/dist/designer/assets/SvGridChartPanel-DWlBO2SD.js +10 -0
- package/dist/designer/assets/SvGridChartView-8c8Mb4j8.js +1 -0
- package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +1 -0
- package/dist/designer/assets/index-DsDgp9Xq.js +78758 -0
- package/dist/designer/assets/index-tTY_Dx4P.css +1 -0
- package/dist/designer/assets/jszip.min-fkJdmAmj.js +2 -0
- package/dist/designer/assets/pdfmake-DeCsnyl9.js +242 -0
- package/dist/designer/assets/smart.export-BZlSCE8T.js +35 -0
- package/dist/designer/assets/vfs_fonts-eX2NpmfX.js +1 -0
- package/dist/designer/index.html +13 -0
- package/dist/node/studio.js +15304 -2708
- package/package.json +35 -11
- package/src/SvAlertRuleEditor.svelte +294 -0
- package/src/SvAlertsManager.svelte +210 -0
- package/src/SvAlertsPanel.svelte +129 -0
- package/src/SvBoard.svelte +3 -1
- package/src/SvExpressionEditor.svelte +341 -0
- package/src/SvGridAlerts.dom.test.ts +113 -0
- package/src/SvGridAlerts.svelte +265 -0
- package/src/SvGridBoard.svelte +2358 -0
- package/src/SvGridEditPanel.svelte +849 -794
- package/src/SvGridScheduler.svelte +5358 -4410
- package/src/SvPivotDesigner.svelte +3 -3
- package/src/SvRecordDetail.svelte +6 -2
- package/src/SvSchedule.svelte +3 -1
- package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +77 -74
- package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +90 -87
- package/src/{ai-export.test.ts → ai-export.dom.test.ts} +114 -110
- package/src/alerts/alert-engine-attach.ts +165 -0
- package/src/alerts/alert-engine.test.ts +135 -0
- package/src/alerts/alert-engine.ts +260 -0
- package/src/alerts/alert-formats.test.ts +87 -0
- package/src/alerts/alert-formats.ts +77 -0
- package/src/alerts/alert-observer.test.ts +189 -0
- package/src/alerts/alert-observer.ts +208 -0
- package/src/alerts/alert-scheduler.ts +96 -0
- package/src/alerts/alert-storage.test.ts +54 -0
- package/src/alerts/alert-storage.ts +116 -0
- package/src/alerts/alert-store.svelte.ts +80 -0
- package/src/alerts/alert-types.ts +94 -0
- package/src/alerts.ts +28 -0
- package/src/board.dom.test.ts +941 -0
- package/src/board.ts +36 -0
- package/src/export-ooxml.ts +4 -0
- package/src/export-xls.ts +3 -0
- package/src/export.ts +6 -0
- package/src/expressions/evaluate.test.ts +111 -0
- package/src/expressions/evaluate.ts +237 -0
- package/src/expressions/expression-columns.ts +133 -0
- package/src/expressions/expression-types.ts +84 -0
- package/src/expressions/parse.test.ts +106 -0
- package/src/expressions/parse.ts +610 -0
- package/src/import.test.ts +1 -1
- package/src/import.ts +3 -1
- package/src/index.ts +192 -46
- package/src/install.ts +12 -2
- package/src/pivot-enable.ts +44 -0
- package/src/pivot.test.ts +0 -1
- package/src/scheduler-assignments.test.ts +97 -0
- package/src/scheduler-assignments.ts +134 -0
- package/src/scheduler-axis.test.ts +108 -0
- package/src/scheduler-axis.ts +238 -0
- package/src/scheduler-booking.test.ts +57 -0
- package/src/scheduler-booking.ts +63 -0
- package/src/scheduler-config.ts +179 -0
- package/src/scheduler-dependencies.test.ts +155 -0
- package/src/scheduler-dependencies.ts +223 -0
- package/src/scheduler-freebusy.test.ts +41 -0
- package/src/scheduler-freebusy.ts +36 -0
- package/src/scheduler-heatmap.test.ts +39 -0
- package/src/scheduler-heatmap.ts +55 -0
- package/src/scheduler-resource-tree.test.ts +84 -0
- package/src/scheduler-resource-tree.ts +107 -0
- package/src/scheduler-slots.test.ts +53 -0
- package/src/scheduler-slots.ts +94 -0
- package/src/scheduler-summary.test.ts +47 -0
- package/src/scheduler-summary.ts +81 -0
- package/src/schema-designer.ts +1 -1
- package/src/sources/index.ts +1 -1
- package/src/sources/introspect-supabase.test.ts +13 -1
- package/src/sources/introspect-supabase.ts +20 -0
- package/src/studio/copilot-core.test.ts +45 -0
- package/src/studio/copilot-core.ts +65 -0
- package/src/studio/deploy-cli.test.ts +56 -0
- package/src/studio/deploy-cli.ts +100 -0
- package/src/studio/emit-project.test.ts +800 -24
- package/src/studio/emit-project.ts +1307 -135
- package/src/studio/emit-schema.test.ts +17 -0
- package/src/studio/emit-schema.ts +182 -41
- package/src/studio/index.ts +59 -1
- package/src/studio/init-flow.test.ts +239 -0
- package/src/studio/init-flow.ts +358 -0
- package/src/studio/introspect-openapi.test.ts +84 -0
- package/src/studio/introspect-openapi.ts +252 -0
- package/src/studio/project.test.ts +57 -1
- package/src/studio/project.ts +302 -8
- package/src/studio/samples/crm.ts +282 -258
- package/src/studio/samples/datasets.test.ts +84 -0
- package/src/studio/samples/datasets.ts +340 -0
- package/src/studio/samples/fleet.ts +213 -186
- package/src/studio/samples/insurance.ts +223 -195
- package/src/studio/samples/inventory.ts +213 -182
- package/src/studio/samples/live-data.test.ts +99 -98
- package/src/studio/samples/live-data.ts +8 -10
- package/src/studio/samples/projects.ts +212 -190
- package/src/studio/samples/samples.test.ts +268 -216
- package/src/studio/samples/shared.ts +22 -145
- package/src/studio/samples/starter.ts +251 -0
- package/src/studio/samples/support.ts +209 -184
- package/src/studio/screen-suites.test.ts +226 -0
- package/src/studio/screen-suites.ts +445 -0
- package/src/studio/ui-components-surface.test.ts +185 -0
- package/src/studio/ui-components.generated.ts +7154 -0
- package/src/studio/ui-components.ts +273 -3
- package/src/sveltekit/index.ts +1 -0
- package/src/sveltekit/sql-source.test.ts +13 -0
- package/src/sveltekit/sql-source.ts +12 -7
- package/src/sveltekit/transport-scope.test.ts +125 -0
- package/src/sveltekit/transport.ts +76 -2
- package/src/upgrade-prompt.ts +2 -2
- package/src/watermark.ts +2 -2
- package/src/ai.test.ts +0 -522
- package/src/ai.ts +0 -1388
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
* parse/serialize and generates a runnable app with no special-casing.
|
|
9
9
|
*/
|
|
10
10
|
import type { EntitySchema } from '../../schema.js'
|
|
11
|
-
import type { ChartType } from '@svgrid/grid'
|
|
12
11
|
import { generateValue } from '../sample-data.js'
|
|
13
|
-
import {
|
|
12
|
+
import { gridConfig, type GridOpts } from '../screen-suites.js'
|
|
13
|
+
import { sanitizeProject, screenFromTemplate, buildDockLayout, type Block, type GridConfig, type Screen, type ScreenTemplate, type StudioProject, type EntityDataSource, type ShellStyle, type SchedulerViewConfig, type SchedulerViewMode, type AuthConfig, type AccessControl } from '../project.js'
|
|
14
|
+
|
|
15
|
+
// The generic screen factories live in `../screen-suites.js` (they are public API
|
|
16
|
+
// now - the guided CRUD suite builds on them). Re-exported here so every sample
|
|
17
|
+
// app keeps importing its whole toolkit from one place.
|
|
18
|
+
export { dashScreen, detailScreen, formScreen, gridConfig, listScreen, statusPills, type GridOpts, type Tile } from '../screen-suites.js'
|
|
14
19
|
|
|
15
20
|
type Row = Record<string, unknown>
|
|
16
21
|
|
|
@@ -72,130 +77,6 @@ export type SampleApp = {
|
|
|
72
77
|
build: () => StudioProject
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
/** Rich-grid options an enterprise screen layers onto the default grid: status
|
|
76
|
-
* pills / thresholds (formatRules), a totals row, row actions, row-click drill,
|
|
77
|
-
* and density. */
|
|
78
|
-
export type GridOpts = {
|
|
79
|
-
format?: FormatRule[]
|
|
80
|
-
rowActions?: RowAction[]
|
|
81
|
-
rowLink?: RowLink
|
|
82
|
-
summaries?: boolean
|
|
83
|
-
density?: GridDensity
|
|
84
|
-
pageSize?: number
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** A dashboard tile: KPI card (with trend/target/format), chart, gauge, pivot,
|
|
88
|
-
* tree, a rich grid, or a tabbed group of display tiles. */
|
|
89
|
-
export type Tile =
|
|
90
|
-
| { kpi: string; measure?: string; reduce: Reduce; format?: KpiFormat; trendField?: string; trendReduce?: Reduce; target?: number; span?: 1 | 2 | 3 }
|
|
91
|
-
| { chart: string; measure?: string; reduce?: Reduce; type?: ChartType; span?: 1 | 2 | 3 }
|
|
92
|
-
| { gauge: string; measure?: string; reduce: Reduce; min?: number; max?: number; unit?: string; span?: 1 | 2 | 3 }
|
|
93
|
-
| { pivot: { rows: string[]; cols: string[]; measure?: string; aggregate?: Reduce }; span?: 1 | 2 | 3 }
|
|
94
|
-
| { tree: { labelField: string; parentField: string }; span?: 1 | 2 | 3 }
|
|
95
|
-
| ({ grid: true; span?: 1 | 2 | 3 } & GridOpts)
|
|
96
|
-
| { filter: string[]; span?: 1 | 2 | 3 }
|
|
97
|
-
| { tabs: { label: string; tiles: Tile[] }[]; span?: 1 | 2 | 3 }
|
|
98
|
-
|
|
99
|
-
/** The default grid config with enterprise options merged on. */
|
|
100
|
-
function gridConfig(entity: EntitySchema, opts: GridOpts): BlockConfig {
|
|
101
|
-
const base = defaultBlockConfig('grid', entity) as GridConfig
|
|
102
|
-
return {
|
|
103
|
-
...base,
|
|
104
|
-
...(opts.format ? { formatRules: opts.format } : {}),
|
|
105
|
-
...(opts.rowActions ? { rowActions: opts.rowActions } : {}),
|
|
106
|
-
...(opts.rowLink ? { rowLink: opts.rowLink } : {}),
|
|
107
|
-
...(opts.summaries ? { rowSummaries: true } : {}),
|
|
108
|
-
...(opts.density ? { density: opts.density } : {}),
|
|
109
|
-
...(opts.pageSize ? { pageSize: opts.pageSize } : {}),
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** One dashboard tile -> a Block. Recurses for `tabs` (whose children must be
|
|
114
|
-
* display blocks: chart / kpi / gauge / pivot / tree). */
|
|
115
|
-
function tileBlock(entity: EntitySchema, t: Tile, i: number): Block {
|
|
116
|
-
const id = `blk-${i + 1}`
|
|
117
|
-
if ('kpi' in t) {
|
|
118
|
-
const config: BlockConfig = {
|
|
119
|
-
kind: 'kpi', label: t.kpi, ...(t.measure ? { measure: t.measure } : {}), reduce: t.reduce,
|
|
120
|
-
...(t.format ? { format: t.format } : {}), ...(t.trendField ? { trendField: t.trendField } : {}),
|
|
121
|
-
...(t.trendReduce ? { trendReduce: t.trendReduce } : {}), ...(t.target != null ? { target: t.target } : {}),
|
|
122
|
-
}
|
|
123
|
-
return { id, span: t.span ?? 1, config }
|
|
124
|
-
}
|
|
125
|
-
if ('chart' in t) {
|
|
126
|
-
return { id, span: t.span ?? 2, config: { kind: 'chart', dimension: t.chart, ...(t.measure ? { measure: t.measure } : {}), reduce: t.reduce ?? 'sum', type: t.type ?? 'bar' } }
|
|
127
|
-
}
|
|
128
|
-
if ('gauge' in t) {
|
|
129
|
-
return { id, span: t.span ?? 1, config: { kind: 'gauge', label: t.gauge, ...(t.measure ? { measure: t.measure } : {}), reduce: t.reduce, min: t.min ?? 0, max: t.max ?? 100, ...(t.unit ? { unit: t.unit } : {}) } }
|
|
130
|
-
}
|
|
131
|
-
if ('pivot' in t) {
|
|
132
|
-
return { id, span: t.span ?? 3, config: { kind: 'pivot', rows: t.pivot.rows, cols: t.pivot.cols, ...(t.pivot.measure ? { measure: t.pivot.measure } : {}), aggregate: t.pivot.aggregate ?? 'sum' } }
|
|
133
|
-
}
|
|
134
|
-
if ('tree' in t) {
|
|
135
|
-
return { id, span: t.span ?? 1, config: { kind: 'tree', labelField: t.tree.labelField, parentField: t.tree.parentField } }
|
|
136
|
-
}
|
|
137
|
-
if ('filter' in t) {
|
|
138
|
-
return { id, span: t.span ?? 3, config: { kind: 'filter', fields: t.filter } }
|
|
139
|
-
}
|
|
140
|
-
if ('tabs' in t) {
|
|
141
|
-
return { id, span: t.span ?? 3, config: { kind: 'tabs', tabs: t.tabs.map((tab) => ({ label: tab.label, blocks: tab.tiles.map((tt, j) => tileBlock(entity, tt, j)) })) } }
|
|
142
|
-
}
|
|
143
|
-
return { id, span: t.span ?? 3, config: gridConfig(entity, t) }
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Compose a dashboard screen from explicit tiles (KPI cards + gauges + charts +
|
|
147
|
-
* pivots + trees + a rich grid). */
|
|
148
|
-
export function dashScreen(
|
|
149
|
-
entity: EntitySchema,
|
|
150
|
-
meta: { id: string; title: string; order: number },
|
|
151
|
-
tiles: Tile[],
|
|
152
|
-
): Screen {
|
|
153
|
-
const blocks = tiles.map((t, i) => tileBlock(entity, t, i))
|
|
154
|
-
return { id: meta.id, entity: entity.name, title: meta.title, route: meta.id, blocks, nav: { show: true, label: meta.title, order: meta.order } }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** FormatRule[] that color-code an enum field's cells by its option colors -
|
|
158
|
-
* turns a plain status column into enterprise status pills. */
|
|
159
|
-
export function statusPills(entity: EntitySchema, field: string): FormatRule[] {
|
|
160
|
-
const f = entity.fields.find((x) => x.field === field)
|
|
161
|
-
if (!f?.options) return []
|
|
162
|
-
return f.options
|
|
163
|
-
.filter((o) => o.color)
|
|
164
|
-
.map((o) => ({ field, op: 'eq' as const, value: o.value as string | number, background: `color-mix(in srgb, ${o.color} 18%, transparent)`, color: o.color, bold: true }))
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** A list screen: an optional faceted filter panel + a rich grid (status pills,
|
|
168
|
-
* totals, row actions, drill-through). */
|
|
169
|
-
export function listScreen(
|
|
170
|
-
entity: EntitySchema,
|
|
171
|
-
meta: { id: string; title: string; order: number },
|
|
172
|
-
opts: { filter?: string[]; grid?: GridOpts } = {},
|
|
173
|
-
): Screen {
|
|
174
|
-
const blocks: Block[] = []
|
|
175
|
-
if (opts.filter?.length) blocks.push({ id: 'filter-1', span: 3, config: { kind: 'filter', fields: opts.filter } })
|
|
176
|
-
blocks.push({ id: 'grid-1', span: 3, config: gridConfig(entity, opts.grid ?? {}) })
|
|
177
|
-
return { id: meta.id, entity: entity.name, title: meta.title, route: meta.id, blocks, nav: { show: true, label: meta.title, order: meta.order } }
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* A form-showcase screen: a grid to pick a row + an editable record panel that
|
|
182
|
-
* renders the full field form (phone / rating / tags / mask / slider editors)
|
|
183
|
-
* inline. Lets a user see and try the rich editors on load, not just on edit.
|
|
184
|
-
*/
|
|
185
|
-
export function formScreen(
|
|
186
|
-
entity: EntitySchema,
|
|
187
|
-
meta: { id: string; title: string; order: number },
|
|
188
|
-
fields?: string[],
|
|
189
|
-
grid?: GridOpts,
|
|
190
|
-
filter?: string[],
|
|
191
|
-
): Screen {
|
|
192
|
-
const blocks: Block[] = []
|
|
193
|
-
if (filter?.length) blocks.push({ id: 'filter-1', span: 3, config: { kind: 'filter', fields: filter } })
|
|
194
|
-
blocks.push({ id: 'grid-1', span: 3, config: gridConfig(entity, grid ?? {}) })
|
|
195
|
-
blocks.push({ id: 'record-1', span: 3, config: { kind: 'record', editable: true, ...(fields ? { fields } : {}) } })
|
|
196
|
-
return { id: meta.id, entity: entity.name, title: meta.title, route: meta.id, blocks, nav: { show: true, label: meta.title, order: meta.order } }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
80
|
/** A Kanban board screen: an optional filter panel + a board grouping rows by an
|
|
200
81
|
* enum field into draggable columns (the signature "pipeline" view). */
|
|
201
82
|
export function boardScreen(
|
|
@@ -272,25 +153,6 @@ export function workspaceScreen(
|
|
|
272
153
|
return { ...base, dock: buildDockLayout(base) }
|
|
273
154
|
}
|
|
274
155
|
|
|
275
|
-
/** A record detail-page screen: a full record view (header + status pill + metric
|
|
276
|
-
* tiles + tabbed Overview / related timelines) - the signature view for
|
|
277
|
-
* relation-heavy records (a customer / deal / patient / trip page). */
|
|
278
|
-
export function detailScreen(
|
|
279
|
-
entity: EntitySchema,
|
|
280
|
-
meta: { id: string; title: string; order: number },
|
|
281
|
-
opts: { titleField: string; subtitleField?: string; statusField?: string; metricFields?: string[]; sections?: { label: string; fields: string[] }[]; related?: DetailRelated[] },
|
|
282
|
-
): Screen {
|
|
283
|
-
const blocks: Block[] = [{ id: 'detail-1', span: 3, config: {
|
|
284
|
-
kind: 'detail', titleField: opts.titleField,
|
|
285
|
-
...(opts.subtitleField ? { subtitleField: opts.subtitleField } : {}),
|
|
286
|
-
...(opts.statusField ? { statusField: opts.statusField } : {}),
|
|
287
|
-
...(opts.metricFields?.length ? { metricFields: opts.metricFields } : {}),
|
|
288
|
-
...(opts.sections?.length ? { sections: opts.sections } : {}),
|
|
289
|
-
...(opts.related?.length ? { related: opts.related } : {}),
|
|
290
|
-
} }]
|
|
291
|
-
return { id: meta.id, entity: entity.name, title: meta.title, route: meta.id, blocks, nav: { show: true, label: meta.title, order: meta.order } }
|
|
292
|
-
}
|
|
293
|
-
|
|
294
156
|
/** Build a screen from a template, then override its id / route / title / nav.
|
|
295
157
|
* `linkScreen` wires a master-detail parent row to drill into a detail screen. */
|
|
296
158
|
export function screen(
|
|
@@ -324,6 +186,17 @@ export function project(opts: {
|
|
|
324
186
|
* what makes each app read as a distinct product (Salesforce vs Excel vs Linear). */
|
|
325
187
|
preset?: string
|
|
326
188
|
mode?: 'light' | 'dark'
|
|
189
|
+
/** Bespoke app chrome: raw CSS written to the generated app's `custom.css` (imported
|
|
190
|
+
* after `app.css`, so it overrides). Scope rules under `.sv-app.<appClass>` so an
|
|
191
|
+
* app can look like its real-world archetype, not a recolored sibling. */
|
|
192
|
+
customCss?: string
|
|
193
|
+
/** Extra class on the shell root (the hook `customCss` targets). */
|
|
194
|
+
appClass?: string
|
|
195
|
+
/** A sign-in starter: `{ enabled: true }` gives the generated app a `/login` page,
|
|
196
|
+
* a signed-cookie session, and route guards (dependency-free, no DB). */
|
|
197
|
+
auth?: AuthConfig
|
|
198
|
+
/** Role-based access. With `auth`, seeds one demo login per role. */
|
|
199
|
+
access?: AccessControl
|
|
327
200
|
entities: EntitySchema[]
|
|
328
201
|
screens: Screen[]
|
|
329
202
|
seed: Record<string, Record<string, unknown>[]>
|
|
@@ -340,7 +213,11 @@ export function project(opts: {
|
|
|
340
213
|
accent: opts.accent,
|
|
341
214
|
...(opts.preset ? { preset: opts.preset } : {}),
|
|
342
215
|
...(opts.mode ? { mode: opts.mode } : {}),
|
|
216
|
+
...(opts.customCss ? { customCss: opts.customCss } : {}),
|
|
217
|
+
...(opts.appClass ? { appClass: opts.appClass } : {}),
|
|
343
218
|
shell: { style: opts.navStyle ?? 'sidebar', brand: opts.brand, footer: opts.footer ?? '', navPosition: opts.navPosition ?? 'left' },
|
|
344
219
|
},
|
|
220
|
+
...(opts.auth?.enabled ? { auth: opts.auth } : {}),
|
|
221
|
+
...(opts.access?.enabled ? { access: opts.access } : {}),
|
|
345
222
|
})
|
|
346
223
|
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project a fresh `svgrid-studio designer` session opens with: not a bare stub,
|
|
3
|
+
* but a complete, enterprise-ready multi-page CRM that shows off the grid's range on
|
|
4
|
+
* first paint - a KPI/chart **Overview**, the rich **Customers** CRUD grid (status
|
|
5
|
+
* pills, health bars, export, popup form), a drag-and-drop **Pipeline** board, a
|
|
6
|
+
* **Deals** grid, a **Calendar** of close dates, and a **Forecast** analytics page.
|
|
7
|
+
* Every generated Studio app already wires `@svgrid/enterprise` (data layer, columns,
|
|
8
|
+
* validation); the rich grids + export make that enterprise surface visible.
|
|
9
|
+
*/
|
|
10
|
+
import { project, dashScreen, boardScreen, calendarScreen } from './shared.js'
|
|
11
|
+
import { gridColumns, defaultBlockConfig, buildDockLayout, type StudioProject, type Screen, type Block, type GridConfig, type GridColumnConfig } from '../project.js'
|
|
12
|
+
import type { EntitySchema } from '../../schema.js'
|
|
13
|
+
|
|
14
|
+
/** A realistic B2B customer/account: company + contact, region, industry, plan,
|
|
15
|
+
* seats, MRR, a health score, and lifecycle dates. */
|
|
16
|
+
const customers: EntitySchema = {
|
|
17
|
+
name: 'customers',
|
|
18
|
+
label: 'Customer',
|
|
19
|
+
idField: 'id',
|
|
20
|
+
fields: [
|
|
21
|
+
{ field: 'id', label: 'ID', type: 'number', primaryKey: true, readonly: true },
|
|
22
|
+
{ field: 'company', label: 'Company', type: 'text', required: true },
|
|
23
|
+
{ field: 'contact', label: 'Contact', type: 'text', required: true },
|
|
24
|
+
{ field: 'email', label: 'Email', type: 'text', format: 'email' },
|
|
25
|
+
{ field: 'country', label: 'Country', type: 'text' },
|
|
26
|
+
{ field: 'industry', label: 'Industry', type: 'enum', input: { editorType: 'list' }, options: [
|
|
27
|
+
{ value: 'saas', label: 'SaaS', color: '#6366f1' },
|
|
28
|
+
{ value: 'fintech', label: 'Fintech', color: '#0ea5e9' },
|
|
29
|
+
{ value: 'healthcare', label: 'Healthcare', color: '#10b981' },
|
|
30
|
+
{ value: 'retail', label: 'Retail', color: '#f59e0b' },
|
|
31
|
+
{ value: 'manufacturing', label: 'Manufacturing', color: '#64748b' },
|
|
32
|
+
{ value: 'media', label: 'Media', color: '#ec4899' },
|
|
33
|
+
{ value: 'education', label: 'Education', color: '#14b8a6' },
|
|
34
|
+
{ value: 'logistics', label: 'Logistics', color: '#a855f7' },
|
|
35
|
+
] },
|
|
36
|
+
{ field: 'status', label: 'Status', type: 'enum', required: true, input: { editorType: 'list' }, options: [
|
|
37
|
+
{ value: 'active', label: 'Active', color: '#10b981' },
|
|
38
|
+
{ value: 'trial', label: 'Trial', color: '#f59e0b' },
|
|
39
|
+
{ value: 'paused', label: 'Paused', color: '#64748b' },
|
|
40
|
+
{ value: 'churned', label: 'Churned', color: '#ef4444' },
|
|
41
|
+
] },
|
|
42
|
+
{ field: 'plan', label: 'Plan', type: 'enum', input: { editorType: 'list' }, options: [
|
|
43
|
+
{ value: 'starter', label: 'Starter', color: '#94a3b8' },
|
|
44
|
+
{ value: 'growth', label: 'Growth', color: '#6366f1' },
|
|
45
|
+
{ value: 'scale', label: 'Scale', color: '#0ea5e9' },
|
|
46
|
+
{ value: 'enterprise', label: 'Enterprise', color: '#8b5cf6' },
|
|
47
|
+
] },
|
|
48
|
+
{ field: 'seats', label: 'Seats', type: 'number' },
|
|
49
|
+
{ field: 'mrr', label: 'MRR', type: 'number' },
|
|
50
|
+
{ field: 'health', label: 'Health', type: 'number', min: 0, max: 100 },
|
|
51
|
+
{ field: 'signupDate', label: 'Signed up', type: 'date' },
|
|
52
|
+
{ field: 'lastContact', label: 'Last contact', type: 'date' },
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Sales opportunities against accounts - the Pipeline board / Calendar / Forecast. */
|
|
57
|
+
const deals: EntitySchema = {
|
|
58
|
+
name: 'deals',
|
|
59
|
+
label: 'Deal',
|
|
60
|
+
idField: 'id',
|
|
61
|
+
fields: [
|
|
62
|
+
{ field: 'id', label: 'ID', type: 'number', primaryKey: true, readonly: true },
|
|
63
|
+
{ field: 'name', label: 'Deal', type: 'text', required: true },
|
|
64
|
+
{ field: 'customerId', label: 'Account', type: 'relation', relation: { entity: 'customers', labelField: 'company' } },
|
|
65
|
+
{ field: 'stage', label: 'Stage', type: 'enum', required: true, input: { editorType: 'list' }, options: [
|
|
66
|
+
{ value: 'lead', label: 'Lead', color: '#94a3b8' },
|
|
67
|
+
{ value: 'qualified', label: 'Qualified', color: '#38bdf8' },
|
|
68
|
+
{ value: 'proposal', label: 'Proposal', color: '#6366f1' },
|
|
69
|
+
{ value: 'negotiation', label: 'Negotiation', color: '#f59e0b' },
|
|
70
|
+
{ value: 'won', label: 'Won', color: '#10b981' },
|
|
71
|
+
{ value: 'lost', label: 'Lost', color: '#ef4444' },
|
|
72
|
+
] },
|
|
73
|
+
{ field: 'owner', label: 'Owner', type: 'enum', input: { editorType: 'list' }, options: [
|
|
74
|
+
{ value: 'Sarah Chen', label: 'Sarah Chen', color: '#6366f1' },
|
|
75
|
+
{ value: 'Marcus Lee', label: 'Marcus Lee', color: '#10b981' },
|
|
76
|
+
{ value: 'Elena Vidal', label: 'Elena Vidal', color: '#f59e0b' },
|
|
77
|
+
{ value: 'Tom Baker', label: 'Tom Baker', color: '#ec4899' },
|
|
78
|
+
] },
|
|
79
|
+
{ field: 'amount', label: 'Amount', type: 'number' },
|
|
80
|
+
{ field: 'probability', label: 'Probability', type: 'number', min: 0, max: 100 },
|
|
81
|
+
{ field: 'closeDate', label: 'Close date', type: 'date' },
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** ~20 seeded accounts with a realistic spread of industries, regions, plans and
|
|
86
|
+
* lifecycle states (active/paused carry MRR; trials + churned sit at 0). */
|
|
87
|
+
const customerSeed: Record<string, unknown>[] = [
|
|
88
|
+
{ id: 1, company: 'Meridian Software', contact: 'Sarah Chen', email: 'sarah.chen@meridiansoft.com', country: 'United States', industry: 'saas', status: 'active', plan: 'scale', seats: 42, mrr: 1490, health: 88, signupDate: '2024-03-12', lastContact: '2026-07-28' },
|
|
89
|
+
{ id: 2, company: 'Cedar Analytics', contact: 'Michael Rodriguez', email: 'm.rodriguez@cedaranalytics.io', country: 'United States', industry: 'fintech', status: 'active', plan: 'enterprise', seats: 180, mrr: 4200, health: 94, signupDate: '2023-11-05', lastContact: '2026-08-04' },
|
|
90
|
+
{ id: 3, company: 'BlueSky Labs', contact: 'Emma Thompson', email: 'emma@blueskylabs.co.uk', country: 'United Kingdom', industry: 'saas', status: 'trial', plan: 'starter', seats: 3, mrr: 0, health: 65, signupDate: '2026-06-20', lastContact: '2026-08-06' },
|
|
91
|
+
{ id: 4, company: 'Northgate Systems', contact: 'David Kim', email: 'dkim@northgate.com', country: 'Canada', industry: 'manufacturing', status: 'active', plan: 'growth', seats: 15, mrr: 499, health: 79, signupDate: '2025-01-18', lastContact: '2026-07-15' },
|
|
92
|
+
{ id: 5, company: 'Vantage Digital', contact: 'Olivia Martins', email: 'olivia.martins@vantage.digital', country: 'Australia', industry: 'media', status: 'active', plan: 'growth', seats: 12, mrr: 399, health: 82, signupDate: '2024-09-30', lastContact: '2026-07-22' },
|
|
93
|
+
{ id: 6, company: 'Harbor Health', contact: "James O'Brien", email: 'j.obrien@harborhealth.org', country: 'United States', industry: 'healthcare', status: 'active', plan: 'enterprise', seats: 240, mrr: 5400, health: 91, signupDate: '2023-06-14', lastContact: '2026-08-01' },
|
|
94
|
+
{ id: 7, company: 'Riverstone Media', contact: 'Priya Sharma', email: 'priya@riverstone.media', country: 'India', industry: 'media', status: 'churned', plan: 'scale', seats: 0, mrr: 0, health: 22, signupDate: '2024-02-08', lastContact: '2026-03-19' },
|
|
95
|
+
{ id: 8, company: 'Lumen Retail', contact: 'Lucas Müller', email: 'lucas.mueller@lumenretail.de', country: 'Germany', industry: 'retail', status: 'active', plan: 'scale', seats: 55, mrr: 1890, health: 85, signupDate: '2024-12-02', lastContact: '2026-07-30' },
|
|
96
|
+
{ id: 9, company: 'Pinewood Financial', contact: 'Aisha Bello', email: 'aisha.bello@pinewoodfin.com', country: 'United States', industry: 'fintech', status: 'paused', plan: 'growth', seats: 8, mrr: 0, health: 48, signupDate: '2025-04-27', lastContact: '2026-05-11' },
|
|
97
|
+
{ id: 10, company: 'Orchard Foods', contact: 'Carlos Mendez', email: 'carlos@orchardfoods.mx', country: 'Brazil', industry: 'retail', status: 'active', plan: 'starter', seats: 4, mrr: 99, health: 73, signupDate: '2025-08-09', lastContact: '2026-07-19' },
|
|
98
|
+
{ id: 11, company: 'Ironclad Security', contact: 'Nina Petrova', email: 'nina.petrova@ironcladsec.com', country: 'Netherlands', industry: 'saas', status: 'active', plan: 'enterprise', seats: 130, mrr: 3600, health: 96, signupDate: '2023-09-21', lastContact: '2026-08-05' },
|
|
99
|
+
{ id: 12, company: 'Brightpath Education', contact: 'Tomás Silva', email: 'tomas.silva@brightpath.edu', country: 'Portugal', industry: 'education', status: 'trial', plan: 'starter', seats: 2, mrr: 0, health: 61, signupDate: '2026-07-01', lastContact: '2026-08-07' },
|
|
100
|
+
{ id: 13, company: 'Quantum Ledger', contact: 'Grace Okafor', email: 'grace@quantumledger.io', country: 'United Kingdom', industry: 'fintech', status: 'active', plan: 'scale', seats: 38, mrr: 1290, health: 87, signupDate: '2024-05-16', lastContact: '2026-07-26' },
|
|
101
|
+
{ id: 14, company: 'Evergreen Energy', contact: 'Hiroshi Tanaka', email: 'h.tanaka@evergreen-energy.jp', country: 'Japan', industry: 'manufacturing', status: 'active', plan: 'growth', seats: 20, mrr: 549, health: 80, signupDate: '2025-02-11', lastContact: '2026-06-28' },
|
|
102
|
+
{ id: 15, company: 'Summit Logistics', contact: 'Fatima Al-Farsi', email: 'fatima@summitlogistics.com', country: 'United States', industry: 'logistics', status: 'active', plan: 'scale', seats: 47, mrr: 1590, health: 89, signupDate: '2024-07-03', lastContact: '2026-08-02' },
|
|
103
|
+
{ id: 16, company: 'Fabrikam Retail', contact: 'Erik Larsson', email: 'erik.larsson@fabrikam.se', country: 'Sweden', industry: 'retail', status: 'churned', plan: 'starter', seats: 0, mrr: 0, health: 18, signupDate: '2025-03-22', lastContact: '2026-01-30' },
|
|
104
|
+
{ id: 17, company: 'Beacon Analytics', contact: 'Sofia Rossi', email: 'sofia.rossi@beaconanalytics.com', country: 'Italy', industry: 'saas', status: 'active', plan: 'growth', seats: 11, mrr: 449, health: 76, signupDate: '2025-05-19', lastContact: '2026-07-24' },
|
|
105
|
+
{ id: 18, company: 'Cobalt Cloud', contact: 'Daniel Okoye', email: 'daniel@cobaltcloud.com', country: 'United States', industry: 'saas', status: 'trial', plan: 'growth', seats: 6, mrr: 0, health: 68, signupDate: '2026-06-28', lastContact: '2026-08-08' },
|
|
106
|
+
{ id: 19, company: 'Willow Health', contact: 'Mei Lin', email: 'mei.lin@willowhealth.sg', country: 'Singapore', industry: 'healthcare', status: 'active', plan: 'enterprise', seats: 95, mrr: 2800, health: 92, signupDate: '2023-12-08', lastContact: '2026-07-31' },
|
|
107
|
+
{ id: 20, company: 'Atlas Manufacturing', contact: 'Pierre Dubois', email: 'p.dubois@atlasmfg.fr', country: 'France', industry: 'manufacturing', status: 'paused', plan: 'scale', seats: 30, mrr: 0, health: 44, signupDate: '2024-10-15', lastContact: '2026-04-22' },
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
/** ~24 open + closed opportunities across the accounts, spread over stages, owners
|
|
111
|
+
* and close dates - so the board, calendar, and forecast are all alive. */
|
|
112
|
+
const dealSeed: Record<string, unknown>[] = [
|
|
113
|
+
{ id: 1, name: 'Enterprise rollout', customerId: 2, stage: 'negotiation', owner: 'Sarah Chen', amount: 84000, probability: 70, closeDate: '2026-09-15' },
|
|
114
|
+
{ id: 2, name: 'Platform upgrade', customerId: 6, stage: 'proposal', owner: 'Marcus Lee', amount: 62000, probability: 55, closeDate: '2026-10-02' },
|
|
115
|
+
{ id: 3, name: 'Seat expansion', customerId: 1, stage: 'won', owner: 'Elena Vidal', amount: 24000, probability: 100, closeDate: '2026-07-20' },
|
|
116
|
+
{ id: 4, name: 'Pilot program', customerId: 3, stage: 'qualified', owner: 'Tom Baker', amount: 12000, probability: 35, closeDate: '2026-11-10' },
|
|
117
|
+
{ id: 5, name: 'Annual renewal', customerId: 11, stage: 'negotiation', owner: 'Sarah Chen', amount: 58000, probability: 65, closeDate: '2026-09-30' },
|
|
118
|
+
{ id: 6, name: 'Security add-on', customerId: 19, stage: 'proposal', owner: 'Marcus Lee', amount: 33000, probability: 45, closeDate: '2026-10-18' },
|
|
119
|
+
{ id: 7, name: 'Data migration', customerId: 8, stage: 'lead', owner: 'Elena Vidal', amount: 21000, probability: 20, closeDate: '2026-12-05' },
|
|
120
|
+
{ id: 8, name: 'Multi-region', customerId: 13, stage: 'won', owner: 'Tom Baker', amount: 47000, probability: 100, closeDate: '2026-06-28' },
|
|
121
|
+
{ id: 9, name: 'Onboarding package', customerId: 5, stage: 'qualified', owner: 'Sarah Chen', amount: 9000, probability: 40, closeDate: '2026-11-22' },
|
|
122
|
+
{ id: 10, name: 'Analytics tier', customerId: 15, stage: 'proposal', owner: 'Marcus Lee', amount: 38000, probability: 50, closeDate: '2026-10-08' },
|
|
123
|
+
{ id: 11, name: 'Compliance module', customerId: 6, stage: 'negotiation', owner: 'Elena Vidal', amount: 72000, probability: 75, closeDate: '2026-09-12' },
|
|
124
|
+
{ id: 12, name: 'Trial conversion', customerId: 3, stage: 'lead', owner: 'Tom Baker', amount: 6000, probability: 15, closeDate: '2026-12-20' },
|
|
125
|
+
{ id: 13, name: 'Enterprise SSO', customerId: 2, stage: 'won', owner: 'Sarah Chen', amount: 30000, probability: 100, closeDate: '2026-07-05' },
|
|
126
|
+
{ id: 14, name: 'Capacity boost', customerId: 14, stage: 'qualified', owner: 'Marcus Lee', amount: 15000, probability: 30, closeDate: '2026-11-15' },
|
|
127
|
+
{ id: 15, name: 'Premium support', customerId: 17, stage: 'proposal', owner: 'Elena Vidal', amount: 18000, probability: 45, closeDate: '2026-10-25' },
|
|
128
|
+
{ id: 16, name: 'Global expansion', customerId: 11, stage: 'lost', owner: 'Tom Baker', amount: 90000, probability: 0, closeDate: '2026-05-30' },
|
|
129
|
+
{ id: 17, name: 'API integration', customerId: 10, stage: 'qualified', owner: 'Sarah Chen', amount: 8000, probability: 35, closeDate: '2026-11-28' },
|
|
130
|
+
{ id: 18, name: 'Renewal + upsell', customerId: 19, stage: 'negotiation', owner: 'Marcus Lee', amount: 66000, probability: 70, closeDate: '2026-09-22' },
|
|
131
|
+
{ id: 19, name: 'Sandbox pilot', customerId: 18, stage: 'lead', owner: 'Elena Vidal', amount: 7000, probability: 20, closeDate: '2026-12-12' },
|
|
132
|
+
{ id: 20, name: 'Custom reporting', customerId: 15, stage: 'won', owner: 'Tom Baker', amount: 26000, probability: 100, closeDate: '2026-06-18' },
|
|
133
|
+
{ id: 21, name: 'Migration service', customerId: 4, stage: 'proposal', owner: 'Sarah Chen', amount: 22000, probability: 50, closeDate: '2026-10-14' },
|
|
134
|
+
{ id: 22, name: 'Team plan', customerId: 5, stage: 'lost', owner: 'Marcus Lee', amount: 11000, probability: 0, closeDate: '2026-04-16' },
|
|
135
|
+
{ id: 23, name: 'Platform renewal', customerId: 1, stage: 'negotiation', owner: 'Elena Vidal', amount: 44000, probability: 60, closeDate: '2026-09-08' },
|
|
136
|
+
{ id: 24, name: 'Advanced tier', customerId: 13, stage: 'qualified', owner: 'Tom Baker', amount: 19000, probability: 40, closeDate: '2026-11-05' },
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
/** The hero grid: the no-code default plus the enterprise surface (filtering, export
|
|
140
|
+
* toolbar, totals, status pills, row actions) and custom cells - MRR as currency,
|
|
141
|
+
* health as a progress bar, email as a link, dates formatted, larger comfortable rows. */
|
|
142
|
+
function crudGrid(): Block {
|
|
143
|
+
const base = defaultBlockConfig('grid', customers) as GridConfig
|
|
144
|
+
const columns: GridColumnConfig[] = gridColumns(customers).map((c) =>
|
|
145
|
+
c.field === 'mrr' ? { ...c, header: 'MRR', format: { type: 'currency', currency: 'USD' }, align: 'right', aggregate: 'sum' }
|
|
146
|
+
: c.field === 'seats' ? { ...c, align: 'right', aggregate: 'sum' }
|
|
147
|
+
: c.field === 'health' ? { ...c, header: 'Health', width: 140, cellType: { kind: 'progress', max: 100 } }
|
|
148
|
+
: c.field === 'signupDate' || c.field === 'lastContact' ? { ...c, format: { type: 'date' } }
|
|
149
|
+
: c.field === 'email' ? { ...c, cellType: { kind: 'link', as: 'email' } }
|
|
150
|
+
: c,
|
|
151
|
+
)
|
|
152
|
+
const config: GridConfig = {
|
|
153
|
+
...base,
|
|
154
|
+
columns,
|
|
155
|
+
filterable: true,
|
|
156
|
+
filterUi: { global: true, row: false, menu: true },
|
|
157
|
+
export: { csv: true, json: true, copy: true },
|
|
158
|
+
rowSummaries: true,
|
|
159
|
+
pageSize: 10,
|
|
160
|
+
density: 'comfortable',
|
|
161
|
+
rowActions: [{ kind: 'edit' }, { kind: 'delete' }],
|
|
162
|
+
props: { headerHeight: 48 },
|
|
163
|
+
}
|
|
164
|
+
return { id: 'grid-1', span: 3, height: 440, config }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The Deals grid: stage/owner chips, currency amount (summed), a probability bar. */
|
|
168
|
+
function dealsGrid(): Block {
|
|
169
|
+
const base = defaultBlockConfig('grid', deals) as GridConfig
|
|
170
|
+
const columns: GridColumnConfig[] = gridColumns(deals).map((c) =>
|
|
171
|
+
c.field === 'amount' ? { ...c, format: { type: 'currency', currency: 'USD' }, align: 'right', aggregate: 'sum' }
|
|
172
|
+
: c.field === 'probability' ? { ...c, header: 'Win %', width: 130, cellType: { kind: 'progress', max: 100 } }
|
|
173
|
+
: c.field === 'closeDate' ? { ...c, format: { type: 'date' } }
|
|
174
|
+
: c,
|
|
175
|
+
)
|
|
176
|
+
const config: GridConfig = {
|
|
177
|
+
...base,
|
|
178
|
+
columns,
|
|
179
|
+
filterable: true,
|
|
180
|
+
filterUi: { global: true, row: false, menu: true },
|
|
181
|
+
export: { csv: true, json: true, copy: true },
|
|
182
|
+
rowSummaries: true,
|
|
183
|
+
pageSize: 10,
|
|
184
|
+
density: 'comfortable',
|
|
185
|
+
rowActions: [{ kind: 'edit' }, { kind: 'delete' }],
|
|
186
|
+
props: { headerHeight: 48 },
|
|
187
|
+
}
|
|
188
|
+
return { id: 'grid-1', span: 3, height: 440, config }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** A docking-workspace console: a facet filter, the rich grid, and a record panel laid
|
|
192
|
+
* out in a live SvDockManager (drag to float / pin / re-dock) - the signature "wow"
|
|
193
|
+
* layout. `buildDockLayout` seeds filter-left, grid-centre, record-right. */
|
|
194
|
+
function dockConsole(entity: EntitySchema, id: string, title: string, order: number, gridBlock: Block, filterFields: string[]): Screen {
|
|
195
|
+
const blocks: Block[] = [
|
|
196
|
+
{ id: 'filter-1', span: 3, config: { kind: 'filter', fields: filterFields } },
|
|
197
|
+
gridBlock,
|
|
198
|
+
{ id: 'record-1', span: 3, config: { kind: 'record', editable: true } },
|
|
199
|
+
]
|
|
200
|
+
const base: Screen = { id, entity: entity.name, title, route: id, blocks, nav: { show: true, label: title, order }, layout: 'dock' }
|
|
201
|
+
return { ...base, dock: buildDockLayout(base) }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The Overview: the hero Customers grid FIRST (so opening the app lands on a real
|
|
205
|
+
* data grid, not a chart), with KPI cards + charts below it as supporting context. */
|
|
206
|
+
function overviewScreen(): Screen {
|
|
207
|
+
const dash = dashScreen(customers, { id: 'overview', title: 'Overview', order: 0 }, [
|
|
208
|
+
{ kpi: 'Recurring revenue', measure: 'mrr', reduce: 'sum', format: 'currency', span: 1 },
|
|
209
|
+
{ kpi: 'Accounts', reduce: 'count', span: 1 },
|
|
210
|
+
{ kpi: 'Seats', measure: 'seats', reduce: 'sum', format: 'compact', span: 1 },
|
|
211
|
+
{ gauge: 'Avg health', measure: 'health', reduce: 'avg', min: 0, max: 100, span: 1 },
|
|
212
|
+
{ chart: 'industry', measure: 'mrr', reduce: 'sum', type: 'bar', span: 2 },
|
|
213
|
+
{ chart: 'status', reduce: 'count', type: 'pie', span: 1 },
|
|
214
|
+
])
|
|
215
|
+
return { ...dash, blocks: [crudGrid(), ...dash.blocks] }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The default first-run project: a complete, enterprise-ready multi-page CRM whose
|
|
219
|
+
* hero is the CRUD grid (Overview + two SvDockManager consoles). */
|
|
220
|
+
export function starterProject(): StudioProject {
|
|
221
|
+
return project({
|
|
222
|
+
title: 'Acme CRM',
|
|
223
|
+
brand: 'Acme CRM',
|
|
224
|
+
accent: '#6366f1',
|
|
225
|
+
footer: 'Built with SvGrid Studio',
|
|
226
|
+
entities: [customers, deals],
|
|
227
|
+
screens: [
|
|
228
|
+
// Overview: KPIs + charts ABOVE the hero Customers grid.
|
|
229
|
+
overviewScreen(),
|
|
230
|
+
// Customers: a dockable console - filter + hero grid + record panel (drag to float / pin).
|
|
231
|
+
dockConsole(customers, 'customers', 'Customers', 1, crudGrid(), ['status', 'industry', 'plan']),
|
|
232
|
+
// Pipeline: deals as draggable cards in stage columns (drag to change stage).
|
|
233
|
+
boardScreen(deals, { id: 'pipeline', title: 'Pipeline', order: 2 }, { groupBy: 'stage', titleField: 'name', badgeField: 'amount', subtitleField: 'owner', filter: ['owner', 'stage'] }),
|
|
234
|
+
// Deals: a second docking console over the opportunity grid.
|
|
235
|
+
dockConsole(deals, 'deals', 'Deals', 3, dealsGrid(), ['stage', 'owner']),
|
|
236
|
+
// Calendar: deals placed on a month calendar by their close date, tinted by stage.
|
|
237
|
+
calendarScreen(deals, { id: 'calendar', title: 'Calendar', order: 4 }, { dateField: 'closeDate', titleField: 'name', colorField: 'stage', filter: ['owner'] }),
|
|
238
|
+
// Forecast: pipeline KPIs, value by stage/owner, and a pivot of amount by owner x stage.
|
|
239
|
+
dashScreen(deals, { id: 'forecast', title: 'Forecast', order: 5 }, [
|
|
240
|
+
{ kpi: 'Pipeline value', measure: 'amount', reduce: 'sum', format: 'currency', span: 1 },
|
|
241
|
+
{ kpi: 'Open deals', reduce: 'count', span: 1 },
|
|
242
|
+
{ kpi: 'Avg deal size', measure: 'amount', reduce: 'avg', format: 'currency', span: 1 },
|
|
243
|
+
{ kpi: 'Largest deal', measure: 'amount', reduce: 'max', format: 'currency', span: 1 },
|
|
244
|
+
{ chart: 'stage', measure: 'amount', reduce: 'sum', type: 'bar', span: 2 },
|
|
245
|
+
{ chart: 'owner', measure: 'amount', reduce: 'sum', type: 'bar', span: 1 },
|
|
246
|
+
{ pivot: { rows: ['owner'], cols: ['stage'], measure: 'amount', aggregate: 'sum' }, span: 3 },
|
|
247
|
+
]),
|
|
248
|
+
],
|
|
249
|
+
seed: { customers: customerSeed, deals: dealSeed },
|
|
250
|
+
})
|
|
251
|
+
}
|