Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
29 KiB
29 KiB
Billing Dashboard Hierarchical Report System - Progress Tracker
Current Status: Designing Core Infrastructure
Completed Tasks
- Research existing report patterns in codebase
- Analyze current billing dashboard structure
- Design high-level hierarchical report architecture
- Identify integration points with existing infrastructure
= In Progress Tasks
- Define named reports structure for billing dashboard
- Design core report infrastructure components
- Create billing dashboard report definitions
=<3D> Upcoming Tasks
- Plan migration strategy from existing patterns
- Update project documentation with reports approach
- Create implementation roadmap
Hierarchical Report System Design
Core Architecture
server/src/lib/reports/
core/
ReportEngine.ts # Main execution engine
ReportDefinition.ts # Report metadata structure
ReportCache.ts # Redis-based caching
MetricCalculator.ts # Metric computation utilities
types.ts # Shared TypeScript interfaces
definitions/
billing/
overview.ts # Billing dashboard overview
revenue-analysis.ts # Revenue trends and analysis
client-performance.ts # Client billing metrics
service-utilization.ts# Service performance metrics
index.ts # Export all billing reports
operations/
time-utilization.ts # Time tracking analytics
asset-performance.ts # Asset utilization reports
index.ts
financial/
accounts-receivable.ts# AR aging and analysis
credit-analysis.ts # Credit usage and trends
index.ts
index.ts # Export all report definitions
builders/
QueryBuilder.ts # Dynamic SQL query construction
FilterBuilder.ts # Dynamic filtering logic
AggregationBuilder.ts # Data aggregation utilities
DateRangeBuilder.ts # Date range calculations
actions/
executeReport.ts # Universal report executor
getReportMetadata.ts # Report definition retrieval
validateReportAccess.ts # Permission checking
index.ts # Export all actions
Key Design Principles
- Named Reports: Each report has a unique identifier (e.g., 'billing.overview')
- Declarative Configuration: Reports defined as configuration objects, not code
- Reusable Components: Metrics can be shared across multiple reports
- Performance Optimized: Built-in caching and query optimization
- Security First: Tenant isolation and permission checking
- Type Safe: Full TypeScript support with strong typing
Core Interface Definitions
Report Definition Structure
export interface ReportDefinition {
id: string; // Unique identifier (e.g., 'billing.overview')
name: string; // Human-readable name
description: string; // Report description
category: ReportCategory; // Category for organization
version: string; // Version for compatibility
metrics: MetricDefinition[]; // List of metrics to calculate
parameters?: ParameterDefinition[]; // Optional input parameters
permissions: { // Access control
roles: string[]; // Required roles
resources: string[]; // Required resource permissions
};
caching?: { // Caching configuration
ttl: number; // Time to live in seconds
key: string; // Cache key template
invalidateOn?: string[]; // Events that invalidate cache
};
scheduling?: { // For scheduled reports
frequency: string; // Cron expression
enabled: boolean;
};
}
export interface MetricDefinition {
id: string; // Metric identifier
name: string; // Display name
description?: string; // Optional description
type: MetricType; // Type of metric (count, sum, avg, etc.)
query: QueryDefinition; // How to calculate the metric
formatting?: FormattingOptions; // Display formatting
dependencies?: string[]; // Other metrics this depends on
conditions?: ConditionDefinition[]; // Conditional logic
}
export interface QueryDefinition {
table: string; // Primary table
joins?: JoinDefinition[]; // Table joins
fields?: string[]; // Fields to select
aggregation?: AggregationType; // Aggregation method
filters?: FilterDefinition[]; // Where conditions
groupBy?: string[]; // Group by fields
orderBy?: OrderDefinition[]; // Sorting
limit?: number; // Result limit
}
export type MetricType = 'count' | 'sum' | 'average' | 'min' | 'max' | 'ratio' | 'trend';
export type AggregationType = 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct';
export type ReportCategory = 'billing' | 'operations' | 'financial' | 'analytics' | 'compliance';
Billing Dashboard Report Definitions
1. Billing Overview Report
// definitions/billing/overview.ts
export const billingOverviewReport: ReportDefinition = {
id: 'billing.overview',
name: 'Billing Dashboard Overview',
description: 'Core metrics for the billing dashboard overview tab',
category: 'billing',
version: '1.0.0',
permissions: {
roles: ['admin', 'billing_manager', 'account_manager'],
resources: ['billing.read']
},
metrics: [
{
id: 'active_plans_count',
name: 'Active Contract Lines',
description: 'Count of currently active contract lines',
type: 'count',
query: {
table: 'contract_lines',
filters: [
{ field: 'is_active', operator: 'eq', value: true },
{ field: 'tenant', operator: 'eq', value: '{{tenant}}' }
]
},
formatting: {
type: 'number',
decimals: 0
}
},
{
id: 'active_clients_count',
name: 'Active Billing Clients',
description: 'Count of companies with active contract lines',
type: 'count',
query: {
table: 'companies',
joins: [
{
type: 'inner',
table: 'client_contract_lines',
on: [
{ left: 'companies.company_id', right: 'client_contract_lines.client_id' },
{ left: 'companies.tenant', right: 'client_contract_lines.tenant' }
]
}
],
aggregation: 'count_distinct',
fields: ['companies.company_id'],
filters: [
{ field: 'client_contract_lines.is_active', operator: 'eq', value: true },
{ field: 'companies.tenant', operator: 'eq', value: '{{tenant}}' }
]
},
formatting: {
type: 'number',
decimals: 0
}
},
{
id: 'monthly_revenue',
name: 'Current Month Revenue',
description: 'Total revenue for the current month',
type: 'sum',
query: {
table: 'invoices',
fields: ['total_amount'],
aggregation: 'sum',
filters: [
{ field: 'tenant', operator: 'eq', value: '{{tenant}}' },
{ field: 'status', operator: 'in', value: ['paid', 'completed'] },
{ field: 'invoice_date', operator: 'gte', value: '{{start_of_month}}' },
{ field: 'invoice_date', operator: 'lt', value: '{{end_of_month}}' }
]
},
formatting: {
type: 'currency',
currency: 'USD',
divisor: 100 // Convert from cents
}
},
{
id: 'active_services_count',
name: 'Active Services',
description: 'Count of services in the service catalog',
type: 'count',
query: {
table: 'service_catalog',
filters: [
{ field: 'tenant', operator: 'eq', value: '{{tenant}}' }
]
},
formatting: {
type: 'number',
decimals: 0
}
},
{
id: 'outstanding_amount',
name: 'Outstanding Invoices',
description: 'Total amount of unpaid invoices',
type: 'sum',
query: {
table: 'invoices',
fields: ['total_amount - COALESCE(credit_applied, 0) as outstanding'],
aggregation: 'sum',
filters: [
{ field: 'tenant', operator: 'eq', value: '{{tenant}}' },
{ field: 'status', operator: 'in', value: ['open', 'overdue', 'sent'] }
]
},
formatting: {
type: 'currency',
currency: 'USD',
divisor: 100
}
},
{
id: 'total_credit_balance',
name: 'Total Credit Balance',
description: 'Sum of all company credit balances',
type: 'sum',
query: {
table: 'companies',
fields: ['credit_balance'],
aggregation: 'sum',
filters: [
{ field: 'tenant', operator: 'eq', value: '{{tenant}}' },
{ field: 'credit_balance', operator: 'gt', value: 0 }
]
},
formatting: {
type: 'currency',
currency: 'USD',
divisor: 100
}
},
{
id: 'pending_time_entries',
name: 'Pending Time Entries',
description: 'Count of time entries awaiting approval',
type: 'count',
query: {
table: 'time_entries',
filters: [
{ field: 'tenant', operator: 'eq', value: '{{tenant}}' },
{ field: 'approval_status', operator: 'eq', value: 'pending' }
]
},
formatting: {
type: 'number',
decimals: 0
}
},
{
id: 'monthly_billable_hours',
name: 'Current Month Billable Hours',
description: 'Total billable hours for the current month',
type: 'sum',
query: {
table: 'time_entries',
fields: ['billable_duration'],
aggregation: 'sum',
filters: [
{ field: 'tenant', operator: 'eq', value: '{{tenant}}' },
{ field: 'billable', operator: 'eq', value: true },
{ field: 'start_time', operator: 'gte', value: '{{start_of_month}}' },
{ field: 'start_time', operator: 'lt', value: '{{end_of_month}}' }
]
},
formatting: {
type: 'duration',
unit: 'hours',
decimals: 1
}
}
],
caching: {
ttl: 300, // 5 minutes
key: 'billing.overview.{{tenant}}',
invalidateOn: ['invoice.created', 'invoice.updated', 'billing_plan.updated']
}
};
Core Report Infrastructure Implementation
ReportEngine Implementation
// core/ReportEngine.ts
'use server';
import { createTenantKnex } from 'server/src/lib/db';
import { withTransaction } from '@shared/db';
import { ReportDefinition, ReportResult, ReportParameters } from './types';
import { QueryBuilder } from '../builders/QueryBuilder';
import { ReportCache } from './ReportCache';
import { validateReportAccess } from '../actions/validateReportAccess';
export class ReportEngine {
static async execute(
definition: ReportDefinition,
parameters: ReportParameters = {},
options: { skipCache?: boolean } = {}
): Promise<ReportResult> {
const startTime = Date.now();
// 1. Validate access permissions
await validateReportAccess(definition.id);
// 2. Check cache first (unless skipped)
if (!options.skipCache && definition.caching) {
const cached = await ReportCache.get(definition, parameters);
if (cached) {
return cached;
}
}
// 3. Get database connection with tenant context
const { knex, tenant } = await createTenantKnex();
if (!tenant) {
throw new Error('Tenant context is required for report execution');
}
// 4. Add tenant to parameters
const enrichedParameters = {
...parameters,
tenant,
start_of_month: this.getStartOfMonth(),
end_of_month: this.getEndOfMonth(),
start_of_year: this.getStartOfYear(),
end_of_year: this.getEndOfYear()
};
// 5. Execute report within transaction
const result = await withTransaction(knex, async (trx) => {
const metrics: Record<string, any> = {};
// Execute each metric calculation
for (const metric of definition.metrics) {
try {
const value = await this.executeMetric(trx, metric, enrichedParameters);
metrics[metric.id] = this.formatMetricValue(value, metric.formatting);
} catch (error) {
console.error(`Error executing metric ${metric.id}:`, error);
metrics[metric.id] = null;
}
}
return {
reportId: definition.id,
reportName: definition.name,
executedAt: new Date().toISOString(),
parameters: enrichedParameters,
metrics,
metadata: {
version: definition.version,
category: definition.category,
executionTime: Date.now() - startTime
}
} as ReportResult;
});
// 6. Cache the result
if (definition.caching) {
await ReportCache.set(definition, parameters, result);
}
return result;
}
private static async executeMetric(
trx: any,
metric: MetricDefinition,
parameters: ReportParameters
): Promise<any> {
// Build the query using QueryBuilder
const query = QueryBuilder.build(trx, metric.query, parameters);
// Execute and return result
const result = await query;
// Handle different aggregation types
if (metric.query.aggregation) {
return result[0]?.[metric.query.aggregation] || 0;
}
return result;
}
private static formatMetricValue(value: any, formatting?: FormattingOptions): any {
if (!formatting || value === null || value === undefined) {
return value;
}
switch (formatting.type) {
case 'currency':
return {
raw: value,
formatted: this.formatCurrency(value, formatting),
type: 'currency'
};
case 'number':
return {
raw: value,
formatted: this.formatNumber(value, formatting),
type: 'number'
};
case 'duration':
return {
raw: value,
formatted: this.formatDuration(value, formatting),
type: 'duration'
};
default:
return value;
}
}
private static formatCurrency(value: number, formatting: FormattingOptions): string {
const amount = formatting.divisor ? value / formatting.divisor : value;
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: formatting.currency || 'USD'
}).format(amount);
}
private static formatNumber(value: number, formatting: FormattingOptions): string {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: formatting.decimals || 0,
maximumFractionDigits: formatting.decimals || 0
}).format(value);
}
private static formatDuration(minutes: number, formatting: FormattingOptions): string {
if (formatting.unit === 'hours') {
const hours = minutes / 60;
return `${hours.toFixed(formatting.decimals || 1)} hours`;
}
return `${minutes} minutes`;
}
private static getStartOfMonth(): string {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
}
private static getEndOfMonth(): string {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth() + 1, 1).toISOString();
}
private static getStartOfYear(): string {
const now = new Date();
return new Date(now.getFullYear(), 0, 1).toISOString();
}
private static getEndOfYear(): string {
const now = new Date();
return new Date(now.getFullYear() + 1, 0, 1).toISOString();
}
}
QueryBuilder Implementation
// builders/QueryBuilder.ts
import { Knex } from 'knex';
import { QueryDefinition, ReportParameters, FilterDefinition } from '../core/types';
export class QueryBuilder {
static build(
trx: Knex.Transaction,
queryDef: QueryDefinition,
parameters: ReportParameters
): Knex.QueryBuilder {
let query = trx(queryDef.table);
// Add joins
if (queryDef.joins) {
queryDef.joins.forEach(join => {
query = query.join(join.table, builder => {
join.on.forEach(condition => {
builder.on(condition.left, condition.right);
});
});
});
}
// Add field selection
if (queryDef.fields) {
query = query.select(queryDef.fields);
} else if (queryDef.aggregation) {
query = query.select(trx.raw(`${queryDef.aggregation}(*) as ${queryDef.aggregation}`));
}
// Add filters
if (queryDef.filters) {
queryDef.filters.forEach(filter => {
query = this.applyFilter(query, filter, parameters);
});
}
// Add group by
if (queryDef.groupBy) {
query = query.groupBy(queryDef.groupBy);
}
// Add order by
if (queryDef.orderBy) {
queryDef.orderBy.forEach(order => {
query = query.orderBy(order.field, order.direction || 'asc');
});
}
// Add limit
if (queryDef.limit) {
query = query.limit(queryDef.limit);
}
return query;
}
private static applyFilter(
query: Knex.QueryBuilder,
filter: FilterDefinition,
parameters: ReportParameters
): Knex.QueryBuilder {
const value = this.resolveFilterValue(filter.value, parameters);
switch (filter.operator) {
case 'eq':
return query.where(filter.field, value);
case 'neq':
return query.whereNot(filter.field, value);
case 'gt':
return query.where(filter.field, '>', value);
case 'gte':
return query.where(filter.field, '>=', value);
case 'lt':
return query.where(filter.field, '<', value);
case 'lte':
return query.where(filter.field, '<=', value);
case 'in':
return query.whereIn(filter.field, Array.isArray(value) ? value : [value]);
case 'not_in':
return query.whereNotIn(filter.field, Array.isArray(value) ? value : [value]);
case 'like':
return query.where(filter.field, 'like', value);
case 'is_null':
return query.whereNull(filter.field);
case 'is_not_null':
return query.whereNotNull(filter.field);
default:
throw new Error(`Unsupported filter operator: ${filter.operator}`);
}
}
private static resolveFilterValue(value: any, parameters: ReportParameters): any {
if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) {
const paramName = value.slice(2, -2);
return parameters[paramName];
}
return value;
}
}
Universal Report Action
// actions/executeReport.ts
'use server';
import { z } from 'zod';
import { ReportEngine } from '../core/ReportEngine';
import { getReportDefinition } from './getReportDefinition';
import { ReportResult } from '../core/types';
const ExecuteReportSchema = z.object({
reportId: z.string(),
parameters: z.record(z.any()).optional().default({}),
options: z.object({
skipCache: z.boolean().optional(),
forceRefresh: z.boolean().optional()
}).optional().default({})
});
export async function executeReport(
input: z.infer<typeof ExecuteReportSchema>
): Promise<ReportResult> {
// Validate input
const validationResult = ExecuteReportSchema.safeParse(input);
if (!validationResult.success) {
const errorMessages = validationResult.error.errors.map(e =>
`${e.path.join('.')}: ${e.message}`
).join(', ');
throw new Error(`Validation Error: ${errorMessages}`);
}
const { reportId, parameters, options } = validationResult.data;
try {
// Get report definition
const definition = await getReportDefinition(reportId);
if (!definition) {
throw new Error(`Report definition not found: ${reportId}`);
}
// Execute the report
const result = await ReportEngine.execute(definition, parameters, options);
return result;
} catch (error) {
console.error(`Error executing report ${reportId}:`, error);
throw new Error(`Failed to execute report: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Convenience function for billing overview
export async function getBillingOverview(): Promise<ReportResult> {
return executeReport({ reportId: 'billing.overview' });
}
Overview Component Integration Plan
// Updated Overview.tsx structure
'use client'
import React, { useState, useEffect } from 'react';
import { getBillingOverview } from 'server/src/lib/reports/actions';
import { ReportResult } from 'server/src/lib/reports/core/types';
import { Card, CardHeader, CardContent } from 'server/src/components/ui/Card';
interface MetricCardProps {
title: string;
value: any;
icon: React.ComponentType;
loading?: boolean;
}
const MetricCard: React.FC<MetricCardProps> = ({ title, value, icon: Icon, loading }) => {
const displayValue = loading ? '...' : (value?.formatted || value);
return (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold">{title}</h3>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-4">
<div className="p-3 rounded-full bg-primary-50">
<Icon className="h-6 w-6 text-primary-500" />
</div>
<div>
<p className="text-2xl font-bold">{displayValue}</p>
<p className="text-sm text-gray-500">{title}</p>
</div>
</div>
</CardContent>
</Card>
);
};
const Overview = () => {
const [reportData, setReportData] = useState<ReportResult | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchBillingOverview() {
try {
setLoading(true);
const data = await getBillingOverview();
setReportData(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load billing data');
} finally {
setLoading(false);
}
}
fetchBillingOverview();
}, []);
if (error) {
return <div className="text-red-600">Error: {error}</div>;
}
const metrics = reportData?.metrics || {};
return (
<div className="space-y-6">
{/* Billing Summary Section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard
title="Active Contract Lines"
value={metrics.active_plans_count}
icon={FileSpreadsheet}
loading={loading}
/>
<MetricCard
title="Billing Clients"
value={metrics.active_clients_count}
icon={Building2}
loading={loading}
/>
<MetricCard
title="Monthly Revenue"
value={metrics.monthly_revenue}
icon={DollarSign}
loading={loading}
/>
</div>
{/* Additional metrics... */}
{/* Execution metadata for debugging */}
{reportData && process.env.NODE_ENV === 'development' && (
<div className="text-xs text-gray-400">
Report executed at: {reportData.executedAt}
(took {reportData.metadata.executionTime}ms)
</div>
)}
</div>
);
};
Next Steps
Immediate Tasks (This Session)
- Define billing overview report structure
- = Design core ReportEngine implementation
- = Create executeReport server action
- = Plan integration with existing Overview component
Short Term (Next 1-2 Days)
- Implement core report infrastructure
- Create additional billing report definitions
- Build query execution engine
- Add caching layer
Medium Term (Next Week)
- Migrate existing report actions to new system
- Update billing dashboard to use reports
- Add advanced filtering and parameters
- Implement permission checking
Long Term (Next Month)
- Extend to other dashboards
- Add scheduled reporting
- Create report builder UI
- Add export capabilities
Technical Decisions Made
- Report IDs: Use dot notation (e.g., 'billing.overview') for hierarchical organization
- Caching: Redis-based with configurable TTL and smart invalidation
- Security: Role and resource-based permissions with tenant isolation
- Flexibility: Declarative configuration allows easy modification without code changes
- Performance: Query optimization and built-in aggregation support
- Integration: Designed to work with existing server action patterns
Questions/Considerations
- Should we support real-time updates via WebSocket/SSE for critical metrics?
- How do we handle report versioning for backwards compatibility?
- Should we implement a report builder UI for non-technical users?
- What's the strategy for handling very large datasets (pagination, streaming)?
- How do we handle cross-tenant reporting for enterprise features?
Migration Strategy & Implementation Status
Phase 1: Core Infrastructure (Week 1)
Steps:
- Create report structure - Set up
/server/src/lib/reports/directory - Implement core classes - ReportEngine, QueryBuilder, basic types
- Add basic actions - executeReport with simple validation
- Create billing overview report - Define 'billing.overview' report
- Test infrastructure - Unit tests for core components
Phase 2: Billing Dashboard Integration (Week 2)
Steps:
- Update Overview component - Integrate getBillingOverview()
- Add loading states - Skeleton loading and error handling
- Implement caching - Redis-based caching for performance
- Add permission checking - Ensure proper access control
- Performance optimization - Query optimization and indexing
Phase 3: Extend to Other Dashboards (Week 3)
Existing Actions to Migrate:
getHoursByServiceType→ 'operations.hours-by-service'getRecentCompanyInvoices→ 'billing.recent-invoices'getRemainingBucketUnits→ 'billing.bucket-usage'getUsageDataMetrics→ 'operations.usage-metrics'
Phase 4: Advanced Features (Week 4)
Steps:
- Scheduled reports - Background report generation
- Export capabilities - PDF/Excel export functionality
- Report builder UI - Non-technical user report creation
- Advanced caching - Smart cache invalidation
- Real-time updates - WebSocket integration for live metrics
✅ Session Completion Summary
Design Work Completed
- ✅ Report system architecture design
- ✅ Core interface definitions
- ✅ Billing overview report specification
- ✅ ReportEngine implementation design
- ✅ QueryBuilder implementation design
- ✅ Universal report action design
- ✅ Overview component integration plan
- ✅ Migration strategy planning
✅ Implementation Completed (Phase 1)
The core hierarchical report system has been successfully implemented! Here's what's been delivered:
Core Infrastructure ✅
- ✅ Complete directory structure (
/server/src/lib/reports/) - ✅ Comprehensive TypeScript interfaces and types
- ✅ ReportEngine class with full execution logic
- ✅ QueryBuilder utility for dynamic query construction
- ✅ ReportRegistry for managing report definitions
- ✅ Universal executeReport server action
- ✅ Billing overview report definition with 8 metrics
- ✅ Updated Overview component using real data
- ✅ Error handling and loading states
- ✅ TypeScript compilation fixes
Files Created:
server/src/lib/reports/
├── core/
│ ├── types.ts # Complete type definitions
│ ├── ReportEngine.ts # Core execution engine
│ ├── ReportRegistry.ts # Report management
│ └── index.ts # Exports
├── definitions/
│ └── billing/
│ ├── overview.ts # Billing overview report
│ └── index.ts # Exports
├── builders/
│ ├── QueryBuilder.ts # Query construction
│ └── index.ts # Exports
├── actions/
│ ├── executeReport.ts # Server actions
│ └── index.ts # Exports
├── index.ts # Main exports
└── test-reports.ts # Testing utilities
Component Updates:
- ✅
Overview.tsx- Now usesgetBillingOverview()instead of dummy data - ✅ Real-time loading states with spinner animations
- ✅ Error handling with user-friendly messages
- ✅ Formatted metric display (currency, numbers, duration)
- ✅ Development debug information
Key Features Implemented:
- Named Reports:
'billing.overview'report with 8 metrics - Dynamic Queries: Tenant filtering, date ranges, aggregations
- Type Safety: Full TypeScript support throughout
- Error Handling: Comprehensive error types and handling
- Formatting: Currency, number, duration formatting
- Extensibility: Easy to add new reports and metrics
Ready for Next Phase:
- Add Redis-based caching system
- Implement permission validation
- Add more billing reports (revenue trends, client analysis)
- Migrate existing report-actions to new system
- Performance optimization and monitoring
Last Updated: Implementation Phase 1 Complete Next Update: After Phase 2 (caching & permissions)