feat: implement v0.4.0 - Reports, Charts, Comparison, Dark Mode, E2E Testing
Backend (@backend-dev): - Add ReportService with PDF/CSV generation (reportlab, pandas) - Implement Report API endpoints (POST, GET, DELETE, download) - Add ReportRepository and schemas - Configure storage with auto-cleanup (30 days) - Rate limiting: 10 downloads/minute - Professional PDF templates with charts support Frontend (@frontend-dev): - Integrate Recharts for data visualization - Add CostBreakdown, TimeSeries, ComparisonBar charts - Implement scenario comparison page with multi-select - Add dark/light mode toggle with ThemeProvider - Create Reports page with generation form and list - Add new UI components: checkbox, dialog, tabs, label, skeleton - Implement useComparison and useReports hooks QA (@qa-engineer): - Setup Playwright E2E testing framework - Create 7 test spec files with 94 test cases - Add visual regression testing with baselines - Configure multi-browser testing (Chrome, Firefox, WebKit) - Add mobile responsive tests - Create test fixtures and helpers - Setup GitHub Actions CI workflow Documentation (@spec-architect): - Create detailed kanban-v0.4.0.md with 27 tasks - Update progress.md with v0.4.0 tracking - Create v0.4.0 planning prompt Features: ✅ PDF/CSV Report Generation ✅ Interactive Charts (Pie, Area, Bar) ✅ Scenario Comparison (2-4 scenarios) ✅ Dark/Light Mode Toggle ✅ E2E Test Suite (94 tests) Dependencies added: - Backend: reportlab, pandas, slowapi - Frontend: recharts, date-fns, @radix-ui/react-checkbox/dialog/tabs - Testing: @playwright/test 27 tasks completed, 100% v0.4.0 implementation
This commit is contained in:
43
frontend/src/hooks/useComparison.ts
Normal file
43
frontend/src/hooks/useComparison.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import type { Scenario, MetricSummary } from '@/types/api';
|
||||
|
||||
const COMPARISON_KEY = 'comparison';
|
||||
|
||||
export interface ComparisonScenario {
|
||||
scenario: Scenario;
|
||||
summary: MetricSummary;
|
||||
}
|
||||
|
||||
export interface ComparisonResult {
|
||||
scenarios: ComparisonScenario[];
|
||||
deltas: Record<string, { value: number; percentage: number }[]>;
|
||||
}
|
||||
|
||||
export interface CompareRequest {
|
||||
scenario_ids: string[];
|
||||
metrics?: string[];
|
||||
}
|
||||
|
||||
export function useCompareScenarios() {
|
||||
return useMutation<ComparisonResult, Error, CompareRequest>({
|
||||
mutationFn: async (data) => {
|
||||
const response = await api.post('/scenarios/compare', data);
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useComparisonCache(scenarioIds: string[]) {
|
||||
return useQuery<ComparisonResult>({
|
||||
queryKey: [COMPARISON_KEY, scenarioIds.sort().join(',')],
|
||||
queryFn: async () => {
|
||||
const response = await api.post('/scenarios/compare', {
|
||||
scenario_ids: scenarioIds,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
enabled: scenarioIds.length >= 2 && scenarioIds.length <= 4,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes cache
|
||||
});
|
||||
}
|
||||
118
frontend/src/hooks/useReports.ts
Normal file
118
frontend/src/hooks/useReports.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
|
||||
const REPORTS_KEY = 'reports';
|
||||
|
||||
export type ReportFormat = 'pdf' | 'csv';
|
||||
export type ReportStatus = 'pending' | 'processing' | 'completed' | 'failed';
|
||||
export type ReportSection = 'summary' | 'costs' | 'metrics' | 'logs' | 'pii';
|
||||
|
||||
export interface Report {
|
||||
id: string;
|
||||
scenario_id: string;
|
||||
format: ReportFormat;
|
||||
status: ReportStatus;
|
||||
created_at: string;
|
||||
completed_at?: string;
|
||||
file_size?: number;
|
||||
file_path?: string;
|
||||
error_message?: string;
|
||||
sections: ReportSection[];
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
}
|
||||
|
||||
export interface ReportList {
|
||||
items: Report[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface GenerateReportRequest {
|
||||
format: ReportFormat;
|
||||
include_logs?: boolean;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
sections: ReportSection[];
|
||||
}
|
||||
|
||||
export function useReports(scenarioId: string) {
|
||||
return useQuery<ReportList>({
|
||||
queryKey: [REPORTS_KEY, scenarioId],
|
||||
queryFn: async () => {
|
||||
const response = await api.get(`/scenarios/${scenarioId}/reports`);
|
||||
return response.data;
|
||||
},
|
||||
enabled: !!scenarioId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useReport(reportId: string) {
|
||||
return useQuery<Report>({
|
||||
queryKey: [REPORTS_KEY, 'detail', reportId],
|
||||
queryFn: async () => {
|
||||
const response = await api.get(`/reports/${reportId}`);
|
||||
return response.data;
|
||||
},
|
||||
enabled: !!reportId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGenerateReport(scenarioId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<Report, Error, GenerateReportRequest>({
|
||||
mutationFn: async (data) => {
|
||||
const response = await api.post(`/scenarios/${scenarioId}/reports`, data);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [REPORTS_KEY, scenarioId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDownloadReport() {
|
||||
return useMutation<Blob, Error, { reportId: string; fileName: string }>({
|
||||
mutationFn: async ({ reportId }) => {
|
||||
const response = await api.get(`/reports/${reportId}/download`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteReport(scenarioId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, Error, string>({
|
||||
mutationFn: async (reportId) => {
|
||||
await api.delete(`/reports/${reportId}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [REPORTS_KEY, scenarioId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes?: number): string {
|
||||
if (!bytes) return '-';
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
export function getStatusBadgeVariant(status: ReportStatus): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return 'default';
|
||||
case 'processing':
|
||||
return 'secondary';
|
||||
case 'failed':
|
||||
return 'destructive';
|
||||
case 'pending':
|
||||
return 'outline';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user