Phase 5 Batch 5 Task 5.19: Report Generator Frontend-Oberfläche

- Created frontend/src/api/reports.ts: React Query hooks for templates, presets, generate
- Created frontend/src/pages/Reports.tsx: 3-column layout (template list, editor, generate)
- Preset quick-action buttons with format selection (PDF/Print/CSV/Excel)
- Template editor with Jinja2 code textarea, name, output format selector
- JSON data input for report parameters
- Download history tracking
- Route /reports registered in index.tsx (lazy-loaded)
- i18n keys added to de.json and en.json (reports section + nav.reports)
- 5 frontend tests: page render, template list, new template, select template, download history
- TSC: 0 new errors (2 pre-existing Dms.tsx errors only)
This commit is contained in:
Agent Zero
2026-07-23 23:26:40 +02:00
parent 15f1a57c0f
commit 0ec8502fd4
6 changed files with 783 additions and 2 deletions
+162
View File
@@ -0,0 +1,162 @@
/**
* Report Generator plugin API client.
*
* All requests use the shared `apiClient` (`baseURL: '/api/v1'`) and target the
* Report Generator plugin routes under `/reports/...`.
*/
import { apiClient, apiDelete, apiGet, apiPost, apiPut } from './client';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
// ─── Types ─────────────────────────────────────────────────────────────────
export type OutputFormat = 'csv' | 'excel' | 'json' | 'pdf' | 'print';
export type TemplateType = 'jinja2' | 'sql';
export interface ReportTemplate {
id: string;
name: string;
description: string;
template_type: TemplateType;
content: string;
output_format: OutputFormat;
created_by: string;
deleted_at?: string | null;
created_at?: string | null;
updated_at?: string | null;
}
export interface TemplateCreateInput {
name: string;
description?: string;
template_type?: TemplateType;
content: string;
output_format?: OutputFormat;
}
export interface TemplateUpdateInput {
name?: string;
description?: string;
template_type?: TemplateType;
content?: string;
output_format?: OutputFormat;
}
export interface ReportGenerateInput {
template_id: string;
data: Record<string, unknown>;
output_format?: OutputFormat;
}
export interface PresetReportInfo {
key: string;
name: string;
description: string;
icon: string;
output_formats: OutputFormat[];
}
export interface PresetGenerateInput {
preset: string;
output_format: OutputFormat;
parameters: Record<string, unknown>;
}
// ─── React Query Hooks ─────────────────────────────────────────────────────
const QUERY_KEYS = {
templates: ['reports', 'templates'] as const,
presets: ['reports', 'presets'] as const,
};
/** Fetch all report templates */
export function useReportTemplates() {
return useQuery<ReportTemplate[]>({
queryKey: QUERY_KEYS.templates,
queryFn: () => apiGet<ReportTemplate[]>('/reports/templates'),
});
}
/** Fetch a single report template by ID */
export function useReportTemplate(templateId: string | null) {
return useQuery<ReportTemplate>({
queryKey: ['reports', 'templates', templateId],
queryFn: () => apiGet<ReportTemplate>(`/reports/templates/${templateId}`),
enabled: !!templateId,
});
}
/** Create a new report template */
export function useCreateReportTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: TemplateCreateInput) =>
apiPost<ReportTemplate>('/reports/templates', input),
onSuccess: () => qc.invalidateQueries({ queryKey: QUERY_KEYS.templates }),
});
}
/** Update an existing report template */
export function useUpdateReportTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, ...input }: TemplateUpdateInput & { id: string }) =>
apiPut<ReportTemplate>(`/reports/templates/${id}`, input),
onSuccess: () => qc.invalidateQueries({ queryKey: QUERY_KEYS.templates }),
});
}
/** Delete a report template (soft-delete) */
export function useDeleteReportTemplate() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiDelete(`/reports/templates/${id}`),
onSuccess: () => qc.invalidateQueries({ queryKey: QUERY_KEYS.templates }),
});
}
/** Fetch all preset report templates */
export function useReportPresets() {
return useQuery<PresetReportInfo[]>({
queryKey: QUERY_KEYS.presets,
queryFn: () => apiGet<PresetReportInfo[]>('/reports/presets'),
});
}
/** Generate a report from a template — returns a file download (blob) */
export function useGenerateReport() {
return useMutation({
mutationFn: async (input: ReportGenerateInput) => {
const response = await apiClient.post('/reports/generate', input, {
responseType: 'blob',
});
return response;
},
});
}
/** Generate a preset report — returns a file download (blob) */
export function useGeneratePresetReport() {
return useMutation({
mutationFn: async (input: PresetGenerateInput) => {
const response = await apiClient.post('/reports/presets/generate', input, {
responseType: 'blob',
});
return response;
},
});
}
// ─── Download Helper ───────────────────────────────────────────────────────
/** Trigger a browser download from a blob response */
export function downloadBlob(blob: Blob, filename: string) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}