AI Prompt: Forma3D.Connect — ECharts Dashboard Analytics¶
Purpose: Instruct an AI to implement an analytics dashboard with Apache ECharts donut charts, trend lines, and KPI metrics
Estimated Effort: 7–11 days (5 phases)
Prerequisites: None — greenfield analytics feature
Research:docs/03-architecture/research/echarts-dashboard-analytics-research.md
Output: A fully functional analytics dashboard with enhanced KPI tiles, donut charts for orders/print jobs/shipments (with shared period dropdown: Today / Last Week / Last Month / All Time), weekly revenue bar chart, and 30-day order trend line
Status: ✅ DONE
🎯 Mission¶
Add visual analytics to the Forma3D.Connect dashboard using Apache ECharts. The current dashboard (apps/web/src/pages/dashboard.tsx) only shows stat cards and lists — no charts, no trend analysis, no revenue breakdowns.
Problem being solved:
- Operators have no visual overview of order/print/shipping pipelines
- No revenue aggregation or trend visibility
- No print success/failure rate visualization
- Business health and growth are invisible from the dashboard
- Stat cards show flat numbers with no comparison to previous period (no "+3 from yesterday")
Solution: Replace the 4 existing simple stat cards with enhanced KPI tiles that include trend indicators ("+3 from yesterday", "No change", etc.), then add ECharts-powered donut charts for orders, print jobs, and shipments (grouped by status), a revenue trend bar chart (weekly), and an order volume trend line (30 days). The rest of the dashboard (System Health, Recent Orders, Active Print Jobs, Welcome Card) stays untouched.
Deliverables:
- Enhanced KPI stat cards with trend indicators (comparison with previous period)
- ECharts foundation: on-demand imports, custom dark theme, reusable
<ChartCard>wrapper <AnalyticsPeriodDropdown>— shared dropdown (Today / Last Week / Last Month / All Time) controlling all 3 donut charts<OrderStatusChart>— donut chart with count + % + EUR per status<PrintJobStatusChart>— donut chart with count + % per status<ShipmentStatusChart>— donut chart with count + % per status<RevenueTrendChart>— bar chart showing current week's daily revenue in EUR<OrderTrendChart>— line chart showing last 30 days order volume- Backend analytics endpoints:
/api/v1/analytics/orders,/api/v1/analytics/print-jobs,/api/v1/analytics/shipments,/api/v1/analytics/trends - Backend enhancement to
/api/v1/dashboard/stats: add comparison deltas (yesterday vs today) - TanStack Query hooks for all analytics data
- Dashboard page integration with responsive layout
Critical constraints:
- Use on-demand ECharts imports to minimize bundle size (~150 KB gz, not ~320 KB)
- Follow existing patterns: repository layer for Prisma, service layer for business logic, controller for HTTP
- All endpoints scoped to
tenantIdfrom authenticated session - Shared API types go in
libs/domain-contracts/src/api/analytics.api.ts - No
any, nots-ignore, noeslint-disable - Charts must work in dark mode (existing theme uses CSS variables)
- Charts must be responsive (stack vertically on mobile)
📌 Context (Current State)¶
What Exists¶
Dashboard Page (apps/web/src/pages/dashboard.tsx):
- System Health Card (status, database, uptime, version)
- 4 StatCards: Pending Orders, Processing, Active Print Jobs, Needs Attention
- These stat cards must be REPLACED with enhanced KPI tiles matching the mockup (
docs/03-architecture/research/assets/echarts-mockups/mockup-dashboard-full-layout.png), which include:- Colored icons (shopping cart, gear, printer, warning triangle)
- Trend comparison indicators ("+3 from yesterday", "→ No change", "↑ +5 from yesterday", "● 2 errors, 2 delays")
- Same 4 metrics, better visual design
- Recent Orders list (last 5)
- Active Print Jobs list (last 5)
- Welcome Card (static branding)
- No charts, no analytics
Dashboard Hook (apps/web/src/hooks/use-dashboard.ts):
useDashboardStats()— queriesapiClient.dashboard.getStats()with 30s pollinguseActivePrintJobs()— queries active print jobs
Dashboard Stats Type (libs/domain-contracts/src/api/dashboard.api.ts):
export interface DashboardStatsApiResponse {
pendingOrders: number;
processingOrders: number;
completedToday: number;
failedOrders: number;
activePrintJobs: number;
completedPrintJobsToday: number;
queuedPrintJobs?: number;
failedPrintJobs?: number;
printersOnline?: number;
averagePrintTime?: number | null;
}
API Client (apps/web/src/lib/api-client.ts):
- Centralized
apiClientobject with nested namespaces request<T>()helper usingfetchwithcredentials: 'include'- Query params via
URLSearchParams
Backend Patterns:
- Controllers:
@Controller('api/v1/{resource}')with@ApiTags,@RequirePermissions - Services:
@Injectable()with repository injection,EventEmitter2,EventLogService - Repositories:
@Injectable()withPrismaServiceinjection,groupBy()for aggregations - Modules: standard NestJS
@Module()with imports, controllers, providers, exports - Guards: global
SessionGuard+PermissionsGuard, use@RequirePermissions(PERMISSIONS.X)decorator
Prisma Models & Indexes:
Order: indexes on[tenantId],[status],[createdAt]— hastotalPrice(Decimal 10,2),currency(default "EUR")PrintJob: indexes on[tenantId],[status]— hasestimatedDuration,actualDuration,retryCountShipment: indexes on[tenantId],[status],[createdAt]— hasweight(Decimal 10,3)LineItem: hasunitPrice(Decimal 10,2),quantity(Int)- No composite indexes on
(tenantId, status, createdAt)— should be added for analytics queries
Status Enums:
| Model | Statuses |
|---|---|
OrderStatus |
PENDING, PROCESSING, PARTIALLY_COMPLETED, COMPLETED, FAILED, CANCELLED |
PrintJobStatus |
QUEUED, ASSIGNED, PRINTING, COMPLETED, FAILED, CANCELLED |
ShipmentStatus |
PENDING, LABEL_CREATED, ANNOUNCED, IN_TRANSIT, DELIVERED, FAILED, CANCELLED |
Chart Libraries Installed: None. This is a greenfield addition.
Frontend Stack: React 19, Vite 7, Tailwind CSS v4, TanStack Query v5, React Router v6, Heroicons, clsx
What's Missing¶
- ECharts dependency — not installed
- Analytics endpoints — no
/api/v1/analytics/*routes - Analytics module — no
AnalyticsModule,AnalyticsService,AnalyticsRepository,AnalyticsController - Analytics shared types — no
analytics.api.tsinlibs/domain-contracts - Chart components — no charting components anywhere
- Stat card trend indicators — existing stat cards show flat counts with no comparison (no "+3 from yesterday" deltas)
- Dashboard stats comparison data — backend
getStats()returns current counts only, not comparison with previous period - Composite database indexes — no
(tenantId, status, createdAt)for efficient analytics queries
Design Decisions¶
Donut Chart Timespan¶
Note: The mockup (
docs/03-architecture/research/assets/echarts-mockups/mockup-dashboard-full-layout.png) does not explicitly show a time-period selector on the donut charts. However, operators need the ability to slice data by time range to answer questions like "how many orders failed this week?"
Chosen approach: A single shared dropdown above the donut charts row controls the time period for all 3 donut charts simultaneously. The dropdown options are:
| Value | Label | Backend dateFrom |
|---|---|---|
today |
Today | Start of today (00:00) |
week |
Last Week | 7 days ago |
month |
Last Month | 30 days ago |
all |
All Time | No date filter |
Default: all (All Time) — matches the mockup's apparent all-time totals ("500+" orders, etc.).
UX design:
- The dropdown is placed above the 3 donut charts row, right-aligned, as a single compact
<select>or styled dropdown - Changing the dropdown re-fetches all 3 donut charts with the selected period
- All 3 hooks share the same
periodstate fromuseStatein the dashboard page - The dropdown does NOT affect the KPI stat cards or the trend charts (those have their own fixed timespans)
Trend Chart Timespans¶
The mockup shows fixed timespans for trend charts:
- Revenue bar chart: "Revenue This Week" — always shows Mon–Sun of the current week
- Order trend line: "Order Trend — Last 30 Days" — always shows the last 30 calendar days
These are fixed views, not user-selectable. Selectable day ranges (7d/30d/90d) can be a future enhancement.
🛠️ Tech Stack Reference¶
New Dependencies¶
echarts ^5.6.0 # Core charting library (Apache Foundation)
echarts-for-react ^3.0.6 # Thin React wrapper (3 KB gzipped)
Existing Stack (No Changes)¶
- Frontend: React 19, Vite 7, Tailwind CSS v4, TanStack Query v5
- Backend: NestJS, Prisma, PostgreSQL
- Testing: Vitest (frontend), Jest (backend)
- Monorepo: Nx with pnpm
🏗️ Architecture Requirements¶
1) ECharts On-Demand Imports¶
Do NOT import the full echarts package. Use selective imports to reduce bundle from ~320 KB to ~150 KB gzipped:
// libs/ui/src/charts/echarts-setup.ts
import * as echarts from 'echarts/core';
import { PieChart, BarChart, LineChart, GaugeChart } from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([
PieChart,
BarChart,
LineChart,
GaugeChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
CanvasRenderer,
]);
export { echarts };
Use ReactEChartsCore from echarts-for-react/lib/core (not the default export) to leverage on-demand imports.
2) React 19 Peer Dependency Override¶
Add to root package.json:
{
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"echarts-for-react>react": "19",
"echarts-for-react>react-dom": "19"
}
}
}
}
3) Backend Analytics Module¶
Follow the existing module pattern (see OrdersModule):
apps/api/src/analytics/
├── analytics.module.ts
├── analytics.controller.ts
├── analytics.service.ts
├── analytics.repository.ts
└── dto/
├── analytics-query.dto.ts
└── analytics-response.dto.ts
4) Shared API Types¶
Add to libs/domain-contracts/src/api/analytics.api.ts and re-export from the barrel file.
5) Database Optimization¶
Add composite indexes for analytics queries via a Prisma migration:
model Order {
@@index([tenantId, status, createdAt])
}
model PrintJob {
@@index([tenantId, status, createdAt])
}
model Shipment {
@@index([tenantId, status, createdAt])
}
📁 Files to Create/Modify¶
Backend (NestJS)¶
apps/api/src/analytics/
├── analytics.module.ts # CREATE: NestJS module
├── analytics.controller.ts # CREATE: REST endpoints
├── analytics.service.ts # CREATE: Business logic
├── analytics.repository.ts # CREATE: Prisma queries
└── dto/
├── analytics-query.dto.ts # CREATE: Query param DTO (shared period for donuts)
└── analytics-response.dto.ts # CREATE: Swagger response schemas
apps/api/src/dashboard/ # UPDATE: enhance existing dashboard stats
├── dashboard.service.ts # UPDATE: add comparison deltas (yesterday vs today)
├── dashboard.controller.ts # UPDATE: return enhanced stats with deltas
apps/api/src/app.module.ts # UPDATE: import AnalyticsModule
prisma/schema.prisma # UPDATE: add composite indexes
Shared Types¶
libs/domain-contracts/src/api/
├── analytics.api.ts # CREATE: Shared request/response types
└── index.ts # UPDATE: re-export analytics types
Frontend (React)¶
libs/ui/src/charts/
├── echarts-setup.ts # CREATE: On-demand imports + theme
├── chart-card.tsx # CREATE: Reusable chart wrapper with time tabs
├── donut-chart.tsx # CREATE: Generic donut chart component
├── bar-chart.tsx # CREATE: Generic bar chart component
├── line-chart.tsx # CREATE: Generic line chart component
├── chart-colors.ts # CREATE: Color constants for chart statuses
└── index.ts # CREATE: Barrel export
apps/web/src/components/analytics/
├── analytics-period-dropdown.tsx # CREATE: Shared period dropdown (Today / Last Week / Last Month / All Time)
├── order-status-chart.tsx # CREATE: Orders donut chart
├── print-job-status-chart.tsx # CREATE: Print jobs donut chart
├── shipment-status-chart.tsx # CREATE: Shipments donut chart
├── revenue-trend-chart.tsx # CREATE: Revenue bar chart
├── order-trend-chart.tsx # CREATE: Order volume line chart
└── index.ts # CREATE: Barrel export
apps/web/src/hooks/
├── use-analytics.ts # CREATE: TanStack Query hooks for analytics
apps/web/src/lib/api-client.ts # UPDATE: add analytics namespace
apps/web/src/pages/dashboard.tsx # UPDATE: integrate chart components
package.json # UPDATE: add echarts dependencies + peer dep rules
🔧 Implementation Details¶
Phase 1: Foundation¶
1.1 Install Dependencies¶
pnpm add echarts echarts-for-react
Add peer dependency rules to root package.json:
{
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"echarts-for-react>react": "19",
"echarts-for-react>react-dom": "19"
}
}
}
}
1.2 ECharts Setup Module¶
Create libs/ui/src/charts/echarts-setup.ts:
import * as echarts from 'echarts/core';
import { PieChart, BarChart, LineChart, GaugeChart } from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([
PieChart,
BarChart,
LineChart,
GaugeChart,
TitleComponent,
TooltipComponent,
LegendComponent,
GridComponent,
CanvasRenderer,
]);
// Register custom Forma3D dark theme
echarts.registerTheme('forma3d', {
backgroundColor: 'transparent',
textStyle: {
color: '#94a3b8', // Slate-400 — matches theme-tertiary
},
title: {
textStyle: { color: '#f1f5f9' }, // Slate-100
subtextStyle: { color: '#94a3b8' }, // Slate-400
},
legend: {
textStyle: { color: '#94a3b8' },
},
tooltip: {
backgroundColor: '#1e293b', // Slate-800
borderColor: '#334155', // Slate-700
textStyle: { color: '#f1f5f9' },
},
});
export { echarts };
export type { EChartsOption } from 'echarts';
1.3 Chart Color Constants¶
Create libs/ui/src/charts/chart-colors.ts:
/** Order status colors — matching apps/web/src/lib/constants.ts */
export const ORDER_STATUS_CHART_COLORS: Record<string, string> = {
PENDING: '#9CA3AF',
PROCESSING: '#3B82F6',
PARTIALLY_COMPLETED: '#F59E0B',
COMPLETED: '#10B981',
FAILED: '#EF4444',
CANCELLED: '#6B7280',
};
/** Print job status colors */
export const PRINT_JOB_STATUS_CHART_COLORS: Record<string, string> = {
QUEUED: '#60A5FA',
ASSIGNED: '#818CF8',
PRINTING: '#A855F7',
COMPLETED: '#10B981',
FAILED: '#EF4444',
CANCELLED: '#6B7280',
};
/** Shipment status colors */
export const SHIPMENT_STATUS_CHART_COLORS: Record<string, string> = {
PENDING: '#9CA3AF',
LABEL_CREATED: '#F59E0B',
ANNOUNCED: '#60A5FA',
IN_TRANSIT: '#818CF8',
DELIVERED: '#10B981',
FAILED: '#EF4444',
CANCELLED: '#6B7280',
};
1.4 Reusable ChartCard Wrapper¶
Create libs/ui/src/charts/chart-card.tsx:
A <ChartCard> component that wraps any chart with:
- Title and optional subtitle (e.g., "Revenue This Week", "Total Orders: 1,850")
- Optional summary value displayed prominently (e.g., "$4,600")
- Loading spinner state
- Empty state when no data
- Responsive container that passes width/height to ECharts
interface ChartCardProps {
title: string;
subtitle?: string;
/** Optional prominent summary value, e.g., "$4,600" */
summaryValue?: string;
loading?: boolean;
empty?: boolean;
children: React.ReactNode;
}
Note: The <ChartCard> itself does NOT contain period selector tabs. Time-period filtering for the donut charts is handled by a single shared <AnalyticsPeriodDropdown> placed above the donut row in the dashboard layout. The <ChartCard> just renders the title, subtitle, and chart content.
1.5 Generic Donut Chart Component¶
Create libs/ui/src/charts/donut-chart.tsx:
A reusable <DonutChart> that accepts:
interface DonutChartSegment {
name: string;
value: number;
color: string;
/** Optional extra data displayed in tooltip (e.g., EUR amount) */
extra?: Record<string, string | number>;
}
interface DonutChartProps {
segments: DonutChartSegment[];
/** Text displayed in the center of the donut */
centerLabel?: string;
centerSubLabel?: string;
/** Tooltip formatter — receives segment data */
tooltipFormatter?: (segment: DonutChartSegment) => string;
/** Fired when a slice is clicked */
onSliceClick?: (segment: DonutChartSegment) => void;
height?: number | string;
}
Implementation notes:
- Use
ReactEChartsCorefromecharts-for-react/lib/corewith our registeredechartsinstance - Apply
theme="forma3d"for dark mode - Donut via
radius: ['40%', '70%'] - Leader-line labels:
label.position: 'outside'withlabelLine: { show: true } - Center text via
graphiccomponent (absolute positioned text in the donut hole) - Click handler via
onEvents={{ click: handler }} notMerge={true}andlazyUpdate={true}for performance
Phase 2: Backend Analytics API¶
2.1 Shared Analytics Types¶
Create libs/domain-contracts/src/api/analytics.api.ts:
// ─── Query ────────────────────────────────────
/** Shared period for all 3 donut charts (single dropdown) */
export type AnalyticsPeriod = 'today' | 'week' | 'month' | 'all';
// ─── Order Analytics ──────────────────────────
export interface OrderStatusMetric {
status: string;
count: number;
percentage: number;
totalValue: number; // EUR
}
export interface OrderAnalyticsApiResponse {
period: AnalyticsPeriod;
totalOrders: number;
totalRevenue: number;
avgOrderValue: number;
statuses: OrderStatusMetric[];
}
// ─── Print Job Analytics ──────────────────────
export interface PrintJobStatusMetric {
status: string;
count: number;
percentage: number;
}
export interface PrintJobAnalyticsApiResponse {
period: AnalyticsPeriod;
totalJobs: number;
avgDurationSeconds: number | null;
successRate: number;
retryRate: number;
statuses: PrintJobStatusMetric[];
}
// ─── Shipment Analytics ───────────────────────
export interface ShipmentStatusMetric {
status: string;
count: number;
percentage: number;
}
export interface ShipmentAnalyticsApiResponse {
period: AnalyticsPeriod;
totalShipments: number;
deliveryRate: number;
avgDeliveryDays: number | null;
statuses: ShipmentStatusMetric[];
}
// ─── Trends ───────────────────────────────────
export type TrendMetric = 'orders' | 'revenue';
export interface TrendDataPoint {
date: string; // ISO date string (YYYY-MM-DD)
value: number;
}
export interface TrendsApiResponse {
metric: TrendMetric;
period: string; // e.g., "This Week", "Last 30 Days"
summaryValue: number; // e.g., total revenue or total orders for the period
dataPoints: TrendDataPoint[];
}
// ─── Dashboard Stats Enhancement ──────────────
/** Delta/comparison data for KPI stat cards */
export interface StatDelta {
/** Absolute change from previous period (e.g., +3) */
change: number;
/** Human-readable label (e.g., "from yesterday") */
label: string;
}
export interface EnhancedDashboardStatsApiResponse {
pendingOrders: number;
pendingOrdersDelta?: StatDelta;
processingOrders: number;
processingOrdersDelta?: StatDelta;
activePrintJobs: number;
activePrintJobsDelta?: StatDelta;
needsAttention: number;
needsAttentionDetails?: string; // e.g., "2 errors, 2 delays"
}
Update libs/domain-contracts/src/api/index.ts to re-export.
2.2 Analytics Repository¶
Create apps/api/src/analytics/analytics.repository.ts:
@Injectable()
export class AnalyticsRepository {
constructor(private readonly prisma: PrismaService) {}
/** Order status distribution, optionally filtered by dateFrom */
async getOrderStatusCounts(
tenantId: string,
dateFrom?: Date
): Promise<Array<{ status: string; _count: number; _sum_totalPrice: number }>> {
// Use prisma.order.groupBy with:
// by: ['status']
// where: { tenantId, createdAt: dateFrom ? { gte: dateFrom } : undefined }
// _count: { id: true }
// _sum: { totalPrice: true }
}
/** Print job status distribution, optionally filtered by dateFrom */
async getPrintJobStatusCounts(
tenantId: string,
dateFrom?: Date
): Promise<Array<{ status: string; _count: number }>> {
// Use prisma.printJob.groupBy with:
// by: ['status']
// where: { tenantId, createdAt: dateFrom ? { gte: dateFrom } : undefined }
// _count: { id: true }
}
async getPrintJobDurationStats(
tenantId: string,
dateFrom?: Date
): Promise<{ avgDuration: number | null; retryCount: number; totalCompleted: number }> {
// Use prisma.printJob.aggregate with _avg on actualDuration
// and _sum on retryCount, filtered by dateFrom
}
/** Shipment status distribution, optionally filtered by dateFrom */
async getShipmentStatusCounts(
tenantId: string,
dateFrom?: Date
): Promise<Array<{ status: string; _count: number }>> {
// Use prisma.shipment.groupBy
// where: { tenantId, createdAt: dateFrom ? { gte: dateFrom } : undefined }
}
/** Order count trend: daily counts from dateFrom to now */
async getOrderTrend(
tenantId: string,
dateFrom: Date
): Promise<Array<{ date: string; count: number }>> {
// Use raw SQL or Prisma groupBy with date truncation
// GROUP BY DATE(createdAt) ORDER BY date
}
/** Revenue trend: daily revenue from dateFrom to now */
async getRevenueTrend(
tenantId: string,
dateFrom: Date
): Promise<Array<{ date: string; revenue: number }>> {
// Similar to order trend but SUM(totalPrice)
}
/** Comparison counts for KPI stat cards (today vs yesterday) */
async getOrderCountByDate(tenantId: string, dateFrom: Date, dateTo: Date): Promise<number> {
// prisma.order.count where createdAt between dateFrom and dateTo
}
}
Important: Use prisma.order.groupBy() for status counts — it maps directly to SQL GROUP BY which is very efficient with the composite index. For daily trends, raw SQL with DATE(created_at) grouping may be needed since Prisma doesn't natively support date truncation in groupBy.
2.3 Analytics Service¶
Create apps/api/src/analytics/analytics.service.ts:
@Injectable()
export class AnalyticsService {
constructor(private readonly analyticsRepository: AnalyticsRepository) {}
/** Returns orders grouped by status for the given period */
async getOrderAnalytics(
tenantId: string,
period: AnalyticsPeriod
): Promise<OrderAnalyticsApiResponse> {
const dateFrom = this.getDateFrom(period);
const statusCounts = await this.analyticsRepository.getOrderStatusCounts(tenantId, dateFrom);
const totalOrders = statusCounts.reduce((sum, s) => sum + s._count, 0);
const totalRevenue = statusCounts.reduce((sum, s) => sum + s._sum_totalPrice, 0);
return {
period,
totalOrders,
totalRevenue,
avgOrderValue: totalOrders > 0 ? totalRevenue / totalOrders : 0,
statuses: statusCounts.map((s) => ({
status: s.status,
count: s._count,
percentage: totalOrders > 0 ? Math.round((s._count / totalOrders) * 100) : 0,
totalValue: s._sum_totalPrice,
})),
};
}
// Similar methods for print jobs and shipments (same period param pattern)...
private getDateFrom(period: AnalyticsPeriod): Date | undefined {
const now = new Date();
switch (period) {
case 'today':
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
case 'week':
const weekAgo = new Date(now);
weekAgo.setDate(now.getDate() - 7);
weekAgo.setHours(0, 0, 0, 0);
return weekAgo;
case 'month':
const monthAgo = new Date(now);
monthAgo.setDate(now.getDate() - 30);
monthAgo.setHours(0, 0, 0, 0);
return monthAgo;
case 'all':
return undefined;
}
}
/** Revenue trend for the current week (Mon–Sun) */
async getRevenueTrend(tenantId: string): Promise<TrendsApiResponse> {
const weekStart = this.getStartOfWeek();
const dataPoints = await this.analyticsRepository.getRevenueTrend(tenantId, weekStart);
const summaryValue = dataPoints.reduce((sum, dp) => sum + dp.revenue, 0);
return {
metric: 'revenue',
period: 'This Week',
summaryValue,
dataPoints: dataPoints.map((dp) => ({ date: dp.date, value: dp.revenue })),
};
}
/** Order trend for the last 30 days */
async getOrderTrend(tenantId: string): Promise<TrendsApiResponse> {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const dataPoints = await this.analyticsRepository.getOrderTrend(tenantId, thirtyDaysAgo);
const summaryValue = dataPoints.reduce((sum, dp) => sum + dp.count, 0);
return {
metric: 'orders',
period: 'Last 30 Days',
summaryValue,
dataPoints: dataPoints.map((dp) => ({ date: dp.date, value: dp.count })),
};
}
private getStartOfWeek(): Date {
const now = new Date();
const day = now.getDay();
const diff = now.getDate() - day + (day === 0 ? -6 : 1); // Monday start
const weekStart = new Date(now.getFullYear(), now.getMonth(), diff);
weekStart.setHours(0, 0, 0, 0);
return weekStart;
}
}
2.4 Analytics Controller¶
Create apps/api/src/analytics/analytics.controller.ts:
@ApiTags('Analytics')
@ApiSecurity('api-key')
@Controller('api/v1/analytics')
@SkipThrottle()
@RequirePermissions(PERMISSIONS.ORDERS_READ) // Analytics requires at least read access
export class AnalyticsController {
constructor(private readonly analyticsService: AnalyticsService) {}
@Get('orders')
@ApiOperation({ summary: 'Get order analytics by status' })
@ApiQuery({ name: 'period', enum: ['today', 'week', 'month', 'all'], required: false })
async getOrderAnalytics(
@Query() query: AnalyticsQueryDto,
@Req() req: AuthenticatedRequest
): Promise<OrderAnalyticsApiResponse> {
return this.analyticsService.getOrderAnalytics(req.tenantId, query.period ?? 'all');
}
@Get('print-jobs')
@ApiOperation({ summary: 'Get print job analytics by status' })
@ApiQuery({ name: 'period', enum: ['today', 'week', 'month', 'all'], required: false })
async getPrintJobAnalytics(
@Query() query: AnalyticsQueryDto,
@Req() req: AuthenticatedRequest
): Promise<PrintJobAnalyticsApiResponse> {
return this.analyticsService.getPrintJobAnalytics(req.tenantId, query.period ?? 'all');
}
@Get('shipments')
@ApiOperation({ summary: 'Get shipment analytics by status' })
@ApiQuery({ name: 'period', enum: ['today', 'week', 'month', 'all'], required: false })
async getShipmentAnalytics(
@Query() query: AnalyticsQueryDto,
@Req() req: AuthenticatedRequest
): Promise<ShipmentAnalyticsApiResponse> {
return this.analyticsService.getShipmentAnalytics(req.tenantId, query.period ?? 'all');
}
@Get('trends/revenue')
@ApiOperation({ summary: 'Get revenue trend for the current week' })
async getRevenueTrend(@Req() req: AuthenticatedRequest): Promise<TrendsApiResponse> {
return this.analyticsService.getRevenueTrend(req.tenantId);
}
@Get('trends/orders')
@ApiOperation({ summary: 'Get order trend for the last 30 days' })
async getOrderTrend(@Req() req: AuthenticatedRequest): Promise<TrendsApiResponse> {
return this.analyticsService.getOrderTrend(req.tenantId);
}
}
2.5 Analytics DTOs¶
Create apps/api/src/analytics/dto/analytics-query.dto.ts:
export class AnalyticsQueryDto {
@ApiPropertyOptional({ enum: ['today', 'week', 'month', 'all'], default: 'all' })
@IsOptional()
@IsIn(['today', 'week', 'month', 'all'])
period?: AnalyticsPeriod;
}
Note: The period param is shared by all 3 donut endpoints (orders, print-jobs, shipments). The frontend sends the same period value to all 3 because a single dropdown controls them. Default is 'all' (All Time). The trend endpoints (revenue, orders) take no query params — they use fixed timespans.
Create apps/api/src/analytics/dto/analytics-response.dto.ts — Swagger response schemas matching the shared types.
2.6 Analytics Module¶
Create apps/api/src/analytics/analytics.module.ts:
@Module({
controllers: [AnalyticsController],
providers: [AnalyticsService, AnalyticsRepository],
exports: [AnalyticsService],
})
export class AnalyticsModule {}
Import in apps/api/src/app.module.ts.
2.7 Database Migration¶
Add composite indexes to prisma/schema.prisma:
model Order {
// ... existing fields
@@index([tenantId, status, createdAt])
}
model PrintJob {
// ... existing fields
@@index([tenantId, status, createdAt])
}
model Shipment {
// ... existing fields
@@index([tenantId, status, createdAt])
}
Run: pnpm prisma migrate dev --name add-analytics-composite-indexes
Phase 3: Frontend Analytics Integration¶
3.1 API Client Extension¶
Update apps/web/src/lib/api-client.ts — add analytics namespace:
analytics: {
getOrderAnalytics: (period: AnalyticsPeriod = 'all'): Promise<OrderAnalyticsApiResponse> =>
request<OrderAnalyticsApiResponse>('/api/v1/analytics/orders', { params: { period } }),
getPrintJobAnalytics: (period: AnalyticsPeriod = 'all'): Promise<PrintJobAnalyticsApiResponse> =>
request<PrintJobAnalyticsApiResponse>('/api/v1/analytics/print-jobs', { params: { period } }),
getShipmentAnalytics: (period: AnalyticsPeriod = 'all'): Promise<ShipmentAnalyticsApiResponse> =>
request<ShipmentAnalyticsApiResponse>('/api/v1/analytics/shipments', { params: { period } }),
getRevenueTrend: (): Promise<TrendsApiResponse> =>
request<TrendsApiResponse>('/api/v1/analytics/trends/revenue'),
getOrderTrend: (): Promise<TrendsApiResponse> =>
request<TrendsApiResponse>('/api/v1/analytics/trends/orders'),
},
3.2 Analytics Hooks¶
Create apps/web/src/hooks/use-analytics.ts:
/** Donut charts — period comes from shared dropdown state */
export function useOrderAnalytics(period: AnalyticsPeriod) {
return useQuery({
queryKey: ['analytics', 'orders', period],
queryFn: () => apiClient.analytics.getOrderAnalytics(period),
refetchInterval: 60000, // 1 minute
});
}
export function usePrintJobAnalytics(period: AnalyticsPeriod) {
return useQuery({
queryKey: ['analytics', 'print-jobs', period],
queryFn: () => apiClient.analytics.getPrintJobAnalytics(period),
refetchInterval: 60000,
});
}
export function useShipmentAnalytics(period: AnalyticsPeriod) {
return useQuery({
queryKey: ['analytics', 'shipments', period],
queryFn: () => apiClient.analytics.getShipmentAnalytics(period),
refetchInterval: 60000,
});
}
/** Trend charts — fixed timespans, no period param */
export function useRevenueTrend() {
return useQuery({
queryKey: ['analytics', 'trends', 'revenue'],
queryFn: () => apiClient.analytics.getRevenueTrend(),
refetchInterval: 300000, // 5 minutes
});
}
export function useOrderTrend() {
return useQuery({
queryKey: ['analytics', 'trends', 'orders'],
queryFn: () => apiClient.analytics.getOrderTrend(),
refetchInterval: 300000, // 5 minutes
});
}
3.3 Chart Components¶
Create domain-specific chart components in apps/web/src/components/analytics/:
order-status-chart.tsx:
- Uses
<ChartCard>with title "Orders by Status" - Receives
periodprop from shared dropdown state (does NOT have its own period selector) - Uses
<DonutChart>withORDER_STATUS_CHART_COLORS - Maps
OrderAnalyticsApiResponse.statusestoDonutChartSegment[] - Center label:
{totalOrders}+(e.g., "500+") — matches mockup - Bottom legend with colored dots: New, Pending, Processing, Completed, Cancelled
- Tooltip: status name, count, percentage, EUR amount
- Click handler: navigates to
/orders?status={STATUS}via React Router
print-job-status-chart.tsx:
- Same donut pattern with
PRINT_JOB_STATUS_CHART_COLORS - Center label:
Active: {activeCount}— matches mockup (showing currently active, not total) - Bottom legend: Queued, Preparing, Printing, Finished, (Failed)
- No EUR in this chart (print jobs have no monetary value)
shipment-status-chart.tsx:
- Same donut pattern with
SHIPMENT_STATUS_CHART_COLORS - Center label:
In Transit: {inTransitCount}— matches mockup (showing current in-transit) - Bottom legend: Ready to Ship, Shipped, In Transit, Delivered, Completed, Returned
revenue-trend-chart.tsx:
- Uses
<ChartCard>with title "Revenue This Week" and summary value "$4,600" - Uses a generic
<BarChart>component - Fixed timespan: current week (Mon–Sun) — no selector
- X-axis: day of week (Mon, Tue, ..., Sun), Y-axis: EUR
- Value labels above each bar
- Tooltip: day + EUR amount
order-trend-chart.tsx:
- Uses
<ChartCard>with title "Order Trend — Last 30 Days" and subtitle "Total Orders: 1,850" - Uses a generic
<LineChart>component - Fixed timespan: last 30 calendar days — no selector
- X-axis: dates, Y-axis: order count
- Area fill below the line (gradient)
- Tooltip on hover with date + order count (e.g., "Oct 25: 78 Orders")
3.4 Dashboard Integration¶
Update apps/web/src/pages/dashboard.tsx:
The 4 existing simple colored StatCard components are REPLACED by enhanced KPI tiles matching the mockup. The rest of the dashboard (System Health, Recent Orders, Active Print Jobs, Welcome Card) stays untouched — only pushed down to make room for the new chart rows.
Layout Order (matching docs/03-architecture/research/assets/echarts-mockups/mockup-dashboard-full-layout.png):
1. Page Header (existing, untouched)
2. System Health Card (existing, untouched)
3. Stats Grid — 4 ENHANCED KPI tiles (REPLACE existing stat cards)
│ Pending Orders │ Processing │ Active Print Jobs │ Needs Attention │
│ 12 │ 24 │ 30 │ 4 │
│ ↗ +3 from yesterday│ → No change │ ↑ +5 from yesterday│ ● 2 errors, 2 delays │
(4 columns on desktop, 2×2 on mobile — same grid as current)
4. ─── NEW: Donut Charts Section ───
┌─────────────────────────────────────────────────────────────────── [Today ▾] ──┐ ← shared period dropdown (right-aligned)
│ Orders by Status (500+) │ Print Jobs by Status (Active: 30) │ Shipments by Status (In Transit: 45) │
(3 columns on desktop, stacked on mobile)
** Single dropdown controls all 3 donut charts — options: Today / Last Week / Last Month / All Time **
** Default: All Time **
5. ─── NEW: Trends Row ───
│ Revenue This Week ($4,600) │ Order Trend — Last 30 Days (1,850) │
(2 columns on desktop, stacked on mobile)
** Fixed timespans — not user-selectable **
6. Recent Orders + Active Print Jobs (existing, untouched, moved down)
7. Welcome Card (existing, untouched, moved down)
StatCard enhancement requirements:
- Extend the existing
StatCardcomponent (or createEnhancedStatCard) to accept: delta?: { change: number; label: string }— comparison indicator ("+3 from yesterday")details?: string— secondary detail text ("2 errors, 2 delays")- Enhance the backend
getStats()endpoint to return comparison data (today vs yesterday counts) - Display trend direction arrow: ↗ for positive change, ↘ for negative, → for no change
- Color the delta text: green for positive, red for negative, gray for no change
Use React.lazy() and <Suspense> for the chart components to avoid blocking initial dashboard render with the ECharts bundle.
Phase 4: Advanced Charts (Optional / P2-P3)¶
Only implement these after Phases 1–3 are stable and working.
4.1 Revenue Breakdown — Nested Donut¶
A nested pie chart with:
- Inner ring: revenue by order status (Completed, Processing, Pending)
- Outer ring: revenue by product SKU within each status
Requires a new endpoint: GET /api/v1/analytics/revenue-breakdown
4.2 Print Success Gauge¶
A gauge chart showing successRate from the print job analytics response:
- Green zone: 90–100%
- Yellow zone: 75–90%
- Red zone: < 75%
Uses the GaugeChart already registered in echarts-setup.ts.
4.3 Failure Reasons — Rose/Nightingale Chart¶
A rose chart showing breakdown of print job failure reasons. Requires extending the print job analytics endpoint with failure categorization.
Phase 5: Polish & QA¶
5.1 Responsive Design¶
- Charts stack vertically on screens < 1024px (
lg:grid-cols-3→grid-cols-1) - Chart height adapts: 300px on desktop, 250px on mobile
- Legend moves from side to bottom on mobile
- Chart titles and summary values wrap on very small screens
5.2 Loading & Empty States¶
- Show
<LoadingSpinner />inside<ChartCard>while data loads - Show a centered "No data available" message when all counts are 0
- Animate chart transitions when data refreshes (ECharts handles this natively)
5.3 Click-to-Filter Navigation¶
When a user clicks a donut slice:
- Orders chart: navigate to
/orders?status={STATUS} - Print jobs chart: navigate to
/orders?printJobStatus={STATUS}(or a future dedicated page) - Shipments chart: navigate to
/orders?shipmentStatus={STATUS}(or a future dedicated page)
5.4 Accessibility¶
- Add
aria-labelto each chart container: "Orders by status donut chart" - Use ECharts
ariaoption for auto-generated chart descriptions - Ensure focus management with keyboard navigation for interactive chart elements
- Sufficient color contrast in chart colors (already verified — all meet WCAG AA)
5.5 Performance Validation¶
- Verify bundle size impact: run
pnpm nx build weband compare bundle stats - Target: < 170 KB gzipped increase from ECharts on-demand imports
- Dashboard initial load should remain < 3 seconds on 3G connection
- Charts should render within 200ms after data arrives
🧪 Testing Requirements¶
Backend Unit Tests¶
Analytics Repository:
| Scenario | Description |
|---|---|
getOrderStatusCounts with tenant filter (all-time) |
Returns grouped counts scoped to tenant, no date filter |
getOrderStatusCounts with tenant + dateFrom |
Filters to orders created after dateFrom |
getOrderStatusCounts with no data |
Returns empty array |
getPrintJobStatusCounts |
Same patterns as order tests |
getShipmentStatusCounts |
Same patterns |
getPrintJobDurationStats |
Returns avg duration, retry count |
getOrderTrend for 7 days |
Returns 7 data points |
getRevenueTrend for 30 days |
Returns 30 data points with EUR sums |
Analytics Service:
| Scenario | Description |
|---|---|
getOrderAnalytics(tenantId, 'all') |
Calculates percentages and averages correctly (all-time) |
getOrderAnalytics(tenantId, 'today') |
Filters to today's orders only |
getOrderAnalytics(tenantId, 'week') |
Filters to last 7 days |
getRevenueTrend(tenantId) |
Returns current week's daily data points |
getOrderTrend(tenantId) |
Returns last 30 days' data points |
| Week start calculation | Correctly identifies Monday as start of week |
| Zero orders | Returns zeros without division errors |
Analytics Controller:
| Scenario | Description |
|---|---|
GET /api/v1/analytics/orders (no period) |
Returns 200 with all-time data (default) |
GET /api/v1/analytics/orders?period=today |
Returns 200 with today's data |
GET /api/v1/analytics/orders?period=invalid |
Returns 400 validation error |
GET /api/v1/analytics/trends/revenue |
Returns 200 with current week's data |
GET /api/v1/analytics/trends/orders |
Returns 200 with last 30 days' data |
| Unauthenticated request | Returns 401 |
| Missing permissions | Returns 403 |
Frontend Unit Tests¶
Analytics Hooks:
| Scenario | Description |
|---|---|
useOrderAnalytics('all') |
Calls API with ?period=all |
useOrderAnalytics('today') |
Calls API with ?period=today — refetches on period change |
useRevenueTrend |
Calls trends/revenue endpoint |
useOrderTrend |
Calls trends/orders endpoint |
| Error handling | Returns error state on API failure |
Chart Components:
| Scenario | Description |
|---|---|
OrderStatusChart renders |
Renders chart card with correct title |
OrderStatusChart loading |
Shows loading spinner |
OrderStatusChart empty |
Shows empty state message |
| Click on donut slice | Navigates to filtered view |
✅ Validation Checklist¶
Infrastructure¶
-
pnpm installsucceeds without errors (only peer dep info for echarts-for-react) -
pnpm nx build websucceeds -
pnpm nx build apisucceeds -
pnpm nx lint webpasses -
pnpm nx lint apipasses - No TypeScript errors
- Prisma migration applied successfully
Backend¶
-
AnalyticsModulecreated and imported inAppModule -
AnalyticsControllerwith 5 endpoints (orders, print-jobs, shipments, trends/revenue, trends/orders) -
AnalyticsServicewith period-based date filtering and fixed timespans for trends -
AnalyticsRepositorywith PrismagroupByqueries - Dashboard stats endpoint enhanced with comparison deltas (yesterday vs today)
- Swagger response DTOs for all endpoints
- Swagger documentation for all endpoints
- Tenant scoping on all queries
-
@RequirePermissionsapplied - Composite indexes added to Prisma schema
Shared Types¶
-
analytics.api.tscreated inlibs/domain-contracts/src/api/ - All response interfaces exported and importable from
@forma3d/domain-contracts
Frontend¶
- ECharts on-demand imports (NOT full import)
- Custom
forma3dtheme registered -
<ChartCard>reusable wrapper (title, subtitle, summary value — no per-chart period tabs) -
<DonutChart>generic component -
<AnalyticsPeriodDropdown>shared dropdown (Today / Last Week / Last Month / All Time) controlling all 3 donuts -
<OrderStatusChart>,<PrintJobStatusChart>,<ShipmentStatusChart>components (receive period from shared dropdown) -
<RevenueTrendChart>(fixed: current week) and<OrderTrendChart>(fixed: last 30 days) -
useOrderAnalytics(period),usePrintJobAnalytics(period),useShipmentAnalytics(period),useRevenueTrend,useOrderTrendhooks -
apiClient.analytics.*methods in API client - Existing 4 stat cards REPLACED with enhanced KPI tiles (trend indicators, colored icons)
- Dashboard page updated with donut chart row + trend chart row
- Charts lazy-loaded with
React.lazy()+<Suspense> - Responsive layout (3-col → 1-col on mobile for donuts, 2-col → 1-col for trends)
- Loading and empty states for all charts
- Dark mode works correctly
Testing¶
- Backend: repository tests for all
groupByqueries - Backend: service tests for percentage logic and trend timespan calculation
- Backend: controller tests for endpoint routing and validation
- Frontend: hook tests for API calls
- Frontend: component render tests for charts
🎬 Execution Order¶
Phase 1: Foundation (1–2 days)¶
- Install
echartsandecharts-for-reactvia pnpm - Add peer dependency rules to root
package.json - Create
libs/ui/src/charts/echarts-setup.tswith on-demand imports and theme - Create
libs/ui/src/charts/chart-colors.tswith status color constants - Create
libs/ui/src/charts/chart-card.tsx— reusable wrapper (title, subtitle, summary value — no per-chart period tabs) - Create
libs/ui/src/charts/donut-chart.tsx— generic donut chart component - Create
libs/ui/src/charts/bar-chart.tsx— generic bar chart component - Create
libs/ui/src/charts/line-chart.tsx— generic line chart component - Create barrel export at
libs/ui/src/charts/index.ts - Verify:
pnpm nx build websucceeds, bundle size is < 170 KB gz increase
Phase 2: Backend Analytics API (2–3 days)¶
- Create shared types in
libs/domain-contracts/src/api/analytics.api.ts - Update barrel export in
libs/domain-contracts/src/api/index.ts - Add composite indexes to
prisma/schema.prisma - Run Prisma migration:
pnpm prisma migrate dev --name add-analytics-composite-indexes - Create
apps/api/src/analytics/analytics.repository.ts - Create
apps/api/src/analytics/analytics.service.ts - Create
apps/api/src/analytics/dto/analytics-query.dto.ts - Create
apps/api/src/analytics/dto/analytics-response.dto.ts - Create
apps/api/src/analytics/analytics.controller.ts - Create
apps/api/src/analytics/analytics.module.ts - Import
AnalyticsModuleinapps/api/src/app.module.ts - Write backend unit tests (repository, service, controller)
- Verify:
pnpm nx build apisucceeds,pnpm nx test apipasses
Phase 3: Frontend — Enhanced Stat Cards + Chart Components (2–3 days)¶
- Enhance stat cards: Update
StatCardcomponent to acceptdeltaanddetailsprops - Enhance dashboard stats hook: Update
useDashboardStatsto consume enhanced API response with deltas - Enhance dashboard stats backend: Update
DashboardService.getStats()to compute comparison with yesterday - Update
apps/web/src/lib/api-client.tswithanalyticsnamespace - Create
apps/web/src/hooks/use-analytics.tswith TanStack Query hooks (donut hooks acceptperiodparam) - Create
apps/web/src/components/analytics/analytics-period-dropdown.tsx— shared dropdown component - Create
apps/web/src/components/analytics/order-status-chart.tsx - Create
apps/web/src/components/analytics/print-job-status-chart.tsx - Create
apps/web/src/components/analytics/shipment-status-chart.tsx - Create
apps/web/src/components/analytics/revenue-trend-chart.tsx(fixed: current week) - Create
apps/web/src/components/analytics/order-trend-chart.tsx(fixed: last 30 days) - Create barrel export at
apps/web/src/components/analytics/index.ts - Update
apps/web/src/pages/dashboard.tsx: replace stat cards + add chart rows (lazy-loaded) - Write frontend tests (hooks, component rendering)
- Verify:
pnpm nx build websucceeds,pnpm nx test webpasses
Phase 4: Advanced Charts — Optional (1–2 days)¶
- Add revenue breakdown endpoint + frontend component (nested donut)
- Add print success gauge component
- Add failure reasons rose chart (if failure categorization data exists)
Phase 5: Polish & QA (1 day)¶
- Responsive testing on mobile viewports
- Loading and empty state verification
- Click-to-filter navigation for donut slices
- Accessibility audit (aria labels, keyboard navigation)
- Bundle size verification:
pnpm nx build web --stats - Final build and lint pass:
pnpm nx run-many --target=build --allandpnpm nx run-many --target=lint --all
📊 Expected Output¶
Verification Commands¶
# Install dependencies
pnpm install
# Run Prisma migration
pnpm prisma migrate dev --name add-analytics-composite-indexes
# Build
pnpm nx build api
pnpm nx build web
# Lint
pnpm nx lint api
pnpm nx lint web
# Test
pnpm nx test api
pnpm nx test web
# Full validation
pnpm nx run-many --target=build --all
pnpm nx run-many --target=lint --all
pnpm nx run-many --target=test --all
Success Criteria¶
- KPI tiles enhanced: 4 stat cards show trend indicators ("+3 from yesterday", "No change", etc.) matching the mockup
- Donut charts: 3 donut charts (orders, print jobs, shipments) with a shared period dropdown (Today / Last Week / Last Month / All Time, default: All Time)
- Period dropdown: Changing the dropdown re-fetches all 3 donut charts; does NOT affect KPI tiles or trend charts
- Order donut shows "500+" (or actual total) in center, with count + percentage per status slice
- Print jobs donut shows "Active: {count}" in center
- Shipments donut shows "In Transit: {count}" in center
- Revenue trend: bar chart shows current week (Mon–Sun) with summary value (e.g., "$4,600")
- Order trend: line chart shows last 30 days with total summary (e.g., "Total Orders: 1,850")
- All charts render in dark mode with the Forma3D theme
- Charts are responsive — donut row stacks vertically on mobile, trend row stacks vertically on mobile
- Loading spinners show while data fetches
- Empty state messages show when no data exists
- Clicking a donut slice navigates to the relevant filtered view
- Bundle size increase from ECharts is < 170 KB gzipped
- All backend endpoints return correct data scoped to the authenticated tenant
- Rest of dashboard untouched: System Health, Recent Orders, Active Print Jobs, Welcome Card all remain as-is
- No TypeScript errors, lint passes, all tests pass
🚫 Constraints and Rules¶
MUST DO¶
- Replace the 4 existing simple stat cards with enhanced KPI tiles (trend indicators, colored icons, comparison deltas)
- Enhance the backend dashboard stats endpoint to return comparison data (yesterday vs today)
- Match the mockup (
docs/03-architecture/research/assets/echarts-mockups/mockup-dashboard-full-layout.png) for layout, tile design, donut chart style, and trend chart style - Use on-demand ECharts imports (NOT
import * as echarts from 'echarts') - Follow the repository pattern (Prisma queries in repository, not service)
- Scope all analytics queries to
tenantId - Use shared types from
libs/domain-contractsfor API contracts - Apply
@RequirePermissionson the analytics controller - Register the
AnalyticsModuleinAppModule - Add composite database indexes before deploying
- Use
ReactEChartsCore(not the default export) with ourechartsinstance - Lazy-load chart components with
React.lazy()to avoid blocking initial render - Use the existing Tailwind design system for chart cards
- Donut charts: single shared period dropdown (Today / Last Week / Last Month / All Time), default All Time
- Trend charts: fixed timespans (current week for revenue, last 30 days for orders) — no period selector
MUST NOT¶
- Import the full
echartspackage (kills bundle size) - Put Prisma queries directly in the service (use repository layer)
- Hardcode
tenantId— always read from authenticated request - Use
any,ts-ignore, oreslint-disable - Use
console.login production code - Create a separate analytics page (charts go on the existing dashboard)
- Break the existing dashboard — System Health, Recent Orders, Active Print Jobs, and Welcome Card must remain untouched (only the 4 stat cards get replaced/enhanced)
- Add per-chart period tabs to individual donut charts (use one shared dropdown for all 3 instead)
- Use React class components
- Skip loading/empty states for charts
END OF PROMPT
This prompt implements a full analytics dashboard for Forma3D.Connect using Apache ECharts. It replaces the 4 basic stat cards with enhanced KPI tiles (with trend indicators), adds donut charts for order/print-job/shipment status distribution (with a shared period dropdown: Today / Last Week / Last Month / All Time), a weekly revenue bar chart, and a 30-day order trend line — transforming the basic stat-card dashboard into a visual operations center for 3D print farm management. The rest of the dashboard (System Health, Recent Orders, Active Print Jobs, Welcome Card) remains untouched.