@contractspec/example.saas-boilerplate 3.7.7 → 3.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +36 -24
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +1 -0
  4. package/dist/browser/index.js +371 -92
  5. package/dist/browser/saas-boilerplate.feature.js +208 -0
  6. package/dist/browser/ui/SaasDashboard.js +311 -60
  7. package/dist/browser/ui/SaasDashboard.visualizations.js +249 -0
  8. package/dist/browser/ui/index.js +362 -92
  9. package/dist/browser/ui/renderers/index.js +229 -3
  10. package/dist/browser/ui/renderers/project-list.markdown.js +229 -3
  11. package/dist/browser/visualizations/catalog.js +155 -0
  12. package/dist/browser/visualizations/index.js +217 -0
  13. package/dist/browser/visualizations/selectors.js +210 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +371 -92
  16. package/dist/node/index.js +371 -92
  17. package/dist/node/saas-boilerplate.feature.js +208 -0
  18. package/dist/node/ui/SaasDashboard.js +311 -60
  19. package/dist/node/ui/SaasDashboard.visualizations.js +249 -0
  20. package/dist/node/ui/index.js +362 -92
  21. package/dist/node/ui/renderers/index.js +229 -3
  22. package/dist/node/ui/renderers/project-list.markdown.js +229 -3
  23. package/dist/node/visualizations/catalog.js +155 -0
  24. package/dist/node/visualizations/index.js +217 -0
  25. package/dist/node/visualizations/selectors.js +210 -0
  26. package/dist/saas-boilerplate.feature.js +208 -0
  27. package/dist/ui/SaasDashboard.js +311 -60
  28. package/dist/ui/SaasDashboard.visualizations.d.ts +5 -0
  29. package/dist/ui/SaasDashboard.visualizations.js +250 -0
  30. package/dist/ui/index.js +362 -92
  31. package/dist/ui/renderers/index.js +229 -3
  32. package/dist/ui/renderers/project-list.markdown.d.ts +1 -1
  33. package/dist/ui/renderers/project-list.markdown.js +229 -3
  34. package/dist/ui/renderers/project-list.renderer.d.ts +1 -1
  35. package/dist/visualizations/catalog.d.ts +11 -0
  36. package/dist/visualizations/catalog.js +156 -0
  37. package/dist/visualizations/index.d.ts +2 -0
  38. package/dist/visualizations/index.js +218 -0
  39. package/dist/visualizations/selectors.d.ts +8 -0
  40. package/dist/visualizations/selectors.js +211 -0
  41. package/dist/visualizations/selectors.test.d.ts +1 -0
  42. package/package.json +70 -13
  43. package/src/index.ts +1 -0
  44. package/src/saas-boilerplate.feature.ts +3 -0
  45. package/src/ui/SaasDashboard.tsx +8 -0
  46. package/src/ui/SaasDashboard.visualizations.tsx +41 -0
  47. package/src/ui/renderers/project-list.markdown.ts +39 -15
  48. package/src/ui/renderers/project-list.renderer.tsx +1 -1
  49. package/src/visualizations/catalog.ts +153 -0
  50. package/src/visualizations/index.ts +2 -0
  51. package/src/visualizations/selectors.test.ts +25 -0
  52. package/src/visualizations/selectors.ts +85 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contractspec/example.saas-boilerplate",
3
- "version": "3.7.7",
3
+ "version": "3.8.4",
4
4
  "description": "SaaS Boilerplate - Users, Orgs, Projects, Billing, Settings",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -327,6 +327,13 @@
327
327
  "node": "./dist/node/ui/SaasDashboard.js",
328
328
  "default": "./dist/ui/SaasDashboard.js"
329
329
  },
330
+ "./ui/SaasDashboard.visualizations": {
331
+ "types": "./dist/ui/SaasDashboard.visualizations.d.ts",
332
+ "browser": "./dist/browser/ui/SaasDashboard.visualizations.js",
333
+ "bun": "./dist/ui/SaasDashboard.visualizations.js",
334
+ "node": "./dist/node/ui/SaasDashboard.visualizations.js",
335
+ "default": "./dist/ui/SaasDashboard.visualizations.js"
336
+ },
330
337
  "./ui/SaasProjectList": {
331
338
  "types": "./dist/ui/SaasProjectList.d.ts",
332
339
  "browser": "./dist/browser/ui/SaasProjectList.js",
@@ -340,6 +347,27 @@
340
347
  "bun": "./dist/ui/SaasSettingsPanel.js",
341
348
  "node": "./dist/node/ui/SaasSettingsPanel.js",
342
349
  "default": "./dist/ui/SaasSettingsPanel.js"
350
+ },
351
+ "./visualizations": {
352
+ "types": "./dist/visualizations/index.d.ts",
353
+ "browser": "./dist/browser/visualizations/index.js",
354
+ "bun": "./dist/visualizations/index.js",
355
+ "node": "./dist/node/visualizations/index.js",
356
+ "default": "./dist/visualizations/index.js"
357
+ },
358
+ "./visualizations/catalog": {
359
+ "types": "./dist/visualizations/catalog.d.ts",
360
+ "browser": "./dist/browser/visualizations/catalog.js",
361
+ "bun": "./dist/visualizations/catalog.js",
362
+ "node": "./dist/node/visualizations/catalog.js",
363
+ "default": "./dist/visualizations/catalog.js"
364
+ },
365
+ "./visualizations/selectors": {
366
+ "types": "./dist/visualizations/selectors.d.ts",
367
+ "browser": "./dist/browser/visualizations/selectors.js",
368
+ "bun": "./dist/visualizations/selectors.js",
369
+ "node": "./dist/node/visualizations/selectors.js",
370
+ "default": "./dist/visualizations/selectors.js"
343
371
  }
344
372
  },
345
373
  "scripts": {
@@ -358,24 +386,25 @@
358
386
  "typecheck": "tsc --noEmit"
359
387
  },
360
388
  "dependencies": {
361
- "@contractspec/lib.identity-rbac": "3.7.7",
362
- "@contractspec/lib.jobs": "3.7.7",
363
- "@contractspec/module.audit-trail": "3.7.7",
364
- "@contractspec/module.notifications": "3.7.7",
365
- "@contractspec/lib.contracts-spec": "4.0.0",
366
- "@contractspec/lib.schema": "3.7.6",
367
- "@contractspec/lib.example-shared-ui": "6.0.7",
368
- "@contractspec/lib.design-system": "3.8.0",
369
- "@contractspec/lib.runtime-sandbox": "2.7.6",
389
+ "@contractspec/lib.identity-rbac": "3.7.12",
390
+ "@contractspec/lib.jobs": "3.7.12",
391
+ "@contractspec/module.audit-trail": "3.7.12",
392
+ "@contractspec/module.notifications": "3.7.12",
393
+ "@contractspec/lib.contracts-spec": "5.0.0",
394
+ "@contractspec/lib.schema": "3.7.10",
395
+ "@contractspec/lib.example-shared-ui": "6.0.12",
396
+ "@contractspec/lib.design-system": "3.8.5",
397
+ "@contractspec/lib.runtime-sandbox": "2.7.10",
370
398
  "react": "19.2.0",
371
- "react-dom": "19.2.0"
399
+ "react-dom": "19.2.0",
400
+ "@contractspec/lib.presentation-runtime-core": "3.9.0"
372
401
  },
373
402
  "devDependencies": {
374
- "@contractspec/tool.typescript": "3.7.6",
403
+ "@contractspec/tool.typescript": "3.7.9",
375
404
  "typescript": "^5.9.3",
376
405
  "@types/react": "^19.2.14",
377
406
  "@types/react-dom": "^19.2.2",
378
- "@contractspec/tool.bun": "3.7.6"
407
+ "@contractspec/tool.bun": "3.7.9"
379
408
  },
380
409
  "publishConfig": {
381
410
  "exports": {
@@ -701,6 +730,13 @@
701
730
  "node": "./dist/node/ui/SaasDashboard.js",
702
731
  "default": "./dist/ui/SaasDashboard.js"
703
732
  },
733
+ "./ui/SaasDashboard.visualizations": {
734
+ "types": "./dist/ui/SaasDashboard.visualizations.d.ts",
735
+ "browser": "./dist/browser/ui/SaasDashboard.visualizations.js",
736
+ "bun": "./dist/ui/SaasDashboard.visualizations.js",
737
+ "node": "./dist/node/ui/SaasDashboard.visualizations.js",
738
+ "default": "./dist/ui/SaasDashboard.visualizations.js"
739
+ },
704
740
  "./ui/SaasProjectList": {
705
741
  "types": "./dist/ui/SaasProjectList.d.ts",
706
742
  "browser": "./dist/browser/ui/SaasProjectList.js",
@@ -714,6 +750,27 @@
714
750
  "bun": "./dist/ui/SaasSettingsPanel.js",
715
751
  "node": "./dist/node/ui/SaasSettingsPanel.js",
716
752
  "default": "./dist/ui/SaasSettingsPanel.js"
753
+ },
754
+ "./visualizations": {
755
+ "types": "./dist/visualizations/index.d.ts",
756
+ "browser": "./dist/browser/visualizations/index.js",
757
+ "bun": "./dist/visualizations/index.js",
758
+ "node": "./dist/node/visualizations/index.js",
759
+ "default": "./dist/visualizations/index.js"
760
+ },
761
+ "./visualizations/catalog": {
762
+ "types": "./dist/visualizations/catalog.d.ts",
763
+ "browser": "./dist/browser/visualizations/catalog.js",
764
+ "bun": "./dist/visualizations/catalog.js",
765
+ "node": "./dist/node/visualizations/catalog.js",
766
+ "default": "./dist/visualizations/catalog.js"
767
+ },
768
+ "./visualizations/selectors": {
769
+ "types": "./dist/visualizations/selectors.d.ts",
770
+ "browser": "./dist/browser/visualizations/selectors.js",
771
+ "bun": "./dist/visualizations/selectors.js",
772
+ "node": "./dist/node/visualizations/selectors.js",
773
+ "default": "./dist/visualizations/selectors.js"
717
774
  }
718
775
  },
719
776
  "registry": "https://registry.npmjs.org/",
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export * from './project';
14
14
  export * from './saas-boilerplate.feature';
15
15
  export * from './settings';
16
16
  export * from './ui';
17
+ export * from './visualizations';
17
18
 
18
19
  // Import docs for registration
19
20
  import './docs';
@@ -4,6 +4,7 @@
4
4
  * Defines the feature module for the SaaS application foundation.
5
5
  */
6
6
  import { defineFeature } from '@contractspec/lib.contracts-spec';
7
+ import { SaasVisualizationRefs } from './visualizations';
7
8
 
8
9
  /**
9
10
  * SaaS Boilerplate feature module that bundles project management,
@@ -102,6 +103,8 @@ export const SaasBoilerplateFeature = defineFeature({
102
103
  },
103
104
  ],
104
105
 
106
+ visualizations: SaasVisualizationRefs,
107
+
105
108
  // Capability requirements
106
109
  capabilities: {
107
110
  requires: [
@@ -30,6 +30,7 @@ import {
30
30
  import { useProjectMutations } from './hooks/useProjectMutations';
31
31
  import { CreateProjectModal } from './modals/CreateProjectModal';
32
32
  import { ProjectActionsModal } from './modals/ProjectActionsModal';
33
+ import { SaasVisualizationOverview } from './SaasDashboard.visualizations';
33
34
 
34
35
  type Tab = 'projects' | 'billing' | 'settings';
35
36
 
@@ -115,6 +116,13 @@ export function SaasDashboard() {
115
116
  </StatCardGroup>
116
117
  )}
117
118
 
119
+ {data && stats && (
120
+ <SaasVisualizationOverview
121
+ projectLimit={stats.projectLimit}
122
+ projects={data.items}
123
+ />
124
+ )}
125
+
118
126
  {/* Navigation Tabs */}
119
127
  <nav className="flex gap-1 rounded-lg bg-muted p-1" role="tablist">
120
128
  {tabs.map((tab) => (
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import {
4
+ VisualizationCard,
5
+ VisualizationGrid,
6
+ } from '@contractspec/lib.design-system';
7
+ import type { Project } from '../handlers/saas.handlers';
8
+ import { createSaasVisualizationItems } from '../visualizations';
9
+
10
+ export function SaasVisualizationOverview({
11
+ projects,
12
+ projectLimit,
13
+ }: {
14
+ projects: Project[];
15
+ projectLimit: number;
16
+ }) {
17
+ const items = createSaasVisualizationItems(projects, projectLimit);
18
+
19
+ return (
20
+ <section className="space-y-3">
21
+ <div>
22
+ <h3 className="font-semibold text-lg">Portfolio Visualizations</h3>
23
+ <p className="text-muted-foreground text-sm">
24
+ Contract-backed charts for project mix, capacity, and activity.
25
+ </p>
26
+ </div>
27
+ <VisualizationGrid>
28
+ {items.map((item) => (
29
+ <VisualizationCard
30
+ key={item.key}
31
+ data={item.data}
32
+ description={item.description}
33
+ height={item.height}
34
+ spec={item.spec}
35
+ title={item.title}
36
+ />
37
+ ))}
38
+ </VisualizationGrid>
39
+ </section>
40
+ );
41
+ }
@@ -3,17 +3,34 @@
3
3
  *
4
4
  * Uses dynamic import to ensure correct build order.
5
5
  */
6
- import type { PresentationRenderer } from '@contractspec/lib.contracts-spec/presentations/transform-engine';
6
+ import type { PresentationRenderer } from '@contractspec/lib.presentation-runtime-core/transform-engine';
7
7
  import {
8
8
  mockGetSubscriptionHandler,
9
9
  mockListProjectsHandler,
10
10
  } from '../../handlers';
11
+ import { createSaasVisualizationItems } from '../../visualizations';
11
12
 
12
- interface ProjectItem {
13
- id: string;
14
- name: string;
15
- status: string;
16
- description?: string;
13
+ type ListProjectsResult = Awaited<ReturnType<typeof mockListProjectsHandler>>;
14
+ type ProjectItem = ListProjectsResult['projects'][number];
15
+ type VisualizationProject = Parameters<
16
+ typeof createSaasVisualizationItems
17
+ >[0][number];
18
+
19
+ const PROJECT_TIERS: VisualizationProject['tier'][] = [
20
+ 'FREE',
21
+ 'PRO',
22
+ 'ENTERPRISE',
23
+ ];
24
+
25
+ function toVisualizationProject(
26
+ project: ProjectItem,
27
+ index: number
28
+ ): VisualizationProject {
29
+ return {
30
+ status: project.status === 'DELETED' ? 'ARCHIVED' : project.status,
31
+ tier: PROJECT_TIERS[index % PROJECT_TIERS.length] ?? 'FREE',
32
+ createdAt: project.createdAt,
33
+ };
17
34
  }
18
35
 
19
36
  /**
@@ -39,11 +56,7 @@ export const projectListMarkdownRenderer: PresentationRenderer<{
39
56
  offset: 0,
40
57
  });
41
58
 
42
- // The example handler returns 'projects', not 'items'
43
- const items =
44
- (data as { projects?: ProjectItem[]; items?: ProjectItem[] }).projects ??
45
- (data as { items?: ProjectItem[] }).items ??
46
- [];
59
+ const items = data.projects ?? [];
47
60
 
48
61
  const lines: string[] = [
49
62
  '# Projects',
@@ -100,12 +113,15 @@ export const saasDashboardMarkdownRenderer: PresentationRenderer<{
100
113
  mockGetSubscriptionHandler(),
101
114
  ]);
102
115
 
103
- const projects =
104
- (projectsData as { projects?: ProjectItem[] }).projects ?? [];
116
+ const projects = projectsData.projects ?? [];
105
117
  const activeProjects = projects.filter((p) => p.status === 'ACTIVE').length;
106
118
  const archivedProjects = projects.filter(
107
119
  (p) => p.status === 'ARCHIVED'
108
120
  ).length;
121
+ const visualizations = createSaasVisualizationItems(
122
+ projects.map(toVisualizationProject),
123
+ 10
124
+ );
109
125
 
110
126
  const lines: string[] = [
111
127
  '# SaaS Dashboard',
@@ -122,10 +138,18 @@ export const saasDashboardMarkdownRenderer: PresentationRenderer<{
122
138
  `| Subscription Plan | ${subscription.planName} |`,
123
139
  `| Subscription Status | ${subscription.status} |`,
124
140
  '',
125
- '## Projects',
126
- '',
127
141
  ];
128
142
 
143
+ lines.push('## Visualization Overview');
144
+ lines.push('');
145
+ for (const item of visualizations) {
146
+ lines.push(`- **${item.title}** via \`${item.spec.meta.key}\``);
147
+ }
148
+
149
+ lines.push('');
150
+ lines.push('## Projects');
151
+ lines.push('');
152
+
129
153
  if (projects.length === 0) {
130
154
  lines.push('_No projects yet._');
131
155
  } else {
@@ -2,7 +2,7 @@
2
2
  * React renderer for SaaS Project List presentation
3
3
  */
4
4
 
5
- import type { PresentationRenderer } from '@contractspec/lib.contracts-spec/presentations/transform-engine';
5
+ import type { PresentationRenderer } from '@contractspec/lib.presentation-runtime-core/transform-engine';
6
6
  import * as React from 'react';
7
7
  import { SaasProjectList } from '../SaasProjectList';
8
8
 
@@ -0,0 +1,153 @@
1
+ import {
2
+ defineVisualization,
3
+ VisualizationRegistry,
4
+ } from '@contractspec/lib.contracts-spec/visualizations';
5
+
6
+ const PROJECT_LIST_REF = {
7
+ key: 'saas.project.list',
8
+ version: '1.0.0',
9
+ } as const;
10
+ const META = {
11
+ version: '1.0.0',
12
+ domain: 'saas',
13
+ stability: 'experimental' as const,
14
+ owners: ['@example.saas-boilerplate'],
15
+ tags: ['saas', 'visualization', 'projects'],
16
+ };
17
+
18
+ export const SaasProjectUsageVisualization = defineVisualization({
19
+ meta: {
20
+ ...META,
21
+ key: 'saas-boilerplate.visualization.project-usage',
22
+ title: 'Project Capacity',
23
+ description: 'Current project count against the current plan limit.',
24
+ goal: 'Show usage against the active plan allowance.',
25
+ context: 'SaaS account overview.',
26
+ },
27
+ source: { primary: PROJECT_LIST_REF, resultPath: 'data' },
28
+ visualization: {
29
+ kind: 'metric',
30
+ measure: 'totalProjects',
31
+ comparisonMeasure: 'projectLimit',
32
+ measures: [
33
+ {
34
+ key: 'totalProjects',
35
+ label: 'Projects',
36
+ dataPath: 'totalProjects',
37
+ format: 'number',
38
+ },
39
+ {
40
+ key: 'projectLimit',
41
+ label: 'Plan Limit',
42
+ dataPath: 'projectLimit',
43
+ format: 'number',
44
+ },
45
+ ],
46
+ table: { caption: 'Current project count and plan limit.' },
47
+ },
48
+ });
49
+
50
+ export const SaasProjectStatusVisualization = defineVisualization({
51
+ meta: {
52
+ ...META,
53
+ key: 'saas-boilerplate.visualization.project-status',
54
+ title: 'Project Status',
55
+ description: 'Distribution of project states.',
56
+ goal: 'Show the mix of active, draft, and archived projects.',
57
+ context: 'Project portfolio overview.',
58
+ },
59
+ source: { primary: PROJECT_LIST_REF, resultPath: 'data' },
60
+ visualization: {
61
+ kind: 'pie',
62
+ nameDimension: 'status',
63
+ valueMeasure: 'projects',
64
+ dimensions: [
65
+ { key: 'status', label: 'Status', dataPath: 'status', type: 'category' },
66
+ ],
67
+ measures: [
68
+ {
69
+ key: 'projects',
70
+ label: 'Projects',
71
+ dataPath: 'projects',
72
+ format: 'number',
73
+ },
74
+ ],
75
+ table: { caption: 'Project counts by status.' },
76
+ },
77
+ });
78
+
79
+ export const SaasProjectTierVisualization = defineVisualization({
80
+ meta: {
81
+ ...META,
82
+ key: 'saas-boilerplate.visualization.project-tiers',
83
+ title: 'Tier Comparison',
84
+ description: 'Distribution of projects across tiers.',
85
+ goal: 'Compare how the current portfolio is distributed by tier.',
86
+ context: 'Plan and packaging overview.',
87
+ },
88
+ source: { primary: PROJECT_LIST_REF, resultPath: 'data' },
89
+ visualization: {
90
+ kind: 'cartesian',
91
+ variant: 'bar',
92
+ xDimension: 'tier',
93
+ yMeasures: ['projects'],
94
+ dimensions: [
95
+ { key: 'tier', label: 'Tier', dataPath: 'tier', type: 'category' },
96
+ ],
97
+ measures: [
98
+ {
99
+ key: 'projects',
100
+ label: 'Projects',
101
+ dataPath: 'projects',
102
+ format: 'number',
103
+ color: '#1d4ed8',
104
+ },
105
+ ],
106
+ table: { caption: 'Project counts by tier.' },
107
+ },
108
+ });
109
+
110
+ export const SaasProjectActivityVisualization = defineVisualization({
111
+ meta: {
112
+ ...META,
113
+ key: 'saas-boilerplate.visualization.project-activity',
114
+ title: 'Recent Project Activity',
115
+ description: 'Daily project creation activity.',
116
+ goal: 'Show recent project activity over time.',
117
+ context: 'Project portfolio trend view.',
118
+ },
119
+ source: { primary: PROJECT_LIST_REF, resultPath: 'data' },
120
+ visualization: {
121
+ kind: 'cartesian',
122
+ variant: 'line',
123
+ xDimension: 'day',
124
+ yMeasures: ['projects'],
125
+ dimensions: [{ key: 'day', label: 'Day', dataPath: 'day', type: 'time' }],
126
+ measures: [
127
+ {
128
+ key: 'projects',
129
+ label: 'Projects',
130
+ dataPath: 'projects',
131
+ format: 'number',
132
+ color: '#0f766e',
133
+ },
134
+ ],
135
+ table: { caption: 'Daily project creation counts.' },
136
+ },
137
+ });
138
+
139
+ export const SaasVisualizationSpecs = [
140
+ SaasProjectUsageVisualization,
141
+ SaasProjectStatusVisualization,
142
+ SaasProjectTierVisualization,
143
+ SaasProjectActivityVisualization,
144
+ ] as const;
145
+
146
+ export const SaasVisualizationRegistry = new VisualizationRegistry([
147
+ ...SaasVisualizationSpecs,
148
+ ]);
149
+
150
+ export const SaasVisualizationRefs = SaasVisualizationSpecs.map((spec) => ({
151
+ key: spec.meta.key,
152
+ version: spec.meta.version,
153
+ }));
@@ -0,0 +1,2 @@
1
+ export * from './catalog';
2
+ export * from './selectors';
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createSaasVisualizationItems } from './selectors';
3
+
4
+ describe('saas visualization selectors', () => {
5
+ it('creates dashboard visualization items', () => {
6
+ const items = createSaasVisualizationItems(
7
+ [
8
+ {
9
+ status: 'ACTIVE',
10
+ tier: 'PRO',
11
+ createdAt: '2026-03-18T10:00:00Z',
12
+ },
13
+ {
14
+ status: 'DRAFT',
15
+ tier: 'FREE',
16
+ createdAt: '2026-03-19T10:00:00Z',
17
+ },
18
+ ],
19
+ 10
20
+ );
21
+
22
+ expect(items).toHaveLength(4);
23
+ expect(items[0]?.title).toBe('Project Capacity');
24
+ });
25
+ });
@@ -0,0 +1,85 @@
1
+ import type { VisualizationSurfaceItem } from '@contractspec/lib.design-system';
2
+ import type { Project } from '../handlers/saas.handlers';
3
+ import {
4
+ SaasProjectActivityVisualization,
5
+ SaasProjectStatusVisualization,
6
+ SaasProjectTierVisualization,
7
+ SaasProjectUsageVisualization,
8
+ } from './catalog';
9
+
10
+ type DateLike = Date | string;
11
+
12
+ interface ProjectLike extends Pick<Project, 'status' | 'tier'> {
13
+ createdAt: DateLike;
14
+ }
15
+
16
+ function toDayKey(value: DateLike): string {
17
+ const date = value instanceof Date ? value : new Date(value);
18
+ return date.toISOString().slice(0, 10);
19
+ }
20
+
21
+ export function createSaasVisualizationItems(
22
+ projects: ProjectLike[],
23
+ projectLimit = 10
24
+ ): VisualizationSurfaceItem[] {
25
+ const statusCounts = new Map<string, number>();
26
+ const tierCounts = new Map<string, number>();
27
+ const activityCounts = new Map<string, number>();
28
+
29
+ for (const project of projects) {
30
+ statusCounts.set(
31
+ project.status,
32
+ (statusCounts.get(project.status) ?? 0) + 1
33
+ );
34
+ tierCounts.set(project.tier, (tierCounts.get(project.tier) ?? 0) + 1);
35
+ const day = toDayKey(project.createdAt);
36
+ activityCounts.set(day, (activityCounts.get(day) ?? 0) + 1);
37
+ }
38
+
39
+ return [
40
+ {
41
+ key: 'saas-capacity',
42
+ spec: SaasProjectUsageVisualization,
43
+ data: { data: [{ totalProjects: projects.length, projectLimit }] },
44
+ title: 'Project Capacity',
45
+ description: 'Current project count compared to the active limit.',
46
+ height: 220,
47
+ },
48
+ {
49
+ key: 'saas-status',
50
+ spec: SaasProjectStatusVisualization,
51
+ data: {
52
+ data: Array.from(statusCounts.entries()).map(([status, count]) => ({
53
+ status,
54
+ projects: count,
55
+ })),
56
+ },
57
+ title: 'Project Status',
58
+ description: 'Status mix across the current project portfolio.',
59
+ height: 260,
60
+ },
61
+ {
62
+ key: 'saas-tier',
63
+ spec: SaasProjectTierVisualization,
64
+ data: {
65
+ data: Array.from(tierCounts.entries()).map(([tier, count]) => ({
66
+ tier,
67
+ projects: count,
68
+ })),
69
+ },
70
+ title: 'Tier Comparison',
71
+ description: 'How projects are distributed across tiers.',
72
+ },
73
+ {
74
+ key: 'saas-activity',
75
+ spec: SaasProjectActivityVisualization,
76
+ data: {
77
+ data: Array.from(activityCounts.entries())
78
+ .sort(([left], [right]) => left.localeCompare(right))
79
+ .map(([day, count]) => ({ day, projects: count })),
80
+ },
81
+ title: 'Recent Project Activity',
82
+ description: 'Daily project creation activity.',
83
+ },
84
+ ];
85
+ }