kamau1's picture
feat: add smart validation for activation requirements with 6 new field types and auto-generated validation rules
dddd6c6
# Reports API - Frontend Documentation
## Quick Start
```typescript
// 1. Generate a report
const response = await fetch('/api/v1/reports/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
report_type: 'custom',
date_range: {
start_date: '2025-12-01',
end_date: '2025-12-31'
},
custom_config: {
entity: 'tickets',
columns: ['status', 'priority'],
aggregations: [{ field: 'id', type: 'count', alias: 'count' }],
group_by: ['status', 'priority']
},
project_id: 'your-project-id'
})
});
const data = await response.json();
// Use data.data for your charts/tables
```
---
## Overview
The Reports API provides business intelligence reports with pre-calculated metrics and aggregations. Use this for dashboard analytics, performance tracking, and compliance monitoring.
**Base URL:** `/api/v1/reports`
**Authorization:**
- `view_reports` permission required for viewing
- `export_reports` permission required for CSV downloads
- Available to: PLATFORM_ADMIN, PROJECT_MANAGER, DISPATCHER, SALES_MANAGER
**Key Features:**
- ✅ Pre-defined reports (SLA, Performance, Financial, Inventory)
- ✅ Custom reports with flexible configuration
- ✅ Calculated fields (SLA violations, days to complete)
- ✅ Aggregations (count, sum, avg, min, max)
- ✅ CSV export for Excel
- ✅ Real-time data (no caching)
---
## TypeScript Interfaces
Copy these into your frontend project:
```typescript
// ============================================
// REQUEST TYPES
// ============================================
type ReportType =
| 'sla_compliance'
| 'user_performance'
| 'financial_summary'
| 'inventory_usage'
| 'custom';
type AggregationType = 'count' | 'sum' | 'avg' | 'min' | 'max';
interface DateRange {
start_date: string; // ISO date: "2025-12-01"
end_date: string; // ISO date: "2025-12-31"
}
interface AggregationConfig {
field: string;
type: AggregationType;
alias?: string;
}
interface CustomReportConfig {
entity: 'tickets' | 'timesheets' | 'payroll' | 'expenses' | 'users';
columns: string[];
calculated_fields?: string[];
aggregations?: AggregationConfig[];
group_by?: string[];
additional_filters?: Record<string, any>;
}
interface ReportRequest {
report_type: ReportType;
date_range: DateRange;
project_id?: string;
region_id?: string;
user_id?: string;
status?: string;
limit?: number;
skip?: number;
custom_config?: CustomReportConfig; // Required when report_type = 'custom'
}
// ============================================
// RESPONSE TYPES
// ============================================
interface ReportMeta {
generated_at: string;
record_count: number;
filters_applied: Record<string, any>;
report_type: string;
}
interface ReportResponse<T = any> {
meta: ReportMeta;
data: T[];
}
// Specific row types
interface SLAReportRow {
ticket_id: string;
ticket_number: string;
project_name: string;
ticket_type: string;
priority: string;
status: string;
created_at: string;
due_date: string | null;
completed_at: string | null;
is_violated: boolean;
violation_margin_hours: number;
assigned_agent: string;
customer_name: string;
}
interface UserPerformanceRow {
user_id: string;
user_name: string;
role: string;
tickets_assigned: number;
tickets_completed: number;
completion_rate: number;
total_hours_logged: number;
avg_resolution_time_hours: number;
sla_violations_count: number;
on_time_percentage: number;
}
interface FinancialReportRow {
category: string;
description: string;
date: string;
project_name: string;
amount: number;
status: string;
transaction_type: 'Credit' | 'Debit';
}
interface InventoryUsageRow {
item_name: string;
serial_number: string | null;
category: string;
used_at: string;
ticket_id: string;
project_name: string;
used_by_user: string;
unit_cost: number;
}
// Custom report rows are dynamic
type CustomReportRow = Record<string, any>;
```
---
## Endpoints
### 1. Generate Report (JSON)
Returns report data as JSON for rendering in tables, charts, or dashboards.
```
POST /api/v1/reports/generate
```
**Headers:**
```
Authorization: Bearer {access_token}
Content-Type: application/json
```
**Request Body:**
```typescript
{
report_type: "sla_compliance" | "user_performance" | "financial_summary" | "inventory_usage";
date_range: {
start_date: string; // ISO date: "2025-12-01"
end_date: string; // ISO date: "2025-12-31"
};
project_id?: string; // UUID - optional filter
region_id?: string; // UUID - optional filter
user_id?: string; // UUID - optional filter
status?: string; // optional filter (e.g., "completed", "open")
limit?: number; // pagination limit (default: no limit)
skip?: number; // pagination offset (default: 0)
}
```
**Response (200 OK):**
```typescript
{
meta: {
generated_at: string; // ISO timestamp
record_count: number; // total records returned
filters_applied: object; // echo of filters used
report_type: string; // report type
};
data: Array<ReportRow>; // varies by report type (see below)
}
```
**Error Responses:**
- `401 Unauthorized` - Missing or invalid token
- `403 Forbidden` - User lacks `view_reports` permission
- `500 Internal Server Error` - Report generation failed
---
### 2. Export Report (CSV Download)
Downloads report data as CSV file for Excel or external processing.
```
POST /api/v1/reports/export
```
**Headers:**
```
Authorization: Bearer {access_token}
Content-Type: application/json
```
**Request Body:**
```typescript
{
report_type: "sla_compliance" | "user_performance" | "financial_summary" | "inventory_usage";
date_range: {
start_date: string; // ISO date: "2025-12-01"
end_date: string; // ISO date: "2025-12-31"
};
format?: "csv"; // only CSV supported currently
project_id?: string; // UUID - optional filter
region_id?: string; // UUID - optional filter
user_id?: string; // UUID - optional filter
status?: string; // optional filter
limit?: number; // pagination limit
skip?: number; // pagination offset
}
```
**Response (200 OK):**
- **Content-Type:** `text/csv`
- **Content-Disposition:** `attachment; filename="{report_type}_{timestamp}.csv"`
- **Body:** CSV file with UTF-8 BOM encoding (Excel-compatible)
**Error Responses:**
- `400 Bad Request` - Invalid format (only CSV supported)
- `401 Unauthorized` - Missing or invalid token
- `403 Forbidden` - User lacks `export_reports` permission
- `500 Internal Server Error` - Export failed
---
## Report Types & Data Shapes
### 1. Custom Report (NEW - Fully Configurable)
**Purpose:** Generate any report with flexible column selection, calculated fields, and aggregations
**Report Type:** `"custom"`
**Use Cases:**
- Ad-hoc data analysis
- Custom dashboard widgets
- Flexible reporting for any entity
- Aggregated statistics by any dimension
**Configuration:**
```typescript
{
report_type: "custom",
date_range: {
start_date: "2025-12-01",
end_date: "2025-12-31"
},
custom_config: {
entity: "tickets" | "timesheets" | "payroll" | "expenses" | "users",
columns: string[], // Fields to include
calculated_fields?: string[], // "sla_violated", "days_to_complete", "is_overdue"
aggregations?: Array<{
field: string,
type: "count" | "sum" | "avg" | "min" | "max",
alias?: string
}>,
group_by?: string[], // Group results by these fields
additional_filters?: Record<string, any> // Entity-specific filters
},
project_id?: string,
region_id?: string,
user_id?: string
}
```
**Example 1: Tickets by Status with Counts**
```json
{
"report_type": "custom",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-31"
},
"custom_config": {
"entity": "tickets",
"columns": ["status", "priority"],
"aggregations": [
{
"field": "id",
"type": "count",
"alias": "ticket_count"
}
],
"group_by": ["status", "priority"]
},
"project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a"
}
```
**Response:**
```json
{
"meta": {
"generated_at": "2025-12-14T20:00:00Z",
"record_count": 6,
"filters_applied": {...},
"report_type": "custom"
},
"data": [
{
"status": "completed",
"priority": "high",
"ticket_count": 15
},
{
"status": "completed",
"priority": "medium",
"ticket_count": 23
},
{
"status": "open",
"priority": "high",
"ticket_count": 8
}
]
}
```
**Example 2: Detailed Ticket List with Calculated Fields**
```json
{
"report_type": "custom",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-31"
},
"custom_config": {
"entity": "tickets",
"columns": [
"id",
"ticket_name",
"status",
"priority",
"created_at",
"completed_at"
],
"calculated_fields": [
"sla_violated",
"days_to_complete",
"is_overdue"
],
"additional_filters": {
"ticket_type": "installation"
}
},
"project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a",
"limit": 50
}
```
**Response:**
```json
{
"meta": {
"generated_at": "2025-12-14T20:00:00Z",
"record_count": 50,
"filters_applied": {...},
"report_type": "custom"
},
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"ticket_name": "Install fiber at ABC Corp",
"status": "completed",
"priority": "high",
"created_at": "2025-12-01T10:00:00Z",
"completed_at": "2025-12-04T15:30:00Z",
"sla_violated": false,
"days_to_complete": 3,
"is_overdue": false
}
]
}
```
**Example 3: Payroll Summary by User**
```json
{
"report_type": "custom",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-31"
},
"custom_config": {
"entity": "payroll",
"columns": ["user_id"],
"aggregations": [
{
"field": "total_amount",
"type": "sum",
"alias": "total_paid"
},
{
"field": "days_worked",
"type": "sum",
"alias": "total_days"
},
{
"field": "tickets_closed",
"type": "sum",
"alias": "total_tickets"
}
],
"group_by": ["user_id"]
},
"project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a"
}
```
**Response:**
```json
{
"meta": {
"generated_at": "2025-12-14T20:00:00Z",
"record_count": 12,
"filters_applied": {...},
"report_type": "custom"
},
"data": [
{
"user_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"total_paid": 45000.0,
"total_days": 20,
"total_tickets": 35
}
]
}
```
**Supported Entities:**
- `tickets` - Work orders/tickets
- `timesheets` - Daily attendance and work logs
- `payroll` - Worker compensation records
- `expenses` - Ticket expenses
- `users` - User records
**Supported Calculated Fields:**
- `sla_violated` - Whether SLA was breached (tickets)
- `days_to_complete` - Days from creation to completion (tickets)
- `is_overdue` - Whether ticket is overdue (tickets)
**Supported Aggregations:**
- `count` - Count records
- `sum` - Sum numeric field
- `avg` - Average of numeric field
- `min` - Minimum value
- `max` - Maximum value
**UI Implementation Tips:**
- Use for custom dashboard widgets
- Allow users to build their own reports
- Combine with date range picker for flexible analysis
- Use aggregations for summary statistics
- Use detailed mode (no aggregations) for drill-down
---
### 2. SLA Compliance Report
**Purpose:** Track ticket completion vs due dates, identify SLA violations
**Report Type:** `"sla_compliance"`
**Use Cases:**
- Dashboard SLA metrics
- Overdue ticket alerts
- Performance monitoring
- Customer satisfaction tracking
**Data Shape:**
```typescript
interface SLAReportRow {
ticket_id: string; // UUID
ticket_number: string; // Display ID (e.g., "TKT-12345")
project_name: string; // Project title
ticket_type: string; // "installation" | "support" | "infrastructure"
priority: string; // "low" | "medium" | "high" | "urgent"
status: string; // "open" | "in_progress" | "completed" | etc.
created_at: string; // ISO timestamp
due_date: string | null; // ISO timestamp
completed_at: string | null; // ISO timestamp
is_violated: boolean; // true if SLA was breached
violation_margin_hours: number; // negative = early, positive = late
assigned_agent: string; // Agent name or "Unassigned"
customer_name: string; // Customer name or "Internal/Infrastructure"
}
```
**Example Response:**
```json
{
"meta": {
"generated_at": "2025-12-14T18:30:00Z",
"record_count": 3,
"filters_applied": {
"report_type": "sla_compliance",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-14"
},
"project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a"
},
"report_type": "sla_compliance"
},
"data": [
{
"ticket_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"ticket_number": "TKT-12345",
"project_name": "Atomio FTTX",
"ticket_type": "installation",
"priority": "high",
"status": "completed",
"created_at": "2025-12-01T10:00:00Z",
"due_date": "2025-12-05T17:00:00Z",
"completed_at": "2025-12-04T15:30:00Z",
"is_violated": false,
"violation_margin_hours": -25.5,
"assigned_agent": "John Doe",
"customer_name": "ABC Corporation"
},
{
"ticket_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"ticket_number": "TKT-12346",
"project_name": "Atomio FTTX",
"ticket_type": "support",
"priority": "urgent",
"status": "completed",
"created_at": "2025-12-02T09:00:00Z",
"due_date": "2025-12-03T17:00:00Z",
"completed_at": "2025-12-04T10:00:00Z",
"is_violated": true,
"violation_margin_hours": 17.0,
"assigned_agent": "Jane Smith",
"customer_name": "XYZ Ltd"
}
]
}
```
**UI Implementation Tips:**
- Show `is_violated: true` tickets in red/warning color
- Display `violation_margin_hours` as "X hours late" (positive) or "X hours early" (negative)
- Filter by `priority` for urgent SLA violations
- Sort by `violation_margin_hours` descending to show worst violations first
---
### 2. User Performance Report
**Purpose:** Aggregate worker performance metrics from timesheets
**Report Type:** `"user_performance"`
**Use Cases:**
- Team performance dashboards
- Individual worker reviews
- Productivity tracking
- Resource allocation planning
**Data Shape:**
```typescript
interface UserPerformanceRow {
user_id: string; // UUID
user_name: string; // Full name
role: string; // "field_agent" | "project_manager" | etc.
tickets_assigned: number; // Total tickets assigned
tickets_completed: number; // Total tickets completed
completion_rate: number; // Percentage (0-100)
total_hours_logged: number; // Total hours from timesheets
avg_resolution_time_hours: number; // Average hours per ticket
sla_violations_count: number; // Number of SLA breaches
on_time_percentage: number; // Percentage completed on time (0-100)
}
```
**Example Response:**
```json
{
"meta": {
"generated_at": "2025-12-14T18:30:00Z",
"record_count": 2,
"filters_applied": {
"report_type": "user_performance",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-14"
}
},
"report_type": "user_performance"
},
"data": [
{
"user_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"user_name": "John Doe",
"role": "field_agent",
"tickets_assigned": 25,
"tickets_completed": 23,
"completion_rate": 92.0,
"total_hours_logged": 184.5,
"avg_resolution_time_hours": 8.02,
"sla_violations_count": 2,
"on_time_percentage": 91.3
},
{
"user_id": "d4e5f6a7-b8c9-0123-def1-234567890123",
"user_name": "Jane Smith",
"role": "field_agent",
"tickets_assigned": 30,
"tickets_completed": 28,
"completion_rate": 93.33,
"total_hours_logged": 210.0,
"avg_resolution_time_hours": 7.5,
"sla_violations_count": 1,
"on_time_percentage": 96.43
}
]
}
```
**UI Implementation Tips:**
- Show `completion_rate` as progress bar or percentage badge
- Highlight users with `on_time_percentage < 90%` in warning color
- Display `avg_resolution_time_hours` as "X.X hours per ticket"
- Sort by `completion_rate` or `on_time_percentage` for leaderboards
- Use `sla_violations_count` for quality metrics
---
### 3. Financial Summary Report
**Purpose:** Unified ledger of revenue (ProjectFinance) and expenses (TicketExpenses)
**Report Type:** `"financial_summary"`
**Use Cases:**
- Project financial tracking
- Budget monitoring
- Expense approval workflows
- Revenue vs cost analysis
**Data Shape:**
```typescript
interface FinancialReportRow {
category: string; // "General" | "Field Expense" | custom
description: string; // Transaction description
date: string; // ISO date (YYYY-MM-DD)
project_name: string; // Project title
amount: number; // Transaction amount (positive number)
status: string; // "Approved" | "Pending" | "Paid" | etc.
transaction_type: "Credit" | "Debit"; // Credit = revenue, Debit = expense
}
```
**Example Response:**
```json
{
"meta": {
"generated_at": "2025-12-14T18:30:00Z",
"record_count": 4,
"filters_applied": {
"report_type": "financial_summary",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-14"
},
"project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a"
},
"report_type": "financial_summary"
},
"data": [
{
"category": "General",
"description": "Client payment - Invoice #INV-001",
"date": "2025-12-01",
"project_name": "Atomio FTTX",
"amount": 150000.0,
"status": "Paid",
"transaction_type": "Credit"
},
{
"category": "Field Expense",
"description": "transport: Fuel for site visit",
"date": "2025-12-02",
"project_name": "Atomio FTTX",
"amount": 2500.0,
"status": "Approved",
"transaction_type": "Debit"
},
{
"category": "Field Expense",
"description": "materials: Fiber optic cable",
"date": "2025-12-03",
"project_name": "Atomio FTTX",
"amount": 15000.0,
"status": "Approved",
"transaction_type": "Debit"
}
]
}
```
**UI Implementation Tips:**
- Show `transaction_type: "Credit"` in green, `"Debit"` in red
- Calculate running balance: `balance += (Credit - Debit)`
- Group by `category` for expense breakdown charts
- Filter by `status` to show pending approvals
- Sort by `date` descending for recent transactions first
- Sum `amount` by `transaction_type` for totals
---
### 4. Inventory Usage Report
**Purpose:** Track inventory items installed/consumed across tickets
**Report Type:** `"inventory_usage"`
**Use Cases:**
- Inventory consumption tracking
- Cost analysis per project
- Equipment deployment monitoring
- Stock replenishment planning
**Data Shape:**
```typescript
interface InventoryUsageRow {
item_name: string; // Inventory item name
serial_number: string | null; // Serial number if tracked
category: string; // "General" | custom category
used_at: string; // ISO timestamp
ticket_id: string; // UUID of ticket where used
project_name: string; // Project title
used_by_user: string; // User name or "Unknown"
unit_cost: number; // Cost per unit
}
```
**Example Response:**
```json
{
"meta": {
"generated_at": "2025-12-14T18:30:00Z",
"record_count": 3,
"filters_applied": {
"report_type": "inventory_usage",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-14"
},
"project_id": "0ade6bd1-e492-4e25-b681-59f42058d29a"
},
"report_type": "inventory_usage"
},
"data": [
{
"item_name": "ONT Device - Model X200",
"serial_number": "ONT-2025-001234",
"category": "Customer Equipment",
"used_at": "2025-12-02T14:30:00Z",
"ticket_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"project_name": "Atomio FTTX",
"used_by_user": "John Doe",
"unit_cost": 5500.0
},
{
"item_name": "Fiber Optic Cable (100m)",
"serial_number": null,
"category": "Installation Materials",
"used_at": "2025-12-03T10:15:00Z",
"ticket_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"project_name": "Atomio FTTX",
"used_by_user": "Jane Smith",
"unit_cost": 12000.0
}
]
}
```
**UI Implementation Tips:**
- Group by `category` for inventory breakdown
- Sum `unit_cost` for total inventory value used
- Link `ticket_id` to ticket detail pages
- Show `serial_number` for trackable equipment
- Filter by `used_by_user` for individual usage tracking
- Sort by `used_at` descending for recent usage
---
## Choosing Between Report Types
### Use Pre-defined Reports When:
- ✅ You need standard business metrics (SLA compliance, user performance)
- ✅ You want consistent reporting across the organization
- ✅ You need optimized queries for common use cases
- ✅ You want pre-calculated fields and complex joins
**Pre-defined Reports:**
- `sla_compliance` - Ticket SLA tracking
- `user_performance` - Worker productivity metrics
- `financial_summary` - Revenue and expenses ledger
- `inventory_usage` - Equipment deployment tracking
### Use Custom Reports When:
- ✅ You need flexible column selection
- ✅ You want to analyze data by custom dimensions
- ✅ You need aggregations (count, sum, avg) by any field
- ✅ You want to build ad-hoc reports or custom dashboards
- ✅ Pre-defined reports don't meet your specific needs
**Custom Report Advantages:**
- Fully configurable columns
- Any entity (tickets, timesheets, payroll, expenses, users)
- Flexible aggregations and grouping
- Calculated fields on demand
- Entity-specific filtering
---
## Common Filters
All report types support these optional filters:
```typescript
interface ReportFilters {
project_id?: string; // Filter by specific project (UUID)
region_id?: string; // Filter by project region (UUID)
user_id?: string; // Filter by specific user (UUID)
status?: string; // Filter by status (varies by report type)
limit?: number; // Pagination: max records to return
skip?: number; // Pagination: records to skip
}
```
**Filter Behavior:**
- All filters are optional (omit for all data)
- Multiple filters are combined with AND logic
- `limit` and `skip` enable pagination for large datasets
- Managers automatically see only their project data (authorization-filtered)
---
## Error Handling
**Common Error Response Format:**
```typescript
{
detail: string; // Human-readable error message
}
```
**Error Scenarios:**
**401 Unauthorized:**
```json
{
"detail": "Not authenticated"
}
```
**403 Forbidden:**
```json
{
"detail": "User does not have permission to view reports"
}
```
**500 Internal Server Error:**
```json
{
"detail": "Report generation failed: [error details]"
}
```
---
## Frontend Implementation Guide
### React Hook Example
```typescript
// hooks/useReports.ts
import { useState } from 'react';
import { ReportRequest, ReportResponse } from '@/types/reports';
export const useReports = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const generateReport = async <T = any>(
request: ReportRequest
): Promise<ReportResponse<T> | null> => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/v1/reports/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to generate report');
}
const data = await response.json();
return data;
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
return null;
} finally {
setLoading(false);
}
};
const exportReport = async (request: ReportRequest) => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/v1/reports/export', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
});
if (!response.ok) {
throw new Error('Failed to export report');
}
// Trigger download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `report_${Date.now()}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
return { generateReport, exportReport, loading, error };
};
```
### React Component Example
```typescript
// components/TicketStatusChart.tsx
import { useEffect, useState } from 'react';
import { useReports } from '@/hooks/useReports';
import { CustomReportRow } from '@/types/reports';
interface Props {
projectId: string;
startDate: string;
endDate: string;
}
export const TicketStatusChart: React.FC<Props> = ({
projectId,
startDate,
endDate
}) => {
const { generateReport, loading, error } = useReports();
const [data, setData] = useState<CustomReportRow[]>([]);
useEffect(() => {
const fetchData = async () => {
const result = await generateReport<CustomReportRow>({
report_type: 'custom',
date_range: { start_date: startDate, end_date: endDate },
project_id: projectId,
custom_config: {
entity: 'tickets',
columns: ['status', 'priority'],
aggregations: [
{ field: 'id', type: 'count', alias: 'count' }
],
group_by: ['status', 'priority']
}
});
if (result) {
setData(result.data);
}
};
fetchData();
}, [projectId, startDate, endDate]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h3>Tickets by Status</h3>
{data.map((row, idx) => (
<div key={idx}>
{row.status} ({row.priority}): {row.count} tickets
</div>
))}
</div>
);
};
```
### Vue Composable Example
```typescript
// composables/useReports.ts
import { ref } from 'vue';
import type { ReportRequest, ReportResponse } from '@/types/reports';
export const useReports = () => {
const loading = ref(false);
const error = ref<string | null>(null);
const generateReport = async <T = any>(
request: ReportRequest
): Promise<ReportResponse<T> | null> => {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/v1/reports/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to generate report');
}
return await response.json();
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error';
return null;
} finally {
loading.value = false;
}
};
const exportReport = async (request: ReportRequest) => {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/v1/reports/export', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
});
if (!response.ok) {
throw new Error('Failed to export report');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `report_${Date.now()}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error';
} finally {
loading.value = false;
}
};
return { generateReport, exportReport, loading, error };
};
```
### API Service Class (Framework Agnostic)
```typescript
// services/reportsService.ts
import { ReportRequest, ReportResponse } from '@/types/reports';
class ReportsService {
private baseUrl = '/api/v1/reports';
private getHeaders() {
return {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
'Content-Type': 'application/json'
};
}
async generate<T = any>(request: ReportRequest): Promise<ReportResponse<T>> {
const response = await fetch(`${this.baseUrl}/generate`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(request)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to generate report');
}
return response.json();
}
async export(request: ReportRequest): Promise<void> {
const response = await fetch(`${this.baseUrl}/export`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(request)
});
if (!response.ok) {
throw new Error('Failed to export report');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `report_${Date.now()}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
// Helper: Generate SLA report
async getSLAReport(projectId: string, startDate: string, endDate: string) {
return this.generate({
report_type: 'sla_compliance',
date_range: { start_date: startDate, end_date: endDate },
project_id: projectId
});
}
// Helper: Generate custom aggregation report
async getAggregationReport(
entity: string,
columns: string[],
aggregations: any[],
groupBy: string[],
filters: any
) {
return this.generate({
report_type: 'custom',
date_range: filters.date_range,
project_id: filters.project_id,
custom_config: {
entity,
columns,
aggregations,
group_by: groupBy
}
});
}
}
export const reportsService = new ReportsService();
```
### 1. Report Dashboard Page
```typescript
// Example: Fetch SLA Compliance Report
const fetchSLAReport = async (projectId: string, startDate: string, endDate: string) => {
const response = await fetch('/api/v1/reports/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
report_type: 'sla_compliance',
date_range: {
start_date: startDate,
end_date: endDate
},
project_id: projectId
})
});
if (!response.ok) {
throw new Error('Failed to fetch report');
}
return await response.json();
};
```
### 2. CSV Export Button
```typescript
// Example: Download CSV Export
const downloadReport = async (reportType: string, filters: any) => {
const response = await fetch('/api/v1/reports/export', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
report_type: reportType,
...filters
})
});
if (!response.ok) {
throw new Error('Failed to export report');
}
// Trigger download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${reportType}_${new Date().toISOString()}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
};
```
### 3. Date Range Picker
```typescript
// Example: Default to current week
const getDefaultDateRange = () => {
const today = new Date();
const monday = new Date(today);
monday.setDate(today.getDate() - today.getDay() + 1);
const sunday = new Date(monday);
sunday.setDate(monday.getDate() + 6);
return {
start_date: monday.toISOString().split('T')[0],
end_date: sunday.toISOString().split('T')[0]
};
};
```
### 4. Report Type Selector
```typescript
const reportTypes = [
{ value: 'sla_compliance', label: 'SLA Compliance', icon: '⏱️' },
{ value: 'user_performance', label: 'User Performance', icon: '📊' },
{ value: 'financial_summary', label: 'Financial Summary', icon: '💰' },
{ value: 'inventory_usage', label: 'Inventory Usage', icon: '📦' }
];
```
---
## Performance Notes
- Reports are designed for **sub-2 second** response times
- Use `limit` and `skip` for pagination on large datasets
- User Performance report uses aggregated timesheet data (fast)
- SLA Compliance may be slower for large date ranges (complex joins)
- Consider caching report results for frequently-accessed data
---
## Custom Report Examples
### Example 1: Ticket Status Dashboard
**Goal:** Show ticket counts by status and priority for current month
```typescript
const response = await fetch('/api/v1/reports/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
report_type: 'custom',
date_range: {
start_date: '2025-12-01',
end_date: '2025-12-31'
},
custom_config: {
entity: 'tickets',
columns: ['status', 'priority'],
aggregations: [
{ field: 'id', type: 'count', alias: 'count' }
],
group_by: ['status', 'priority']
},
project_id: projectId
})
});
// Use for stacked bar chart or heatmap
```
### Example 2: Worker Performance Leaderboard
**Goal:** Rank workers by tickets completed this week
```typescript
const response = await fetch('/api/v1/reports/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
report_type: 'custom',
date_range: {
start_date: '2025-12-08',
end_date: '2025-12-14'
},
custom_config: {
entity: 'timesheets',
columns: ['user_id'],
aggregations: [
{ field: 'tickets_completed', type: 'sum', alias: 'total_completed' },
{ field: 'hours_worked', type: 'sum', alias: 'total_hours' }
],
group_by: ['user_id']
},
project_id: projectId
})
});
// Sort by total_completed descending for leaderboard
```
### Example 3: Expense Analysis by Category
**Goal:** Breakdown expenses by type for budget tracking
```typescript
const response = await fetch('/api/v1/reports/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
report_type: 'custom',
date_range: {
start_date: '2025-12-01',
end_date: '2025-12-31'
},
custom_config: {
entity: 'expenses',
columns: ['expense_type'],
aggregations: [
{ field: 'total_cost', type: 'sum', alias: 'total_amount' },
{ field: 'id', type: 'count', alias: 'expense_count' }
],
group_by: ['expense_type'],
additional_filters: {
is_approved: true
}
},
project_id: projectId
})
});
// Use for pie chart or expense breakdown table
```
### Example 4: Overdue Tickets Report
**Goal:** List all overdue tickets with details
```typescript
const response = await fetch('/api/v1/reports/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
report_type: 'custom',
date_range: {
start_date: '2025-01-01',
end_date: '2025-12-31'
},
custom_config: {
entity: 'tickets',
columns: [
'id',
'ticket_name',
'priority',
'status',
'due_date',
'created_at'
],
calculated_fields: ['is_overdue', 'days_to_complete'],
additional_filters: {
status: 'open'
}
},
project_id: projectId
})
});
// Filter client-side for is_overdue === true
// Sort by due_date ascending
```
---
## Testing
**Test with cURL:**
```bash
# Generate SLA Report
curl -X POST "http://localhost:7860/api/v1/reports/generate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"report_type": "sla_compliance",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-14"
}
}'
# Generate Custom Report
curl -X POST "http://localhost:7860/api/v1/reports/generate" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"report_type": "custom",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-14"
},
"custom_config": {
"entity": "tickets",
"columns": ["status", "priority"],
"aggregations": [
{"field": "id", "type": "count", "alias": "count"}
],
"group_by": ["status", "priority"]
}
}'
# Export to CSV
curl -X POST "http://localhost:7860/api/v1/reports/export" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"report_type": "user_performance",
"date_range": {
"start_date": "2025-12-01",
"end_date": "2025-12-14"
}
}' \
--output report.csv
```
---
## Questions?
Contact the backend team or check:
- API Swagger docs: `/docs`
- Source code: `src/app/api/v1/reports.py`
- Service logic: `src/app/services/report_service.py`
---
## Common Integration Patterns
### Pattern 1: Dashboard Widget with Auto-Refresh
```typescript
// Auto-refresh every 5 minutes
useEffect(() => {
const fetchData = async () => {
const result = await reportsService.generate({
report_type: 'custom',
date_range: { start_date: startDate, end_date: endDate },
project_id: projectId,
custom_config: {
entity: 'tickets',
columns: ['status'],
aggregations: [{ field: 'id', type: 'count', alias: 'count' }],
group_by: ['status']
}
});
setData(result.data);
};
fetchData();
const interval = setInterval(fetchData, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [projectId, startDate, endDate]);
```
### Pattern 2: Report Builder UI
```typescript
interface ReportBuilderState {
entity: string;
selectedColumns: string[];
aggregations: AggregationConfig[];
groupBy: string[];
filters: Record<string, any>;
}
const ReportBuilder = () => {
const [config, setConfig] = useState<ReportBuilderState>({
entity: 'tickets',
selectedColumns: [],
aggregations: [],
groupBy: [],
filters: {}
});
const handleGenerate = async () => {
const result = await reportsService.generate({
report_type: 'custom',
date_range: config.filters.date_range,
project_id: config.filters.project_id,
custom_config: {
entity: config.entity,
columns: config.selectedColumns,
aggregations: config.aggregations,
group_by: config.groupBy
}
});
// Display result
};
return (
<div>
<EntitySelector value={config.entity} onChange={...} />
<ColumnSelector columns={config.selectedColumns} onChange={...} />
<AggregationBuilder aggregations={config.aggregations} onChange={...} />
<button onClick={handleGenerate}>Generate Report</button>
</div>
);
};
```
### Pattern 3: Cached Reports with React Query
```typescript
import { useQuery } from '@tanstack/react-query';
const useTicketStatusReport = (projectId: string, dateRange: DateRange) => {
return useQuery({
queryKey: ['report', 'ticket-status', projectId, dateRange],
queryFn: () => reportsService.generate({
report_type: 'custom',
date_range: dateRange,
project_id: projectId,
custom_config: {
entity: 'tickets',
columns: ['status', 'priority'],
aggregations: [{ field: 'id', type: 'count', alias: 'count' }],
group_by: ['status', 'priority']
}
}),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000 // 10 minutes
});
};
// Usage
const { data, isLoading, error } = useTicketStatusReport(projectId, dateRange);
```
### Pattern 4: Export with Loading State
```typescript
const ExportButton = ({ reportConfig }: { reportConfig: ReportRequest }) => {
const [exporting, setExporting] = useState(false);
const handleExport = async () => {
setExporting(true);
try {
await reportsService.export(reportConfig);
toast.success('Report exported successfully');
} catch (error) {
toast.error('Failed to export report');
} finally {
setExporting(false);
}
};
return (
<button onClick={handleExport} disabled={exporting}>
{exporting ? 'Exporting...' : 'Export to CSV'}
</button>
);
};
```
---
## Troubleshooting
### Issue: 403 Forbidden
**Cause:** User lacks `view_reports` or `export_reports` permission
**Solution:** Check user role - only PLATFORM_ADMIN, PROJECT_MANAGER, DISPATCHER, SALES_MANAGER have access
### Issue: 422 Validation Error
**Cause:** Invalid request body (missing required fields, wrong date format)
**Solution:**
- Ensure `date_range` has both `start_date` and `end_date`
- Use ISO date format: "YYYY-MM-DD"
- For custom reports, ensure `custom_config` is provided
### Issue: 500 Internal Server Error
**Cause:** Report generation failed (database error, invalid entity, etc.)
**Solution:**
- Check entity name is valid (tickets, timesheets, payroll, expenses, users)
- Verify column names exist for the entity
- Check aggregation field names are valid
- Review browser console for detailed error message
### Issue: Empty Data Array
**Cause:** No records match the filters
**Solution:**
- Expand date range
- Remove or adjust filters
- Check if project has data for the selected period
### Issue: Slow Report Generation
**Cause:** Large dataset or complex aggregations
**Solution:**
- Use pagination (`limit` and `skip`)
- Narrow date range
- Add more specific filters (project_id, region_id)
- Consider caching results
---
## Best Practices
### 1. Always Handle Errors
```typescript
try {
const result = await reportsService.generate(request);
setData(result.data);
} catch (error) {
console.error('Report generation failed:', error);
showErrorToast(error.message);
}
```
### 2. Use Pagination for Large Datasets
```typescript
const request: ReportRequest = {
report_type: 'custom',
date_range: { start_date, end_date },
limit: 100, // Fetch 100 records at a time
skip: page * 100, // Offset for pagination
custom_config: { ... }
};
```
### 3. Cache Report Results
```typescript
// Use React Query, SWR, or simple state management
const cacheKey = `report_${reportType}_${projectId}_${startDate}_${endDate}`;
const cached = localStorage.getItem(cacheKey);
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
return JSON.parse(cached.data);
}
```
### 4. Show Loading States
```typescript
{loading && <Spinner />}
{error && <ErrorMessage message={error} />}
{data && <ReportTable data={data} />}
```
### 5. Validate Dates Before Sending
```typescript
const validateDateRange = (start: string, end: string) => {
const startDate = new Date(start);
const endDate = new Date(end);
if (startDate > endDate) {
throw new Error('Start date must be before end date');
}
if (endDate > new Date()) {
throw new Error('End date cannot be in the future');
}
};
```
---
## Questions?
Contact the backend team or check:
- API Swagger docs: `/docs`
- Source code: `src/app/api/v1/reports.py`
- Service logic: `src/app/services/report_service.py`
- Feature docs: `docs/api/reports/CUSTOM_REPORTS_FEATURE.md`