@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,187 @@
|
|
|
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
|
+
const products: EntitySchema = {
|
|
6
|
+
name: 'products',
|
|
7
|
+
label: 'Product',
|
|
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: 'sku', type: 'text', label: 'SKU', input: { editorType: 'mask', mask: 'AAA-#####', help: 'Warehouse stock code' } },
|
|
13
|
+
{ field: 'category', type: 'enum', required: true, options: [
|
|
14
|
+
{ value: 'electronics', label: 'Electronics', color: '#6366f1' },
|
|
15
|
+
{ value: 'home', label: 'Home', color: '#10b981' },
|
|
16
|
+
{ value: 'apparel', label: 'Apparel', color: '#f59e0b' },
|
|
17
|
+
{ value: 'office', label: 'Office', color: '#0ea5e9' },
|
|
18
|
+
] },
|
|
19
|
+
{ field: 'price', type: 'number', label: 'Price ($)', required: true, min: 0, defaultValue: 0, input: { prefix: '$', help: 'Retail price' } },
|
|
20
|
+
{ field: 'stock', type: 'number', label: 'In stock', min: 0, defaultValue: 0 },
|
|
21
|
+
// Derived: inventory value on hand (display-only, never stored).
|
|
22
|
+
{ field: 'stockValue', type: 'number', label: 'Stock value ($)', readonly: true, formula: 'price * stock' },
|
|
23
|
+
{ field: 'rating', type: 'number', label: 'Rating', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 customer rating' } },
|
|
24
|
+
{ field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Merchandising tags' } },
|
|
25
|
+
{ field: 'color', type: 'text', label: 'Swatch', input: { editorType: 'color', help: 'Primary colorway' } },
|
|
26
|
+
{ field: 'imageUrl', type: 'text', label: 'Image', format: 'url', input: { placeholder: 'https://…' } },
|
|
27
|
+
{ field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const customers: EntitySchema = {
|
|
32
|
+
name: 'customers',
|
|
33
|
+
label: 'Customer',
|
|
34
|
+
idField: 'id',
|
|
35
|
+
fields: [
|
|
36
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
37
|
+
{ field: 'name', type: 'text', required: true, minLength: 2 },
|
|
38
|
+
{ field: 'email', type: 'text', format: 'email', required: true },
|
|
39
|
+
{ field: 'phone', type: 'text', input: { editorType: 'phone', help: 'Primary contact number' } },
|
|
40
|
+
{ field: 'country', type: 'text', label: 'Country', input: { editorType: 'country', help: 'Billing country' } },
|
|
41
|
+
{ field: 'tier', type: 'enum', required: true, options: [
|
|
42
|
+
{ value: 'free', label: 'Free', color: '#94a3b8' },
|
|
43
|
+
{ value: 'plus', label: 'Plus', color: '#6366f1' },
|
|
44
|
+
{ value: 'vip', label: 'VIP', color: '#f59e0b' },
|
|
45
|
+
] },
|
|
46
|
+
{ field: 'joinedAt', type: 'datetime', label: 'Joined', input: { help: 'Signup date and time' } },
|
|
47
|
+
{ field: 'active', type: 'boolean', defaultValue: true },
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const orders: EntitySchema = {
|
|
52
|
+
name: 'orders',
|
|
53
|
+
label: 'Order',
|
|
54
|
+
idField: 'id',
|
|
55
|
+
fields: [
|
|
56
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
57
|
+
{ field: 'ref', type: 'text', label: 'Reference' },
|
|
58
|
+
{ field: 'customerId', type: 'relation', label: 'Customer', relation: { entity: 'customers', foreignKey: 'customerId', labelField: 'name' } },
|
|
59
|
+
{ field: 'status', type: 'enum', required: true, options: [
|
|
60
|
+
{ value: 'pending', label: 'Pending', color: '#f59e0b' },
|
|
61
|
+
{ value: 'paid', label: 'Paid', color: '#6366f1' },
|
|
62
|
+
{ value: 'shipped', label: 'Shipped', color: '#10b981' },
|
|
63
|
+
{ value: 'refunded', label: 'Refunded', color: '#ef4444' },
|
|
64
|
+
] },
|
|
65
|
+
{ field: 'total', type: 'number', label: 'Total ($)', required: true, min: 0, input: { prefix: '$' } },
|
|
66
|
+
{ field: 'discount', type: 'number', label: 'Discount (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Order-level discount' } },
|
|
67
|
+
// Derived: revenue after the discount is applied (display-only).
|
|
68
|
+
{ field: 'net', type: 'number', label: 'Net ($)', readonly: true, formula: 'total - total * discount / 100' },
|
|
69
|
+
{ field: 'placedAt', type: 'datetime', label: 'Placed' },
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Order grid formatting: status pills plus numeric thresholds - big-ticket orders
|
|
74
|
+
// green + bold, tiny orders muted, and steep discounts flagged red so margin risk
|
|
75
|
+
// pops without reading a number.
|
|
76
|
+
const orderFormats: FormatRule[] = [
|
|
77
|
+
...statusPills(orders, 'status'),
|
|
78
|
+
{ field: 'total', op: 'gte', value: 500, color: '#16a34a', bold: true },
|
|
79
|
+
{ field: 'total', op: 'lt', value: 100, color: '#94a3b8' },
|
|
80
|
+
{ field: 'discount', op: 'gte', value: 20, color: '#dc2626', bold: true },
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
// Product grid formatting: category pills plus stock thresholds - out-of-stock /
|
|
84
|
+
// low stock red + bold, healthy stock green, and premium prices bold.
|
|
85
|
+
const productFormats: FormatRule[] = [
|
|
86
|
+
...statusPills(products, 'category'),
|
|
87
|
+
{ field: 'stock', op: 'lt', value: 10, color: '#dc2626', bold: true },
|
|
88
|
+
{ field: 'stock', op: 'gte', value: 200, color: '#16a34a' },
|
|
89
|
+
{ field: 'price', op: 'gte', value: 300, bold: true },
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
const seed = {
|
|
93
|
+
products: [
|
|
94
|
+
{ id: 'p1', name: 'Mechanical Keyboard', sku: 'ELE-01001', category: 'electronics', price: 120, stock: 42, rating: 5, tags: ['Bestseller', 'New'], color: '#111827', imageUrl: 'https://cdn.shopadmin.io/p/keyboard.png', active: true },
|
|
95
|
+
{ id: 'p2', name: 'Standing Desk', sku: 'HOM-02002', category: 'home', price: 480, stock: 12, rating: 4, tags: ['Ergonomic'], color: '#a16207', imageUrl: 'https://cdn.shopadmin.io/p/desk.png', active: true },
|
|
96
|
+
{ id: 'p3', name: 'Noise-Cancelling Headphones', sku: 'ELE-01003', category: 'electronics', price: 260, stock: 0, rating: 4, tags: ['Premium', 'Audio'], color: '#1e3a8a', imageUrl: 'https://cdn.shopadmin.io/p/headphones.png', active: false },
|
|
97
|
+
{ id: 'p4', name: 'Ergonomic Mouse', sku: 'ELE-01004', category: 'electronics', price: 60, stock: 130, rating: 5, tags: ['Ergonomic', 'Wireless'], color: '#374151', imageUrl: 'https://cdn.shopadmin.io/p/mouse.png', active: true },
|
|
98
|
+
{ id: 'p5', name: 'Desk Lamp', sku: 'HOM-02005', category: 'home', price: 45, stock: 88, rating: 3, tags: ['Home'], color: '#f59e0b', imageUrl: 'https://cdn.shopadmin.io/p/lamp.png', active: true },
|
|
99
|
+
{ id: 'p6', name: 'Cotton Hoodie', sku: 'APP-03006', category: 'apparel', price: 55, stock: 210, rating: 4, tags: ['Cotton', 'Casual'], color: '#6366f1', imageUrl: 'https://cdn.shopadmin.io/p/hoodie.png', active: true },
|
|
100
|
+
{ id: 'p7', name: 'Notebook Pack', sku: 'OFF-04007', category: 'office', price: 18, stock: 500, rating: 5, tags: ['Bulk'], color: '#10b981', imageUrl: 'https://cdn.shopadmin.io/p/notebook.png', active: true },
|
|
101
|
+
],
|
|
102
|
+
customers: [
|
|
103
|
+
{ id: 'c1', name: 'Ada Lovelace', email: 'ada@analytic.io', phone: '+1 (415) 555-0110', country: 'United States', tier: 'vip', joinedAt: '2025-01-15T09:20:00Z', active: true },
|
|
104
|
+
{ id: 'c2', name: 'Alan Turing', email: 'alan@bletchley.uk', phone: '+44 20 7946 0111', country: 'United Kingdom', tier: 'plus', joinedAt: '2025-03-02T14:05:00Z', active: true },
|
|
105
|
+
{ id: 'c3', name: 'Grace Hopper', email: 'grace@navy.mil', phone: '+1 (202) 555-0112', country: 'United States', tier: 'vip', joinedAt: '2024-11-08T11:45:00Z', active: true },
|
|
106
|
+
{ id: 'c4', name: 'Linus Torvalds', email: 'linus@kernel.org', phone: '+358 9 5550113', country: 'Finland', tier: 'plus', joinedAt: '2025-05-21T08:30:00Z', active: true },
|
|
107
|
+
{ id: 'c5', name: 'Barbara Liskov', email: 'barbara@sub.dev', phone: '+1 (650) 555-0114', country: 'United States', tier: 'free', joinedAt: '2025-02-17T16:10:00Z', active: false },
|
|
108
|
+
{ id: 'c6', name: 'Guido van Rossum', email: 'guido@py.org', phone: '+31 20 5550115', country: 'Netherlands', tier: 'plus', joinedAt: '2025-04-09T10:00:00Z', active: true },
|
|
109
|
+
],
|
|
110
|
+
orders: [
|
|
111
|
+
{ id: 'o1', ref: 'ORD-1001', customerId: 'c1', status: 'shipped', total: 640, discount: 10, placedAt: '2026-06-12T14:30:00Z' },
|
|
112
|
+
{ id: 'o2', ref: 'ORD-1002', customerId: 'c2', status: 'paid', total: 120, discount: 0, placedAt: '2026-06-18T09:15:00Z' },
|
|
113
|
+
{ id: 'o3', ref: 'ORD-1003', customerId: 'c3', status: 'pending', total: 305, discount: 15, placedAt: '2026-07-01T18:40:00Z' },
|
|
114
|
+
{ id: 'o4', ref: 'ORD-1004', customerId: 'c1', status: 'refunded', total: 260, discount: 0, placedAt: '2026-05-22T12:05:00Z' },
|
|
115
|
+
{ id: 'o5', ref: 'ORD-1005', customerId: 'c4', status: 'shipped', total: 78, discount: 5, placedAt: '2026-07-03T07:50:00Z' },
|
|
116
|
+
{ id: 'o6', ref: 'ORD-1006', customerId: 'c6', status: 'paid', total: 540, discount: 20, placedAt: '2026-07-08T20:25:00Z' },
|
|
117
|
+
{ id: 'o7', ref: 'ORD-1007', customerId: 'c3', status: 'pending', total: 45, discount: 0, placedAt: '2026-07-10T13:00:00Z' },
|
|
118
|
+
{ id: 'o8', ref: 'ORD-1008', customerId: 'c2', status: 'shipped', total: 198, discount: 25, placedAt: '2026-06-28T15:35:00Z' },
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const ecommerce: SampleApp = {
|
|
123
|
+
id: 'ecommerce',
|
|
124
|
+
name: 'E-commerce admin',
|
|
125
|
+
description: 'Products, customers and orders - enterprise revenue + catalog dashboards (KPI revenue sparkline + target, gauges, area sales trend, category x stock pivot, tabbed breakdowns), filtered status-pill grids with low-stock red formatting and row actions, per-customer order-history master/detail, and rich edit forms (SKU mask, star rating, tags, color swatch, phone, country, discount slider).',
|
|
126
|
+
emoji: '\u{1F6D2}',
|
|
127
|
+
accent: '#10b981',
|
|
128
|
+
build: () => {
|
|
129
|
+
const productRows = pad(products, seed.products, 18)
|
|
130
|
+
const customerRows = pad(customers, seed.customers, 16)
|
|
131
|
+
const orderRows = pad(orders, seed.orders, 34, { customerId: ids(customerRows) })
|
|
132
|
+
return project({
|
|
133
|
+
title: 'Shop Admin',
|
|
134
|
+
brand: 'Shop Admin',
|
|
135
|
+
accent: '#10b981',
|
|
136
|
+
preset: 'vercel',
|
|
137
|
+
navStyle: 'top-nav',
|
|
138
|
+
footer: '© Shop Inc.',
|
|
139
|
+
entities: [products, customers, orders],
|
|
140
|
+
seed: { products: productRows, customers: customerRows, orders: orderRows },
|
|
141
|
+
screens: [
|
|
142
|
+
// Revenue dashboard: KPI cards (a revenue card with a sparkline over placed
|
|
143
|
+
// dates + a $12k target), a target gauge, a status breakdown + pie, a
|
|
144
|
+
// sales-over-time area trend, a tabbed breakdown, and a status-pill order grid.
|
|
145
|
+
dashScreen(orders, { id: 'overview', title: 'Overview', order: 0 }, [
|
|
146
|
+
{ kpi: 'Revenue', measure: 'total', reduce: 'sum', format: 'currency', trendField: 'placedAt', trendReduce: 'sum', target: 12000, span: 1 },
|
|
147
|
+
{ kpi: 'Orders', reduce: 'count', span: 1 },
|
|
148
|
+
{ kpi: 'Avg order', measure: 'total', reduce: 'avg', format: 'currency', span: 1 },
|
|
149
|
+
{ gauge: 'Revenue vs $15k target', measure: 'total', reduce: 'sum', min: 0, max: 15000, unit: '$', span: 1 },
|
|
150
|
+
{ chart: 'status', measure: 'total', reduce: 'sum', type: 'bar', span: 2 },
|
|
151
|
+
{ chart: 'status', reduce: 'count', type: 'pie', span: 1 },
|
|
152
|
+
{ chart: 'placedAt', measure: 'total', reduce: 'sum', type: 'area', span: 3 },
|
|
153
|
+
{ tabs: [
|
|
154
|
+
{ label: 'Revenue by status', tiles: [{ chart: 'status', measure: 'total', reduce: 'sum', type: 'bar' }] },
|
|
155
|
+
{ label: 'Orders by status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
|
|
156
|
+
], span: 3 },
|
|
157
|
+
{ grid: true, format: orderFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
158
|
+
]),
|
|
159
|
+
// Catalog dashboard: stock + price KPIs (units compact-formatted), a rating
|
|
160
|
+
// gauge, category breakdowns, a category x active pivot of stock, a faceted
|
|
161
|
+
// filter, and a product grid with low-stock red formatting + inline edit.
|
|
162
|
+
// Fulfillment board: orders as cards in status columns (pending/paid/shipped/refunded).
|
|
163
|
+
boardScreen(orders, { id: 'board', title: 'Fulfillment', order: 1 }, { groupBy: 'status', titleField: 'ref', badgeField: 'total', filter: ['status', 'customerId'] }),
|
|
164
|
+
// Customer 360: shopper header (tier pill) with an order-history timeline
|
|
165
|
+
// (by placed date, colored by fulfillment status, $ order totals).
|
|
166
|
+
detailScreen(customers, { id: 'customer-360', title: 'Customer 360', order: 2 }, {
|
|
167
|
+
titleField: 'name', subtitleField: 'country', statusField: 'tier',
|
|
168
|
+
related: [{ entity: 'orders', foreignKey: 'customerId', label: 'Orders', titleField: 'ref', subtitleField: 'total', dateField: 'placedAt', statusField: 'status' }],
|
|
169
|
+
}),
|
|
170
|
+
dashScreen(products, { id: 'catalog', title: 'Catalog', order: 3 }, [
|
|
171
|
+
{ kpi: 'Products', reduce: 'count', span: 1 },
|
|
172
|
+
{ kpi: 'Units in stock', measure: 'stock', reduce: 'sum', format: 'compact', span: 1 },
|
|
173
|
+
{ kpi: 'Avg price', measure: 'price', reduce: 'avg', format: 'currency', span: 1 },
|
|
174
|
+
{ gauge: 'Avg rating', measure: 'rating', reduce: 'avg', min: 0, max: 5, span: 1 },
|
|
175
|
+
{ chart: 'category', measure: 'stock', reduce: 'sum', type: 'bar', span: 2 },
|
|
176
|
+
{ chart: 'category', reduce: 'count', type: 'pie', span: 1 },
|
|
177
|
+
{ pivot: { rows: ['category'], cols: ['active'], measure: 'stock', aggregate: 'sum' }, span: 3 },
|
|
178
|
+
{ filter: ['category', 'active'], span: 3 },
|
|
179
|
+
{ grid: true, format: productFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
180
|
+
]),
|
|
181
|
+
screen(customers, 'master-detail', { id: 'customers', title: 'Customers', order: 4, child: orders, foreignKey: 'customerId', linkScreen: 'customer-360' }),
|
|
182
|
+
formScreen(products, { id: 'products', title: 'Products', order: 5 }, undefined, { format: productFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['category', 'tags', 'active']),
|
|
183
|
+
formScreen(orders, { id: 'orders', title: 'Orders', order: 6 }, undefined, { format: orderFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['status', 'customerId']),
|
|
184
|
+
],
|
|
185
|
+
})
|
|
186
|
+
},
|
|
187
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { EntitySchema } from '../../schema.js'
|
|
2
|
+
import type { FormatRule } from '../project.js'
|
|
3
|
+
import { screen, formScreen, calendarScreen, project, dashScreen, detailScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
|
|
4
|
+
|
|
5
|
+
const eventsEntity: EntitySchema = {
|
|
6
|
+
name: 'events',
|
|
7
|
+
label: 'Event',
|
|
8
|
+
idField: 'id',
|
|
9
|
+
fields: [
|
|
10
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
11
|
+
{ field: 'name', type: 'text', required: true, minLength: 3, input: { help: 'Public event title' } },
|
|
12
|
+
{ field: 'category', type: 'enum', label: 'Category', required: true, defaultValue: 'conference', options: [
|
|
13
|
+
{ value: 'conference', label: 'Conference', color: '#6366f1' },
|
|
14
|
+
{ value: 'workshop', label: 'Workshop', color: '#10b981' },
|
|
15
|
+
{ value: 'webinar', label: 'Webinar', color: '#0ea5e9' },
|
|
16
|
+
{ value: 'meetup', label: 'Meetup', color: '#f59e0b' },
|
|
17
|
+
] },
|
|
18
|
+
{ field: 'status', type: 'enum', label: 'Status', required: true, defaultValue: 'draft', options: [
|
|
19
|
+
{ value: 'draft', label: 'Draft', color: '#94a3b8' },
|
|
20
|
+
{ value: 'published', label: 'Published', color: '#6366f1' },
|
|
21
|
+
{ value: 'sold_out', label: 'Sold out', color: '#ef4444' },
|
|
22
|
+
{ value: 'done', label: 'Done', color: '#10b981' },
|
|
23
|
+
] },
|
|
24
|
+
{ field: 'capacity', type: 'number', label: 'Capacity', required: true, min: 1, max: 5000, defaultValue: 100, input: { help: 'Maximum seats available' } },
|
|
25
|
+
{ field: 'price', type: 'number', label: 'Ticket price ($)', min: 0, defaultValue: 0, input: { prefix: '$', help: 'Standard ticket price' } },
|
|
26
|
+
// Derived: projected gross if the event sells out (display-only, never stored).
|
|
27
|
+
{ field: 'revenue', type: 'number', label: 'Max revenue ($)', readonly: true, formula: 'price * capacity' },
|
|
28
|
+
{ field: 'startAt', type: 'datetime', label: 'Starts at', required: true, input: { editorType: 'datetime', help: 'Local start date and time' } },
|
|
29
|
+
{ field: 'topics', type: 'text', label: 'Topics', input: { editorType: 'chips', help: 'Session themes / tracks' } },
|
|
30
|
+
{ field: 'color', type: 'text', label: 'Brand color', defaultValue: '#6366f1', input: { editorType: 'color', help: 'Accent used on the event page' } },
|
|
31
|
+
{ field: 'website', type: 'text', label: 'Website', format: 'url', input: { placeholder: 'https://…', help: 'Landing page URL' } },
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const attendees: EntitySchema = {
|
|
36
|
+
name: 'attendees',
|
|
37
|
+
label: 'Attendee',
|
|
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, input: { help: 'Used for the confirmation email' } },
|
|
43
|
+
{ field: 'phone', type: 'text', label: 'Phone', input: { editorType: 'phone', help: 'Mobile for event reminders' } },
|
|
44
|
+
{ field: 'country', type: 'text', label: 'Country', input: { editorType: 'country', help: 'Billing country' } },
|
|
45
|
+
{ field: 'company', type: 'text', label: 'Company' },
|
|
46
|
+
{ field: 'interests', type: 'text', label: 'Interests', input: { editorType: 'chips', help: 'Topics this attendee follows' } },
|
|
47
|
+
{ field: 'vip', type: 'boolean', label: 'VIP', defaultValue: false },
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const registrations: EntitySchema = {
|
|
52
|
+
name: 'registrations',
|
|
53
|
+
label: 'Registration',
|
|
54
|
+
idField: 'id',
|
|
55
|
+
fields: [
|
|
56
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
57
|
+
{ field: 'eventId', type: 'relation', label: 'Event', relation: { entity: 'events', foreignKey: 'eventId', labelField: 'name' } },
|
|
58
|
+
{ field: 'attendeeId', type: 'relation', label: 'Attendee', relation: { entity: 'attendees', foreignKey: 'attendeeId', labelField: 'name' } },
|
|
59
|
+
{ field: 'ticket', type: 'enum', label: 'Ticket', required: true, defaultValue: 'standard', options: [
|
|
60
|
+
{ value: 'standard', label: 'Standard', color: '#6366f1' },
|
|
61
|
+
{ value: 'vip', label: 'VIP', color: '#f59e0b' },
|
|
62
|
+
{ value: 'student', label: 'Student', color: '#10b981' },
|
|
63
|
+
] },
|
|
64
|
+
{ field: 'amount', type: 'number', label: 'Amount ($)', min: 0, defaultValue: 0, input: { prefix: '$', help: 'Charged after any discount' } },
|
|
65
|
+
{ field: 'discount', type: 'number', label: 'Discount (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Promo discount applied' } },
|
|
66
|
+
{ field: 'status', type: 'enum', label: 'Status', required: true, defaultValue: 'confirmed', options: [
|
|
67
|
+
{ value: 'confirmed', label: 'Confirmed', color: '#10b981' },
|
|
68
|
+
{ value: 'cancelled', label: 'Cancelled', color: '#ef4444' },
|
|
69
|
+
{ value: 'waitlist', label: 'Waitlist', color: '#94a3b8' },
|
|
70
|
+
] },
|
|
71
|
+
{ field: 'registeredAt', type: 'datetime', label: 'Registered at', input: { editorType: 'datetime', help: 'When the ticket was booked' } },
|
|
72
|
+
{ field: 'rating', type: 'number', label: 'Feedback', min: 1, max: 5, input: { editorType: 'rating', help: 'Post-event satisfaction (1-5)' } },
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Registration grid formatting: status + ticket-tier pills, plus numeric
|
|
77
|
+
// thresholds (big-ticket amounts bold, poor ratings red, top ratings green) -
|
|
78
|
+
// data-driven color, not just status pills.
|
|
79
|
+
const registrationFormats: FormatRule[] = [
|
|
80
|
+
...statusPills(registrations, 'status'),
|
|
81
|
+
...statusPills(registrations, 'ticket'),
|
|
82
|
+
{ field: 'amount', op: 'gte', value: 300, bold: true },
|
|
83
|
+
{ field: 'rating', op: 'lte', value: 2, color: '#dc2626' },
|
|
84
|
+
{ field: 'rating', op: 'gte', value: 5, color: '#16a34a', bold: true },
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
// Event grid formatting: status + category pills, plus capacity / price
|
|
88
|
+
// thresholds (large-venue capacity bold, free events muted, premium tickets green).
|
|
89
|
+
const eventFormats: FormatRule[] = [
|
|
90
|
+
...statusPills(eventsEntity, 'status'),
|
|
91
|
+
...statusPills(eventsEntity, 'category'),
|
|
92
|
+
{ field: 'capacity', op: 'gte', value: 500, bold: true },
|
|
93
|
+
{ field: 'price', op: 'eq', value: 0, color: '#94a3b8' },
|
|
94
|
+
{ field: 'price', op: 'gte', value: 400, color: '#16a34a', bold: true },
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
const seed = {
|
|
98
|
+
events: [
|
|
99
|
+
{ id: 'ev1', name: 'Svelte Summit 2026', category: 'conference', status: 'published', capacity: 800, price: 490, startAt: '2026-09-12T09:00:00.000Z', topics: ['Svelte', 'Frontend', 'Performance'], color: '#6366f1', website: 'https://sveltesummit.com' },
|
|
100
|
+
{ id: 'ev2', name: 'Grid Performance Workshop', category: 'workshop', status: 'sold_out', capacity: 50, price: 150, startAt: '2026-07-28T13:30:00.000Z', topics: ['Grids', 'Virtualization'], color: '#10b981', website: 'https://svgrid.com/workshop' },
|
|
101
|
+
{ id: 'ev3', name: 'Data Apps Webinar', category: 'webinar', status: 'done', capacity: 500, price: 0, startAt: '2026-05-19T16:00:00.000Z', topics: ['Studio', 'No-code'], color: '#0ea5e9', website: 'https://svgrid.com/webinar' },
|
|
102
|
+
{ id: 'ev4', name: 'Frontend Meetup Berlin', category: 'meetup', status: 'published', capacity: 120, price: 25, startAt: '2026-08-06T18:00:00.000Z', topics: ['Community', 'Svelte'], color: '#f59e0b', website: 'https://frontend.berlin' },
|
|
103
|
+
{ id: 'ev5', name: 'AI in Tooling Conference', category: 'conference', status: 'draft', capacity: 600, price: 420, startAt: '2026-11-03T09:30:00.000Z', topics: ['AI', 'DevTools', 'MCP'], color: '#8b5cf6', website: 'https://aitooling.dev' },
|
|
104
|
+
{ id: 'ev6', name: 'TypeScript Deep Dive', category: 'workshop', status: 'published', capacity: 60, price: 320, startAt: '2026-10-15T10:00:00.000Z', topics: ['TypeScript', 'Types'], color: '#3b82f6', website: 'https://tsdeepdive.io' },
|
|
105
|
+
],
|
|
106
|
+
attendees: [
|
|
107
|
+
{ id: 'at1', name: 'Clara Voss', email: 'clara.voss@nimbus.io', phone: '+1 (415) 555-0110', country: 'United States', company: 'Nimbus', interests: ['Svelte', 'Performance'], vip: true },
|
|
108
|
+
{ id: 'at2', name: 'Diego Martins', email: 'diego.martins@lumen.dev', phone: '+55 11 95555-0111', country: 'Brazil', company: 'Lumen', interests: ['Frontend'], vip: false },
|
|
109
|
+
{ id: 'at3', name: 'Priya Anand', email: 'priya.anand@quanta.co', phone: '+91 80 5550 0112', country: 'India', company: 'Quanta', interests: ['AI', 'DevTools'], vip: false },
|
|
110
|
+
{ id: 'at4', name: 'Felix Braun', email: 'felix.braun@orbit.tech', phone: '+49 30 5550113', country: 'Germany', company: 'Orbit', interests: ['TypeScript', 'Grids'], vip: true },
|
|
111
|
+
{ id: 'at5', name: 'Hana Sato', email: 'hana.sato@meridian.jp', phone: '+81 3 5550 0114', country: 'Japan', company: 'Meridian', interests: ['Community'], vip: false },
|
|
112
|
+
{ id: 'at6', name: 'Lucas Moreau', email: 'lucas.moreau@volta.fr', phone: '+33 1 55 55 0115', country: 'France', company: 'Volta', interests: ['Studio', 'No-code'], vip: false },
|
|
113
|
+
],
|
|
114
|
+
registrations: [
|
|
115
|
+
{ id: 'rg1', eventId: 'ev1', attendeeId: 'at1', ticket: 'vip', amount: 490, discount: 0, status: 'confirmed', registeredAt: '2026-06-01T10:15:00.000Z', rating: 5 },
|
|
116
|
+
{ id: 'rg2', eventId: 'ev1', attendeeId: 'at2', ticket: 'standard', amount: 190, discount: 0, status: 'confirmed', registeredAt: '2026-06-02T14:40:00.000Z', rating: 4 },
|
|
117
|
+
{ id: 'rg3', eventId: 'ev2', attendeeId: 'at3', ticket: 'standard', amount: 150, discount: 0, status: 'waitlist', registeredAt: '2026-06-10T09:05:00.000Z', rating: 3 },
|
|
118
|
+
{ id: 'rg4', eventId: 'ev3', attendeeId: 'at4', ticket: 'student', amount: 0, discount: 100, status: 'confirmed', registeredAt: '2026-04-28T11:20:00.000Z', rating: 5 },
|
|
119
|
+
{ id: 'rg5', eventId: 'ev4', attendeeId: 'at5', ticket: 'standard', amount: 25, discount: 0, status: 'cancelled', registeredAt: '2026-07-01T17:30:00.000Z', rating: 2 },
|
|
120
|
+
{ id: 'rg6', eventId: 'ev6', attendeeId: 'at6', ticket: 'vip', amount: 320, discount: 0, status: 'confirmed', registeredAt: '2026-07-03T08:45:00.000Z', rating: 4 },
|
|
121
|
+
{ id: 'rg7', eventId: 'ev1', attendeeId: 'at5', ticket: 'student', amount: 90, discount: 50, status: 'confirmed', registeredAt: '2026-06-05T20:10:00.000Z', rating: 5 },
|
|
122
|
+
{ id: 'rg8', eventId: 'ev4', attendeeId: 'at2', ticket: 'standard', amount: 25, discount: 0, status: 'confirmed', registeredAt: '2026-07-08T12:00:00.000Z', rating: 4 },
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const events: SampleApp = {
|
|
127
|
+
id: 'events',
|
|
128
|
+
name: 'Events',
|
|
129
|
+
description: 'Events, attendees and registrations - sales + events dashboards (KPI sparklines, revenue target, feedback + capacity gauges, amount-over-time area trend, category x status pivot, tabs), filtered status-pill grids with row actions, master/detail, and rich edit forms (datetime, topic chips, brand color, phone, country, feedback rating, discount slider) with a computed max-revenue field.',
|
|
130
|
+
emoji: '\u{1F39F}\u{FE0F}',
|
|
131
|
+
accent: '#ec4899',
|
|
132
|
+
build: () => {
|
|
133
|
+
const eventRows = pad(eventsEntity, seed.events, 24)
|
|
134
|
+
const attendeeRows = pad(attendees, seed.attendees, 70)
|
|
135
|
+
const registrationRows = pad(registrations, seed.registrations, 90, { eventId: ids(eventRows), attendeeId: ids(attendeeRows) })
|
|
136
|
+
return project({
|
|
137
|
+
title: 'Evently',
|
|
138
|
+
brand: 'Evently',
|
|
139
|
+
accent: '#ec4899',
|
|
140
|
+
preset: 'catppuccin',
|
|
141
|
+
mode: 'dark',
|
|
142
|
+
navStyle: 'top-nav',
|
|
143
|
+
footer: '',
|
|
144
|
+
entities: [eventsEntity, attendees, registrations],
|
|
145
|
+
seed: { events: eventRows, attendees: attendeeRows, registrations: registrationRows },
|
|
146
|
+
screens: [
|
|
147
|
+
// Sales dashboard: registration KPIs (a revenue card with a sparkline over
|
|
148
|
+
// register dates + a $50k target), a feedback gauge, ticket / status
|
|
149
|
+
// breakdowns, an amount-over-time area trend, a ticket x status pivot, a
|
|
150
|
+
// tabbed breakdown, and a status-pill registration grid.
|
|
151
|
+
dashScreen(registrations, { id: 'overview', title: 'Overview', order: 0 }, [
|
|
152
|
+
{ kpi: 'Ticket revenue', measure: 'amount', reduce: 'sum', format: 'currency', trendField: 'registeredAt', trendReduce: 'sum', target: 50000, span: 1 },
|
|
153
|
+
{ kpi: 'Registrations', reduce: 'count', span: 1 },
|
|
154
|
+
{ kpi: 'Avg ticket', measure: 'amount', reduce: 'avg', format: 'currency', span: 1 },
|
|
155
|
+
{ gauge: 'Avg feedback', measure: 'rating', reduce: 'avg', min: 0, max: 5, span: 1 },
|
|
156
|
+
{ chart: 'ticket', measure: 'amount', reduce: 'sum', type: 'bar', span: 2 },
|
|
157
|
+
{ chart: 'status', reduce: 'count', type: 'pie', span: 1 },
|
|
158
|
+
{ chart: 'registeredAt', measure: 'amount', reduce: 'sum', type: 'area', span: 3 },
|
|
159
|
+
{ pivot: { rows: ['ticket'], cols: ['status'], measure: 'amount', aggregate: 'sum' }, span: 3 },
|
|
160
|
+
{ tabs: [
|
|
161
|
+
{ label: 'Revenue by ticket', tiles: [{ chart: 'ticket', measure: 'amount', reduce: 'sum', type: 'bar' }] },
|
|
162
|
+
{ label: 'Registrations by status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
|
|
163
|
+
], span: 3 },
|
|
164
|
+
{ grid: true, format: registrationFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
165
|
+
]),
|
|
166
|
+
// Events dashboard: catalog KPIs (an avg-price card trending over start
|
|
167
|
+
// dates), a capacity gauge, category breakdowns, a category x status pivot, a
|
|
168
|
+
// tabbed view, a filter, and an event grid with status + category pills.
|
|
169
|
+
dashScreen(eventsEntity, { id: 'catalog', title: 'Catalog', order: 1 }, [
|
|
170
|
+
{ kpi: 'Events', reduce: 'count', span: 1 },
|
|
171
|
+
{ kpi: 'Total capacity', measure: 'capacity', reduce: 'sum', format: 'compact', span: 1 },
|
|
172
|
+
{ kpi: 'Avg price', measure: 'price', reduce: 'avg', format: 'currency', trendField: 'startAt', trendReduce: 'avg', span: 1 },
|
|
173
|
+
{ gauge: 'Avg capacity', measure: 'capacity', reduce: 'avg', min: 0, max: 1000, span: 1 },
|
|
174
|
+
{ chart: 'category', measure: 'capacity', reduce: 'sum', type: 'bar', span: 2 },
|
|
175
|
+
{ chart: 'category', reduce: 'count', type: 'pie', span: 1 },
|
|
176
|
+
{ pivot: { rows: ['category'], cols: ['status'], measure: 'price', aggregate: 'avg' }, span: 3 },
|
|
177
|
+
{ tabs: [
|
|
178
|
+
{ label: 'Capacity by category', tiles: [{ chart: 'category', measure: 'capacity', reduce: 'sum', type: 'bar' }] },
|
|
179
|
+
{ label: 'Price by category', tiles: [{ chart: 'category', measure: 'price', reduce: 'avg', type: 'bar' }] },
|
|
180
|
+
], span: 3 },
|
|
181
|
+
{ filter: ['category', 'status'], span: 3 },
|
|
182
|
+
{ grid: true, format: eventFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
183
|
+
]),
|
|
184
|
+
// Schedule: a month calendar placing every event on its start date, tinted
|
|
185
|
+
// by category. The signature "real app" scheduling view.
|
|
186
|
+
calendarScreen(eventsEntity, { id: 'schedule', title: 'Schedule', order: 2 }, { dateField: 'startAt', titleField: 'name', colorField: 'category', filter: ['category', 'status'] }),
|
|
187
|
+
// Event 360: a full record page for one event - header with status pill and
|
|
188
|
+
// capacity / price tiles, a tabbed overview, and a registrations timeline.
|
|
189
|
+
detailScreen(eventsEntity, { id: 'event-detail', title: 'Event detail', order: 3 }, {
|
|
190
|
+
titleField: 'name', statusField: 'status', metricFields: ['capacity', 'price'],
|
|
191
|
+
related: [{ entity: 'registrations', foreignKey: 'eventId', label: 'Registrations', titleField: 'attendeeId', subtitleField: 'amount', dateField: 'registeredAt', statusField: 'status' }],
|
|
192
|
+
}),
|
|
193
|
+
screen(eventsEntity, 'master-detail', { id: 'events', title: 'Events', order: 4, child: registrations, foreignKey: 'eventId', linkScreen: 'event-detail' }),
|
|
194
|
+
formScreen(registrations, { id: 'registrations', title: 'Registrations', order: 5 }, undefined, { format: registrationFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['ticket', 'status']),
|
|
195
|
+
formScreen(attendees, { id: 'attendees', title: 'Attendees', order: 6 }, undefined, {}, ['country', 'company', 'vip']),
|
|
196
|
+
],
|
|
197
|
+
})
|
|
198
|
+
},
|
|
199
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
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 drivers: EntitySchema = {
|
|
6
|
+
name: 'drivers',
|
|
7
|
+
label: 'Driver',
|
|
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: 'phone', type: 'text', input: { editorType: 'phone', help: 'Mobile contact number' } },
|
|
13
|
+
{ field: 'email', type: 'text', format: 'email', required: true, input: { placeholder: 'name@fleetly.com' } },
|
|
14
|
+
{ field: 'license', type: 'text', label: 'License', input: { editorType: 'mask', mask: 'AA-######', help: 'State code + 6 digits' } },
|
|
15
|
+
{ field: 'rating', type: 'number', label: 'Safety rating', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 safety score' } },
|
|
16
|
+
{ field: 'hireDate', type: 'date', label: 'Hire date' },
|
|
17
|
+
],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const vehicles: EntitySchema = {
|
|
21
|
+
name: 'vehicles',
|
|
22
|
+
label: 'Vehicle',
|
|
23
|
+
idField: 'id',
|
|
24
|
+
fields: [
|
|
25
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
26
|
+
{ field: 'name', type: 'text', required: true, minLength: 2 },
|
|
27
|
+
{ field: 'vin', type: 'text', label: 'VIN', input: { editorType: 'mask', mask: '*****************', help: '17-character vehicle identification number' } },
|
|
28
|
+
{ field: 'plate', type: 'text', label: 'Plate', input: { editorType: 'mask', mask: 'AAA-####', help: 'Fleet plate, e.g. FLT-1201' } },
|
|
29
|
+
{ field: 'type', type: 'enum', required: true, defaultValue: 'van', options: [
|
|
30
|
+
{ value: 'van', label: 'Van', color: '#6366f1' },
|
|
31
|
+
{ value: 'truck', label: 'Truck', color: '#10b981' },
|
|
32
|
+
{ value: 'car', label: 'Car', color: '#f59e0b' },
|
|
33
|
+
{ value: 'bike', label: 'Bike', color: '#0ea5e9' },
|
|
34
|
+
] },
|
|
35
|
+
{ field: 'year', type: 'number', label: 'Year', min: 1990, max: 2030 },
|
|
36
|
+
{ field: 'mileage', type: 'number', label: 'Mileage', min: 0, input: { help: 'Odometer reading' } },
|
|
37
|
+
{ field: 'fuelLevel', type: 'number', label: 'Fuel level (%)', min: 0, max: 100, defaultValue: 100, input: { editorType: 'slider', help: 'Current tank level' } },
|
|
38
|
+
{ field: 'status', type: 'enum', required: true, defaultValue: 'available', options: [
|
|
39
|
+
{ value: 'available', label: 'Available', color: '#10b981' },
|
|
40
|
+
{ value: 'on_trip', label: 'On trip', color: '#0ea5e9' },
|
|
41
|
+
{ value: 'maintenance', label: 'Maintenance', color: '#ef4444' },
|
|
42
|
+
] },
|
|
43
|
+
{ field: 'color', type: 'text', label: 'Livery color', input: { editorType: 'color', help: 'Fleet livery / map color' } },
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const trips: EntitySchema = {
|
|
48
|
+
name: 'trips',
|
|
49
|
+
label: 'Trip',
|
|
50
|
+
idField: 'id',
|
|
51
|
+
fields: [
|
|
52
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
53
|
+
{ field: 'vehicleId', type: 'relation', label: 'Vehicle', required: true, relation: { entity: 'vehicles', foreignKey: 'vehicleId', labelField: 'name' } },
|
|
54
|
+
{ field: 'driverId', type: 'relation', label: 'Driver', required: true, relation: { entity: 'drivers', foreignKey: 'driverId', labelField: 'name' } },
|
|
55
|
+
{ field: 'status', type: 'enum', required: true, defaultValue: 'planned', options: [
|
|
56
|
+
{ value: 'planned', label: 'Planned', color: '#94a3b8' },
|
|
57
|
+
{ value: 'ongoing', label: 'Ongoing', color: '#0ea5e9' },
|
|
58
|
+
{ value: 'done', label: 'Done', color: '#10b981' },
|
|
59
|
+
{ value: 'cancelled', label: 'Cancelled', color: '#ef4444' },
|
|
60
|
+
] },
|
|
61
|
+
{ field: 'distanceKm', type: 'number', label: 'Distance (km)', min: 0, input: { help: 'Route distance in kilometers' } },
|
|
62
|
+
{ field: 'cost', type: 'number', label: 'Cost ($)', min: 0 },
|
|
63
|
+
// Derived: cost efficiency per kilometer (display-only, never stored).
|
|
64
|
+
{ field: 'costPerKm', type: 'number', label: 'Cost / km ($)', readonly: true, formula: 'cost / distanceKm' },
|
|
65
|
+
{ field: 'startAt', type: 'datetime', label: 'Start', input: { editorType: 'datetime', help: 'Scheduled departure time' } },
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const seed = {
|
|
70
|
+
drivers: [
|
|
71
|
+
{ id: 'dr1', name: 'Miguel Santos', phone: '+1 (415) 555-0201', email: 'miguel@fleetly.com', license: 'CA-482910', rating: 5, hireDate: '2021-03-15' },
|
|
72
|
+
{ id: 'dr2', name: 'Aisha Khan', phone: '+1 (312) 555-0202', email: 'aisha@fleetly.com', license: 'IL-118273', rating: 4, hireDate: '2022-07-01' },
|
|
73
|
+
{ id: 'dr3', name: 'Tom Becker', phone: '+1 (206) 555-0203', email: 'tom@fleetly.com', license: 'WA-905641', rating: 4, hireDate: '2020-11-20' },
|
|
74
|
+
{ id: 'dr4', name: 'Lena Novak', phone: '+1 (720) 555-0204', email: 'lena@fleetly.com', license: 'CO-337882', rating: 3, hireDate: '2023-02-10' },
|
|
75
|
+
{ id: 'dr5', name: 'Raj Mehta', phone: '+1 (646) 555-0205', email: 'raj@fleetly.com', license: 'NY-556201', rating: 5, hireDate: '2019-09-05' },
|
|
76
|
+
{ id: 'dr6', name: 'Sofia Rossi', phone: '+1 (305) 555-0206', email: 'sofia@fleetly.com', license: 'FL-774519', rating: 4, hireDate: '2024-01-22' },
|
|
77
|
+
],
|
|
78
|
+
vehicles: [
|
|
79
|
+
{ id: 've1', name: 'Sprinter 1', vin: '1HGCM82633A004352', plate: 'FLT-1201', type: 'van', year: 2021, mileage: 82000, fuelLevel: 80, status: 'available', color: '#0284c7' },
|
|
80
|
+
{ id: 've2', name: 'Hauler 2', vin: '2FMDK48C97BB12345', plate: 'FLT-1202', type: 'truck', year: 2019, mileage: 154000, fuelLevel: 45, status: 'on_trip', color: '#dc2626' },
|
|
81
|
+
{ id: 've3', name: 'Sedan 3', vin: '3VWFE21C04M001234', plate: 'FLT-1203', type: 'car', year: 2022, mileage: 41000, fuelLevel: 95, status: 'available', color: '#0f172a' },
|
|
82
|
+
{ id: 've4', name: 'Courier 4', vin: '5UXWX7C5XG0R12345', plate: 'FLT-1204', type: 'bike', year: 2023, mileage: 12000, fuelLevel: 60, status: 'maintenance', color: '#f59e0b' },
|
|
83
|
+
{ id: 've5', name: 'Hauler 5', vin: '1FTFW1ET5DFA12345', plate: 'FLT-1205', type: 'truck', year: 2020, mileage: 132000, fuelLevel: 30, status: 'on_trip', color: '#10b981' },
|
|
84
|
+
{ id: 've6', name: 'Sprinter 6', vin: '1GNEK13ZX3R123456', plate: 'FLT-1206', type: 'van', year: 2021, mileage: 76000, fuelLevel: 70, status: 'available', color: '#6366f1' },
|
|
85
|
+
],
|
|
86
|
+
trips: [
|
|
87
|
+
{ id: 'tp1', vehicleId: 've1', driverId: 'dr1', status: 'done', distanceKm: 120, cost: 180, startAt: '2026-07-08T08:30:00' },
|
|
88
|
+
{ id: 'tp2', vehicleId: 've2', driverId: 'dr2', status: 'ongoing', distanceKm: 340, cost: 520, startAt: '2026-07-12T06:15:00' },
|
|
89
|
+
{ id: 'tp3', vehicleId: 've3', driverId: 'dr3', status: 'planned', distanceKm: 60, cost: 90, startAt: '2026-07-15T09:00:00' },
|
|
90
|
+
{ id: 'tp4', vehicleId: 've4', driverId: 'dr4', status: 'cancelled', distanceKm: 15, cost: 25, startAt: '2026-07-09T14:45:00' },
|
|
91
|
+
{ id: 'tp5', vehicleId: 've5', driverId: 'dr5', status: 'ongoing', distanceKm: 410, cost: 640, startAt: '2026-07-13T05:30:00' },
|
|
92
|
+
{ id: 'tp6', vehicleId: 've1', driverId: 'dr6', status: 'done', distanceKm: 95, cost: 140, startAt: '2026-07-11T11:20:00' },
|
|
93
|
+
{ id: 'tp7', vehicleId: 've2', driverId: 'dr1', status: 'done', distanceKm: 275, cost: 430, startAt: '2026-07-10T07:00:00' },
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Vehicle grid formatting: type + status pills, plus numeric thresholds (low fuel
|
|
98
|
+
// red + bold, healthy fuel green, high-mileage vehicles bold) - data-driven color,
|
|
99
|
+
// not just status pills.
|
|
100
|
+
const vehicleFormats: FormatRule[] = [
|
|
101
|
+
...statusPills(vehicles, 'status'),
|
|
102
|
+
...statusPills(vehicles, 'type'),
|
|
103
|
+
{ field: 'fuelLevel', op: 'lt', value: 25, color: '#dc2626', bold: true },
|
|
104
|
+
{ field: 'fuelLevel', op: 'gte', value: 75, color: '#16a34a' },
|
|
105
|
+
{ field: 'mileage', op: 'gte', value: 150000, bold: true },
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
// Trip grid formatting: status pills, plus expensive trips bold and long-haul
|
|
109
|
+
// distances highlighted.
|
|
110
|
+
const tripFormats: FormatRule[] = [
|
|
111
|
+
...statusPills(trips, 'status'),
|
|
112
|
+
{ field: 'cost', op: 'gte', value: 500, bold: true },
|
|
113
|
+
{ field: 'distanceKm', op: 'gte', value: 300, color: '#0284c7', bold: true },
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
export const fleet: SampleApp = {
|
|
117
|
+
id: 'fleet',
|
|
118
|
+
name: 'Fleet',
|
|
119
|
+
description: 'Vehicles, drivers and trips - fleet + trips dashboards (mileage/cost KPIs with a startAt sparkline, a fuel-level gauge, a type x status pivot, a cost-over-time area trend, tabbed breakdowns), filtered status-pill grids with row actions, master/detail, and rich edit forms (phone, license/VIN/plate masks, safety rating, fuel slider, livery color, datetime).',
|
|
120
|
+
emoji: '\u{1F69A}',
|
|
121
|
+
accent: '#38bdf8',
|
|
122
|
+
build: () => {
|
|
123
|
+
const driverRows = pad(drivers, seed.drivers, 12)
|
|
124
|
+
const vehicleRows = pad(vehicles, seed.vehicles, 14)
|
|
125
|
+
const tripRows = pad(trips, seed.trips, 30, { vehicleId: ids(vehicleRows), driverId: ids(driverRows) })
|
|
126
|
+
return project({
|
|
127
|
+
title: 'Fleetly',
|
|
128
|
+
brand: 'Fleetly',
|
|
129
|
+
accent: '#38bdf8',
|
|
130
|
+
preset: 'carbon',
|
|
131
|
+
mode: 'dark',
|
|
132
|
+
footer: '',
|
|
133
|
+
entities: [drivers, vehicles, trips],
|
|
134
|
+
seed: { drivers: driverRows, vehicles: vehicleRows, trips: tripRows },
|
|
135
|
+
screens: [
|
|
136
|
+
// Fleet dashboard: fleet-size + mileage KPIs, a fuel-level gauge (avg tank
|
|
137
|
+
// 0-100), mileage-by-type + status breakdowns, a type x status pivot, a
|
|
138
|
+
// tabbed view, and a status-pill vehicle grid with inline edit actions.
|
|
139
|
+
dashScreen(vehicles, { id: 'overview', title: 'Fleet', order: 0 }, [
|
|
140
|
+
{ kpi: 'Vehicles', reduce: 'count', span: 1 },
|
|
141
|
+
{ kpi: 'Total mileage', measure: 'mileage', reduce: 'sum', format: 'compact', span: 1 },
|
|
142
|
+
{ kpi: 'Avg mileage', measure: 'mileage', reduce: 'avg', format: 'compact', span: 1 },
|
|
143
|
+
{ gauge: 'Avg fuel level', measure: 'fuelLevel', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
|
|
144
|
+
{ chart: 'type', measure: 'mileage', reduce: 'sum', type: 'bar', span: 2 },
|
|
145
|
+
{ chart: 'status', reduce: 'count', type: 'pie', span: 1 },
|
|
146
|
+
{ pivot: { rows: ['type'], cols: ['status'], aggregate: 'count' }, span: 3 },
|
|
147
|
+
{ tabs: [
|
|
148
|
+
{ label: 'Mileage by type', tiles: [{ chart: 'type', measure: 'mileage', reduce: 'sum', type: 'bar' }] },
|
|
149
|
+
{ label: 'Vehicles by status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
|
|
150
|
+
], span: 3 },
|
|
151
|
+
{ grid: true, format: vehicleFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
152
|
+
]),
|
|
153
|
+
// Trips dashboard: distance + cost KPIs (both trending over start time via a
|
|
154
|
+
// sparkline), an avg-distance gauge, a cost-over-time area trend, a status
|
|
155
|
+
// pie, a tabbed breakdown, a faceted filter, and a status-pill trip grid.
|
|
156
|
+
dashScreen(trips, { id: 'trips-dashboard', title: 'Trips', order: 1 }, [
|
|
157
|
+
{ kpi: 'Trips', reduce: 'count', span: 1 },
|
|
158
|
+
{ kpi: 'Total distance', measure: 'distanceKm', reduce: 'sum', format: 'compact', trendField: 'startAt', trendReduce: 'sum', span: 1 },
|
|
159
|
+
{ kpi: 'Total cost', measure: 'cost', reduce: 'sum', format: 'currency', trendField: 'startAt', trendReduce: 'sum', span: 1 },
|
|
160
|
+
{ kpi: 'Avg cost', measure: 'cost', reduce: 'avg', format: 'currency', span: 1 },
|
|
161
|
+
{ gauge: 'Avg distance (km)', measure: 'distanceKm', reduce: 'avg', min: 0, max: 500, unit: 'km', span: 1 },
|
|
162
|
+
{ chart: 'startAt', measure: 'cost', reduce: 'sum', type: 'area', span: 2 },
|
|
163
|
+
{ chart: 'status', reduce: 'count', type: 'pie', span: 1 },
|
|
164
|
+
{ tabs: [
|
|
165
|
+
{ label: 'Cost by status', tiles: [{ chart: 'status', measure: 'cost', reduce: 'sum', type: 'bar' }] },
|
|
166
|
+
{ label: 'Distance by status', tiles: [{ chart: 'status', measure: 'distanceKm', reduce: 'sum', type: 'bar' }] },
|
|
167
|
+
], span: 3 },
|
|
168
|
+
{ filter: ['status', 'vehicleId', 'driverId'], span: 3 },
|
|
169
|
+
{ grid: true, format: tripFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
|
|
170
|
+
]),
|
|
171
|
+
calendarScreen(trips, { id: 'dispatch', title: 'Dispatch', order: 2 }, { dateField: 'startAt', titleField: 'driverId', colorField: 'status', filter: ['status', 'vehicleId'] }),
|
|
172
|
+
// Vehicle detail: asset header (status pill) + mileage / fuel tiles with a
|
|
173
|
+
// trip-history timeline (each trip titled by its driver, colored by status).
|
|
174
|
+
detailScreen(vehicles, { id: 'vehicle-detail', title: 'Vehicle detail', order: 3 }, {
|
|
175
|
+
titleField: 'name', subtitleField: 'plate', statusField: 'status', metricFields: ['mileage', 'fuelLevel'],
|
|
176
|
+
related: [{ entity: 'trips', foreignKey: 'vehicleId', label: 'Trips', titleField: 'driverId', subtitleField: 'distanceKm', dateField: 'startAt', statusField: 'status' }],
|
|
177
|
+
}),
|
|
178
|
+
screen(vehicles, 'master-detail', { id: 'vehicles', title: 'Vehicles', order: 4, child: trips, foreignKey: 'vehicleId', linkScreen: 'vehicle-detail' }),
|
|
179
|
+
formScreen(trips, { id: 'trip-forms', title: 'Trip forms', order: 5 }, ['vehicleId', 'driverId', 'status'], { format: tripFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['status', 'vehicleId', 'driverId']),
|
|
180
|
+
formScreen(drivers, { id: 'drivers', title: 'Drivers', order: 6 }, ['name', 'phone', 'rating'], { summaries: true }, ['rating']),
|
|
181
|
+
],
|
|
182
|
+
})
|
|
183
|
+
},
|
|
184
|
+
}
|