@open-mercato/core 0.4.2-canary-cf7d9b4116 → 0.4.2-canary-e6bf6a353e
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/modules/auth/lib/setup-app.js +2 -0
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/catalog/analytics.js +27 -0
- package/dist/modules/catalog/analytics.js.map +7 -0
- package/dist/modules/customers/analytics.js +50 -0
- package/dist/modules/customers/analytics.js.map +7 -0
- package/dist/modules/dashboards/acl.js +2 -1
- package/dist/modules/dashboards/acl.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +187 -0
- package/dist/modules/dashboards/api/widgets/data/route.js.map +7 -0
- package/dist/modules/dashboards/cli.js +142 -1
- package/dist/modules/dashboards/cli.js.map +2 -2
- package/dist/modules/dashboards/di.js +11 -0
- package/dist/modules/dashboards/di.js.map +7 -0
- package/dist/modules/dashboards/lib/aggregations.js +162 -0
- package/dist/modules/dashboards/lib/aggregations.js.map +7 -0
- package/dist/modules/dashboards/lib/formatters.js +34 -0
- package/dist/modules/dashboards/lib/formatters.js.map +7 -0
- package/dist/modules/dashboards/seed/analytics.js +383 -0
- package/dist/modules/dashboards/seed/analytics.js.map +7 -0
- package/dist/modules/dashboards/services/analyticsRegistry.js +52 -0
- package/dist/modules/dashboards/services/analyticsRegistry.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +207 -0
- package/dist/modules/dashboards/services/widgetDataService.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +128 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +126 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +151 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +126 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +16 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +123 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +128 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js +21 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +211 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js +19 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +131 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js +19 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +153 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/config.js +22 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +180 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js.map +7 -0
- package/dist/modules/sales/analytics.js +67 -0
- package/dist/modules/sales/analytics.js.map +7 -0
- package/package.json +2 -2
- package/src/modules/auth/lib/setup-app.ts +2 -0
- package/src/modules/catalog/analytics.ts +24 -0
- package/src/modules/customers/analytics.ts +47 -0
- package/src/modules/dashboards/acl.ts +1 -0
- package/src/modules/dashboards/api/widgets/data/route.ts +221 -0
- package/src/modules/dashboards/cli.ts +164 -1
- package/src/modules/dashboards/di.ts +9 -0
- package/src/modules/dashboards/i18n/de.json +115 -1
- package/src/modules/dashboards/i18n/en.json +115 -1
- package/src/modules/dashboards/i18n/es.json +115 -1
- package/src/modules/dashboards/i18n/pl.json +115 -1
- package/src/modules/dashboards/lib/__tests__/aggregations.test.ts +327 -0
- package/src/modules/dashboards/lib/__tests__/formatters.test.ts +128 -0
- package/src/modules/dashboards/lib/aggregations.ts +225 -0
- package/src/modules/dashboards/lib/formatters.ts +36 -0
- package/src/modules/dashboards/seed/analytics.ts +405 -0
- package/src/modules/dashboards/services/analyticsRegistry.ts +79 -0
- package/src/modules/dashboards/services/widgetDataService.ts +329 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +135 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +133 -0
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +154 -0
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +133 -0
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +17 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +137 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +135 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/config.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +220 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/config.ts +21 -0
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +131 -0
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/top-customers/config.ts +21 -0
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +161 -0
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/top-products/config.ts +27 -0
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +181 -0
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.ts +24 -0
- package/src/modules/sales/analytics.ts +64 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type FormatCurrencyOptions = {
|
|
2
|
+
currency?: string
|
|
3
|
+
minimumFractionDigits?: number
|
|
4
|
+
maximumFractionDigits?: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatCurrency(value: number, options: FormatCurrencyOptions = {}): string {
|
|
8
|
+
const { currency = 'USD', minimumFractionDigits = 0, maximumFractionDigits = 0 } = options
|
|
9
|
+
return new Intl.NumberFormat(undefined, {
|
|
10
|
+
style: 'currency',
|
|
11
|
+
currency,
|
|
12
|
+
minimumFractionDigits,
|
|
13
|
+
maximumFractionDigits,
|
|
14
|
+
}).format(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatCurrencyWithDecimals(value: number, options: FormatCurrencyOptions = {}): string {
|
|
18
|
+
return formatCurrency(value, { minimumFractionDigits: 2, maximumFractionDigits: 2, ...options })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatCurrencyCompact(value: number, currencySymbol = '$'): string {
|
|
22
|
+
if (Math.abs(value) >= 1_000_000) {
|
|
23
|
+
return `${currencySymbol}${(value / 1_000_000).toFixed(1)}M`
|
|
24
|
+
}
|
|
25
|
+
if (Math.abs(value) >= 1_000) {
|
|
26
|
+
return `${currencySymbol}${(value / 1_000).toFixed(1)}K`
|
|
27
|
+
}
|
|
28
|
+
return `${currencySymbol}${value.toFixed(0)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatCurrencySafe(value: unknown, fallback = '--'): string {
|
|
32
|
+
if (value === null || value === undefined) return fallback
|
|
33
|
+
const num = Number(value)
|
|
34
|
+
if (!Number.isFinite(num)) return fallback
|
|
35
|
+
return formatCurrency(num)
|
|
36
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import {
|
|
4
|
+
SalesOrder,
|
|
5
|
+
SalesOrderLine,
|
|
6
|
+
} from '@open-mercato/core/modules/sales/data/entities'
|
|
7
|
+
import {
|
|
8
|
+
CustomerEntity,
|
|
9
|
+
CustomerCompanyProfile,
|
|
10
|
+
CustomerDeal,
|
|
11
|
+
} from '@open-mercato/core/modules/customers/data/entities'
|
|
12
|
+
import {
|
|
13
|
+
CatalogProduct,
|
|
14
|
+
CatalogProductVariant,
|
|
15
|
+
} from '@open-mercato/core/modules/catalog/data/entities'
|
|
16
|
+
|
|
17
|
+
export type AnalyticsSeedScope = {
|
|
18
|
+
tenantId: string
|
|
19
|
+
organizationId: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type AnalyticsSeedOptions = {
|
|
23
|
+
months?: number
|
|
24
|
+
ordersPerMonth?: number
|
|
25
|
+
customersCount?: number
|
|
26
|
+
productsCount?: number
|
|
27
|
+
dealsCount?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ORDER_STATUSES = ['draft', 'pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled'] as const
|
|
31
|
+
const FULFILLMENT_STATUSES = ['pending', 'in_fulfillment', 'partially_fulfilled', 'fulfilled'] as const
|
|
32
|
+
const PAYMENT_STATUSES = ['unpaid', 'partial', 'paid', 'refunded'] as const
|
|
33
|
+
const DEAL_PIPELINE_STAGES = ['lead', 'qualified', 'proposal', 'negotiation', 'closed_won', 'closed_lost'] as const
|
|
34
|
+
const COUNTRIES = ['US', 'GB', 'DE', 'FR', 'CA', 'AU', 'NL', 'ES', 'IT', 'PL'] as const
|
|
35
|
+
const REGIONS_BY_COUNTRY: Record<string, string[]> = {
|
|
36
|
+
US: ['California', 'New York', 'Texas', 'Florida', 'Illinois', 'Washington', 'Massachusetts'],
|
|
37
|
+
GB: ['England', 'Scotland', 'Wales'],
|
|
38
|
+
DE: ['Bavaria', 'Berlin', 'Hamburg', 'Hessen'],
|
|
39
|
+
FR: ['Île-de-France', 'Provence', 'Rhône-Alpes'],
|
|
40
|
+
CA: ['Ontario', 'Quebec', 'British Columbia'],
|
|
41
|
+
AU: ['New South Wales', 'Victoria', 'Queensland'],
|
|
42
|
+
NL: ['North Holland', 'South Holland'],
|
|
43
|
+
ES: ['Madrid', 'Catalonia', 'Andalusia'],
|
|
44
|
+
IT: ['Lombardy', 'Lazio', 'Veneto'],
|
|
45
|
+
PL: ['Mazovia', 'Lesser Poland', 'Silesia'],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const COMPANY_NAMES = [
|
|
49
|
+
'Acme Corp', 'Global Industries', 'Tech Solutions', 'Prime Services',
|
|
50
|
+
'Northern Analytics', 'Blue Ocean Trading', 'Summit Enterprises', 'Horizon Dynamics',
|
|
51
|
+
'Vertex Systems', 'Atlas Logistics', 'Pinnacle Group', 'Quantum Labs',
|
|
52
|
+
'Stellar Innovations', 'Pacific Partners', 'Apex Manufacturing', 'Nexus Technologies',
|
|
53
|
+
'Eclipse Ventures', 'Titan Holdings', 'Vanguard Solutions', 'Momentum Corp',
|
|
54
|
+
'Crystal Clear Media', 'Silver Line Transport', 'Golden Gate Imports', 'Red Rock Mining',
|
|
55
|
+
'Green Valley Foods', 'Blue Sky Aviation', 'White Mountain Retail', 'Black Diamond Sports',
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const PRODUCT_NAMES = [
|
|
59
|
+
'Premium Widget', 'Standard Component', 'Professional Kit', 'Enterprise Module',
|
|
60
|
+
'Basic Starter Pack', 'Advanced System', 'Deluxe Bundle', 'Essential Tools',
|
|
61
|
+
'Pro Series Device', 'Ultra Performance Unit', 'Classic Edition', 'Limited Series',
|
|
62
|
+
'Industrial Grade Part', 'Consumer Package', 'Business Solution', 'Home Edition',
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const DEAL_TITLES = [
|
|
66
|
+
'Enterprise License Deal', 'Annual Subscription', 'Pilot Program', 'Strategic Partnership',
|
|
67
|
+
'Volume Purchase Agreement', 'Service Contract', 'Implementation Project', 'Expansion Deal',
|
|
68
|
+
'Renewal Opportunity', 'Upsell Initiative', 'Cross-sell Package', 'Custom Solution',
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
function randomInt(min: number, max: number): number {
|
|
72
|
+
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function randomFloat(min: number, max: number, decimals = 2): number {
|
|
76
|
+
const value = Math.random() * (max - min) + min
|
|
77
|
+
return Number(value.toFixed(decimals))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function randomElement<T>(arr: readonly T[]): T {
|
|
81
|
+
return arr[Math.floor(Math.random() * arr.length)]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function randomElements<T>(arr: readonly T[], count: number): T[] {
|
|
85
|
+
const shuffled = [...arr].sort(() => Math.random() - 0.5)
|
|
86
|
+
return shuffled.slice(0, Math.min(count, arr.length))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toAmount(value: number): string {
|
|
90
|
+
return value.toFixed(2)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function daysAgo(days: number): Date {
|
|
94
|
+
const date = new Date()
|
|
95
|
+
date.setDate(date.getDate() - days)
|
|
96
|
+
return date
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function randomDateInRange(startDaysAgo: number, endDaysAgo: number): Date {
|
|
100
|
+
const daysOffset = randomInt(endDaysAgo, startDaysAgo)
|
|
101
|
+
return daysAgo(daysOffset)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function generateOrderNumber(index: number): string {
|
|
105
|
+
return `SO-ANALYTICS-${String(index).padStart(5, '0')}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function seedAnalyticsData(
|
|
109
|
+
em: EntityManager,
|
|
110
|
+
scope: AnalyticsSeedScope,
|
|
111
|
+
options: AnalyticsSeedOptions = {}
|
|
112
|
+
): Promise<{ orders: number; customers: number; products: number; deals: number }> {
|
|
113
|
+
const {
|
|
114
|
+
months = 6,
|
|
115
|
+
ordersPerMonth = 50,
|
|
116
|
+
customersCount = 25,
|
|
117
|
+
productsCount = 15,
|
|
118
|
+
dealsCount = 20,
|
|
119
|
+
} = options
|
|
120
|
+
|
|
121
|
+
const existingOrders = await em.count(SalesOrder, {
|
|
122
|
+
tenantId: scope.tenantId,
|
|
123
|
+
organizationId: scope.organizationId,
|
|
124
|
+
orderNumber: { $like: 'SO-ANALYTICS-%' },
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (existingOrders > 0) {
|
|
128
|
+
return { orders: 0, customers: 0, products: 0, deals: 0 }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const customers: CustomerEntity[] = []
|
|
132
|
+
const products: CatalogProduct[] = []
|
|
133
|
+
const variants: CatalogProductVariant[] = []
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < customersCount; i++) {
|
|
136
|
+
const companyName = COMPANY_NAMES[i % COMPANY_NAMES.length]
|
|
137
|
+
const customerCreatedAt = randomDateInRange(months * 30 + 60, 0)
|
|
138
|
+
|
|
139
|
+
const customer = em.create(CustomerEntity, {
|
|
140
|
+
id: randomUUID(),
|
|
141
|
+
organizationId: scope.organizationId,
|
|
142
|
+
tenantId: scope.tenantId,
|
|
143
|
+
kind: 'company',
|
|
144
|
+
displayName: `${companyName} #${i + 1}`,
|
|
145
|
+
primaryEmail: `contact${i + 1}@${companyName.toLowerCase().replace(/\s+/g, '')}.example.com`,
|
|
146
|
+
status: 'active',
|
|
147
|
+
lifecycleStage: randomElement(['lead', 'customer', 'opportunity']),
|
|
148
|
+
isActive: true,
|
|
149
|
+
createdAt: customerCreatedAt,
|
|
150
|
+
updatedAt: customerCreatedAt,
|
|
151
|
+
})
|
|
152
|
+
em.persist(customer)
|
|
153
|
+
customers.push(customer)
|
|
154
|
+
|
|
155
|
+
const companyProfile = em.create(CustomerCompanyProfile, {
|
|
156
|
+
id: randomUUID(),
|
|
157
|
+
organizationId: scope.organizationId,
|
|
158
|
+
tenantId: scope.tenantId,
|
|
159
|
+
entity: customer,
|
|
160
|
+
legalName: `${companyName} Inc.`,
|
|
161
|
+
brandName: companyName,
|
|
162
|
+
industry: randomElement(['Technology', 'Manufacturing', 'Retail', 'Services', 'Healthcare']),
|
|
163
|
+
sizeBucket: randomElement(['small', 'medium', 'large', 'enterprise']),
|
|
164
|
+
annualRevenue: toAmount(randomFloat(100000, 50000000)),
|
|
165
|
+
createdAt: customerCreatedAt,
|
|
166
|
+
updatedAt: customerCreatedAt,
|
|
167
|
+
})
|
|
168
|
+
em.persist(companyProfile)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < productsCount; i++) {
|
|
172
|
+
const productName = PRODUCT_NAMES[i % PRODUCT_NAMES.length]
|
|
173
|
+
const productCreatedAt = daysAgo(months * 30 + randomInt(0, 30))
|
|
174
|
+
|
|
175
|
+
const product = em.create(CatalogProduct, {
|
|
176
|
+
id: randomUUID(),
|
|
177
|
+
organizationId: scope.organizationId,
|
|
178
|
+
tenantId: scope.tenantId,
|
|
179
|
+
title: `${productName} ${i + 1}`,
|
|
180
|
+
handle: `analytics-product-${i + 1}`,
|
|
181
|
+
sku: `SKU-ANALYTICS-${String(i + 1).padStart(3, '0')}`,
|
|
182
|
+
productType: 'simple',
|
|
183
|
+
isConfigurable: false,
|
|
184
|
+
isActive: true,
|
|
185
|
+
createdAt: productCreatedAt,
|
|
186
|
+
updatedAt: productCreatedAt,
|
|
187
|
+
})
|
|
188
|
+
em.persist(product)
|
|
189
|
+
products.push(product)
|
|
190
|
+
|
|
191
|
+
const variant = em.create(CatalogProductVariant, {
|
|
192
|
+
id: randomUUID(),
|
|
193
|
+
organizationId: scope.organizationId,
|
|
194
|
+
tenantId: scope.tenantId,
|
|
195
|
+
product,
|
|
196
|
+
name: 'Default',
|
|
197
|
+
sku: `${product.sku}-DEFAULT`,
|
|
198
|
+
isDefault: true,
|
|
199
|
+
isActive: true,
|
|
200
|
+
createdAt: productCreatedAt,
|
|
201
|
+
updatedAt: productCreatedAt,
|
|
202
|
+
})
|
|
203
|
+
em.persist(variant)
|
|
204
|
+
variants.push(variant)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let orderIndex = 1
|
|
208
|
+
const totalDays = months * 30
|
|
209
|
+
const orders: SalesOrder[] = []
|
|
210
|
+
|
|
211
|
+
for (let dayOffset = totalDays; dayOffset >= 0; dayOffset--) {
|
|
212
|
+
const ordersToday = Math.round(ordersPerMonth / 30 * randomFloat(0.5, 1.5))
|
|
213
|
+
|
|
214
|
+
for (let j = 0; j < ordersToday; j++) {
|
|
215
|
+
const orderDate = daysAgo(dayOffset)
|
|
216
|
+
const customer = randomElement(customers)
|
|
217
|
+
const country = randomElement(COUNTRIES)
|
|
218
|
+
const region = randomElement(REGIONS_BY_COUNTRY[country] || [''])
|
|
219
|
+
|
|
220
|
+
const lineCount = randomInt(1, 5)
|
|
221
|
+
const selectedProducts = randomElements(products, lineCount)
|
|
222
|
+
|
|
223
|
+
let subtotalNet = 0
|
|
224
|
+
let subtotalGross = 0
|
|
225
|
+
let taxTotal = 0
|
|
226
|
+
|
|
227
|
+
const orderLines: Array<{
|
|
228
|
+
product: CatalogProduct
|
|
229
|
+
variant: CatalogProductVariant
|
|
230
|
+
quantity: number
|
|
231
|
+
unitPriceNet: number
|
|
232
|
+
unitPriceGross: number
|
|
233
|
+
taxRate: number
|
|
234
|
+
lineNetAmount: number
|
|
235
|
+
lineGrossAmount: number
|
|
236
|
+
lineTaxAmount: number
|
|
237
|
+
}> = []
|
|
238
|
+
|
|
239
|
+
for (let k = 0; k < selectedProducts.length; k++) {
|
|
240
|
+
const product = selectedProducts[k]
|
|
241
|
+
const variant = variants.find((v) => v.product.id === product.id) || variants[0]
|
|
242
|
+
const quantity = randomInt(1, 10)
|
|
243
|
+
const unitPriceNet = randomFloat(10, 500)
|
|
244
|
+
const taxRate = randomElement([0, 5, 10, 20, 23])
|
|
245
|
+
const unitPriceGross = unitPriceNet * (1 + taxRate / 100)
|
|
246
|
+
const lineNetAmount = unitPriceNet * quantity
|
|
247
|
+
const lineGrossAmount = unitPriceGross * quantity
|
|
248
|
+
const lineTaxAmount = lineGrossAmount - lineNetAmount
|
|
249
|
+
|
|
250
|
+
subtotalNet += lineNetAmount
|
|
251
|
+
subtotalGross += lineGrossAmount
|
|
252
|
+
taxTotal += lineTaxAmount
|
|
253
|
+
|
|
254
|
+
orderLines.push({
|
|
255
|
+
product,
|
|
256
|
+
variant,
|
|
257
|
+
quantity,
|
|
258
|
+
unitPriceNet,
|
|
259
|
+
unitPriceGross,
|
|
260
|
+
taxRate,
|
|
261
|
+
lineNetAmount,
|
|
262
|
+
lineGrossAmount,
|
|
263
|
+
lineTaxAmount,
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const order = em.create(SalesOrder, {
|
|
268
|
+
id: randomUUID(),
|
|
269
|
+
organizationId: scope.organizationId,
|
|
270
|
+
tenantId: scope.tenantId,
|
|
271
|
+
orderNumber: generateOrderNumber(orderIndex++),
|
|
272
|
+
status: randomElement(ORDER_STATUSES),
|
|
273
|
+
fulfillmentStatus: randomElement(FULFILLMENT_STATUSES),
|
|
274
|
+
paymentStatus: randomElement(PAYMENT_STATUSES),
|
|
275
|
+
customerEntityId: customer.id,
|
|
276
|
+
customerSnapshot: {
|
|
277
|
+
customer: {
|
|
278
|
+
id: customer.id,
|
|
279
|
+
kind: customer.kind,
|
|
280
|
+
displayName: customer.displayName,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
currencyCode: 'USD',
|
|
284
|
+
placedAt: orderDate,
|
|
285
|
+
shippingAddressSnapshot: {
|
|
286
|
+
country,
|
|
287
|
+
region,
|
|
288
|
+
city: `City ${randomInt(1, 100)}`,
|
|
289
|
+
postalCode: String(randomInt(10000, 99999)),
|
|
290
|
+
},
|
|
291
|
+
billingAddressSnapshot: {
|
|
292
|
+
country,
|
|
293
|
+
region,
|
|
294
|
+
city: `City ${randomInt(1, 100)}`,
|
|
295
|
+
postalCode: String(randomInt(10000, 99999)),
|
|
296
|
+
},
|
|
297
|
+
subtotalNetAmount: toAmount(subtotalNet),
|
|
298
|
+
subtotalGrossAmount: toAmount(subtotalGross),
|
|
299
|
+
discountTotalAmount: '0.00',
|
|
300
|
+
taxTotalAmount: toAmount(taxTotal),
|
|
301
|
+
shippingNetAmount: '0.00',
|
|
302
|
+
shippingGrossAmount: '0.00',
|
|
303
|
+
surchargeTotalAmount: '0.00',
|
|
304
|
+
grandTotalNetAmount: toAmount(subtotalNet),
|
|
305
|
+
grandTotalGrossAmount: toAmount(subtotalGross),
|
|
306
|
+
paidTotalAmount: '0.00',
|
|
307
|
+
refundedTotalAmount: '0.00',
|
|
308
|
+
outstandingAmount: toAmount(subtotalGross),
|
|
309
|
+
lineItemCount: orderLines.length,
|
|
310
|
+
metadata: { seed: 'dashboards.analytics' },
|
|
311
|
+
createdAt: orderDate,
|
|
312
|
+
updatedAt: orderDate,
|
|
313
|
+
})
|
|
314
|
+
em.persist(order)
|
|
315
|
+
orders.push(order)
|
|
316
|
+
|
|
317
|
+
for (let k = 0; k < orderLines.length; k++) {
|
|
318
|
+
const lineData = orderLines[k]
|
|
319
|
+
const line = em.create(SalesOrderLine, {
|
|
320
|
+
id: randomUUID(),
|
|
321
|
+
order,
|
|
322
|
+
organizationId: scope.organizationId,
|
|
323
|
+
tenantId: scope.tenantId,
|
|
324
|
+
lineNumber: k + 1,
|
|
325
|
+
kind: 'product',
|
|
326
|
+
name: lineData.product.title,
|
|
327
|
+
quantity: toAmount(lineData.quantity),
|
|
328
|
+
currencyCode: 'USD',
|
|
329
|
+
unitPriceNet: toAmount(lineData.unitPriceNet),
|
|
330
|
+
unitPriceGross: toAmount(lineData.unitPriceGross),
|
|
331
|
+
discountAmount: '0.00',
|
|
332
|
+
discountPercent: '0.00',
|
|
333
|
+
taxRate: toAmount(lineData.taxRate),
|
|
334
|
+
taxAmount: toAmount(lineData.lineTaxAmount),
|
|
335
|
+
totalNetAmount: toAmount(lineData.lineNetAmount),
|
|
336
|
+
totalGrossAmount: toAmount(lineData.lineGrossAmount),
|
|
337
|
+
reservedQuantity: '0',
|
|
338
|
+
fulfilledQuantity: '0',
|
|
339
|
+
invoicedQuantity: '0',
|
|
340
|
+
returnedQuantity: '0',
|
|
341
|
+
productId: lineData.product.id,
|
|
342
|
+
productVariantId: lineData.variant?.id ?? null,
|
|
343
|
+
catalogSnapshot: {
|
|
344
|
+
product: {
|
|
345
|
+
id: lineData.product.id,
|
|
346
|
+
title: lineData.product.title,
|
|
347
|
+
sku: lineData.product.sku,
|
|
348
|
+
},
|
|
349
|
+
variant: lineData.variant
|
|
350
|
+
? {
|
|
351
|
+
id: lineData.variant.id,
|
|
352
|
+
name: lineData.variant.name,
|
|
353
|
+
sku: lineData.variant.sku,
|
|
354
|
+
}
|
|
355
|
+
: null,
|
|
356
|
+
},
|
|
357
|
+
createdAt: orderDate,
|
|
358
|
+
updatedAt: orderDate,
|
|
359
|
+
})
|
|
360
|
+
em.persist(line)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (let i = 0; i < dealsCount; i++) {
|
|
366
|
+
const customer = randomElement(customers)
|
|
367
|
+
const dealCreatedAt = randomDateInRange(months * 30, 0)
|
|
368
|
+
const pipelineStage = randomElement(DEAL_PIPELINE_STAGES)
|
|
369
|
+
|
|
370
|
+
const probabilityByStage: Record<string, number> = {
|
|
371
|
+
lead: 10,
|
|
372
|
+
qualified: 25,
|
|
373
|
+
proposal: 50,
|
|
374
|
+
negotiation: 75,
|
|
375
|
+
closed_won: 100,
|
|
376
|
+
closed_lost: 0,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const deal = em.create(CustomerDeal, {
|
|
380
|
+
id: randomUUID(),
|
|
381
|
+
organizationId: scope.organizationId,
|
|
382
|
+
tenantId: scope.tenantId,
|
|
383
|
+
title: `${randomElement(DEAL_TITLES)} - ${customer.displayName}`,
|
|
384
|
+
status: pipelineStage === 'closed_won' || pipelineStage === 'closed_lost' ? 'closed' : 'open',
|
|
385
|
+
pipelineStage,
|
|
386
|
+
valueAmount: toAmount(randomFloat(5000, 500000)),
|
|
387
|
+
valueCurrency: 'USD',
|
|
388
|
+
probability: probabilityByStage[pipelineStage],
|
|
389
|
+
expectedCloseAt: daysAgo(randomInt(-60, 90)),
|
|
390
|
+
source: randomElement(['inbound', 'outbound', 'referral', 'partner']),
|
|
391
|
+
createdAt: dealCreatedAt,
|
|
392
|
+
updatedAt: dealCreatedAt,
|
|
393
|
+
})
|
|
394
|
+
em.persist(deal)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
await em.flush()
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
orders: orders.length,
|
|
401
|
+
customers: customers.length,
|
|
402
|
+
products: products.length,
|
|
403
|
+
deals: dealsCount,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnalyticsEntityConfig,
|
|
3
|
+
AnalyticsEntityTypeConfig,
|
|
4
|
+
AnalyticsFieldMapping,
|
|
5
|
+
AnalyticsLabelResolverConfig,
|
|
6
|
+
AnalyticsModuleConfig,
|
|
7
|
+
} from '@open-mercato/shared/modules/analytics'
|
|
8
|
+
import { getAnalyticsModuleConfigs } from '@open-mercato/shared/modules/analytics'
|
|
9
|
+
|
|
10
|
+
export interface AnalyticsRegistry {
|
|
11
|
+
getAllEntityConfigs(): AnalyticsEntityConfig[]
|
|
12
|
+
getEntityConfig(entityId: string): AnalyticsEntityConfig | null
|
|
13
|
+
isValidEntityType(entityId: string): boolean
|
|
14
|
+
getEntityTypeConfig(entityId: string): AnalyticsEntityTypeConfig | null
|
|
15
|
+
getFieldMapping(entityId: string, field: string): AnalyticsFieldMapping | null
|
|
16
|
+
getRequiredFeatures(entityId: string): string[] | null
|
|
17
|
+
getLabelResolverConfig(entityId: string, field: string): AnalyticsLabelResolverConfig | null
|
|
18
|
+
getAllFieldMappings(entityId: string): Record<string, AnalyticsFieldMapping> | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class DefaultAnalyticsRegistry implements AnalyticsRegistry {
|
|
22
|
+
private configs: AnalyticsModuleConfig[]
|
|
23
|
+
private entityConfigMap: Map<string, AnalyticsEntityConfig>
|
|
24
|
+
|
|
25
|
+
constructor(configs?: AnalyticsModuleConfig[]) {
|
|
26
|
+
this.configs = configs ?? getAnalyticsModuleConfigs()
|
|
27
|
+
this.entityConfigMap = new Map()
|
|
28
|
+
this.buildEntityConfigMap()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private buildEntityConfigMap(): void {
|
|
32
|
+
for (const moduleConfig of this.configs) {
|
|
33
|
+
for (const entityConfig of moduleConfig.entities) {
|
|
34
|
+
this.entityConfigMap.set(entityConfig.entityId, entityConfig)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getAllEntityConfigs(): AnalyticsEntityConfig[] {
|
|
40
|
+
return Array.from(this.entityConfigMap.values())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getEntityConfig(entityId: string): AnalyticsEntityConfig | null {
|
|
44
|
+
return this.entityConfigMap.get(entityId) ?? null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isValidEntityType(entityId: string): boolean {
|
|
48
|
+
return this.entityConfigMap.has(entityId)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getEntityTypeConfig(entityId: string): AnalyticsEntityTypeConfig | null {
|
|
52
|
+
const config = this.getEntityConfig(entityId)
|
|
53
|
+
return config?.entityConfig ?? null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getFieldMapping(entityId: string, field: string): AnalyticsFieldMapping | null {
|
|
57
|
+
const config = this.getEntityConfig(entityId)
|
|
58
|
+
return config?.fieldMappings[field] ?? null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getRequiredFeatures(entityId: string): string[] | null {
|
|
62
|
+
const config = this.getEntityConfig(entityId)
|
|
63
|
+
return config?.requiredFeatures ?? null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getLabelResolverConfig(entityId: string, field: string): AnalyticsLabelResolverConfig | null {
|
|
67
|
+
const config = this.getEntityConfig(entityId)
|
|
68
|
+
return config?.labelResolvers?.[field] ?? null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getAllFieldMappings(entityId: string): Record<string, AnalyticsFieldMapping> | null {
|
|
72
|
+
const config = this.getEntityConfig(entityId)
|
|
73
|
+
return config?.fieldMappings ?? null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createAnalyticsRegistry(configs?: AnalyticsModuleConfig[]): AnalyticsRegistry {
|
|
78
|
+
return new DefaultAnalyticsRegistry(configs)
|
|
79
|
+
}
|