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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user