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
@@ -0,0 +1,138 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { ReportsPage } from '../Reports';
// Mock the API hooks
vi.mock('@/api/reports', () => ({
useReportTemplates: vi.fn(() => ({
data: [
{ id: 'tpl-1', name: 'My Report', description: 'Test', template_type: 'jinja2', content: '<html></html>', output_format: 'pdf', created_by: 'user-1' },
{ id: 'tpl-2', name: 'CSV Report', description: 'Test CSV', template_type: 'jinja2', content: 'name,email', output_format: 'csv', created_by: 'user-1' },
],
isLoading: false,
})),
useReportPresets: vi.fn(() => ({
data: [
{ key: 'contact_list', name: 'Kontaktliste', description: 'Liste aller Kontakte', icon: 'Users', output_formats: ['pdf', 'print', 'csv', 'excel'] },
{ key: 'company_list', name: 'Firmenliste', description: 'Liste aller Firmen', icon: 'Building2', output_formats: ['pdf', 'print', 'csv', 'excel'] },
],
})),
useCreateReportTemplate: vi.fn(() => ({
mutateAsync: vi.fn().mockResolvedValue({}),
isPending: false,
})),
useUpdateReportTemplate: vi.fn(() => ({
mutateAsync: vi.fn().mockResolvedValue({}),
isPending: false,
})),
useDeleteReportTemplate: vi.fn(() => ({
mutateAsync: vi.fn().mockResolvedValue({}),
isPending: false,
})),
useGenerateReport: vi.fn(() => ({
mutateAsync: vi.fn().mockResolvedValue({ data: new Blob(['test'], { type: 'application/pdf' }) }),
isPending: false,
})),
useGeneratePresetReport: vi.fn(() => ({
mutateAsync: vi.fn().mockResolvedValue({ data: new Blob(['test'], { type: 'application/pdf' }) }),
isPending: false,
})),
downloadBlob: vi.fn(),
}));
// Mock i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, fallback?: string) => fallback || key,
i18n: { language: 'de' },
}),
}));
// Mock toast
vi.mock('@/components/ui/Toast', () => ({
useToast: () => ({
toast: { success: vi.fn(), error: vi.fn() },
}),
}));
// Mock lucide-react icons
vi.mock('lucide-react', () => {
const Icon = ({ className }: { className?: string }) => <div className={className} data-testid="icon" />;
return {
BarChart3: Icon,
Building2: Icon,
Calendar: Icon,
CalendarDays: Icon,
Download: Icon,
FileText: Icon,
Loader2: Icon,
Plus: Icon,
Printer: Icon,
Save: Icon,
ShieldCheck: Icon,
Trash2: Icon,
Users: Icon,
};
});
// Helper to render with providers
function renderWithProviders(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
{ui}
</BrowserRouter>
</QueryClientProvider>
);
}
describe('ReportsPage', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders the reports page with title and preset quick actions', () => {
renderWithProviders(<ReportsPage />);
expect(screen.getByTestId('reports-page')).toBeInTheDocument();
expect(screen.getByTestId('preset-quick-actions')).toBeInTheDocument();
// Should have preset buttons for contact_list and company_list
expect(screen.getByTestId('preset-contact_list-pdf')).toBeInTheDocument();
expect(screen.getByTestId('preset-company_list-pdf')).toBeInTheDocument();
});
it('displays templates in the template list', () => {
renderWithProviders(<ReportsPage />);
expect(screen.getByTestId('template-list-panel')).toBeInTheDocument();
expect(screen.getByText('My Report')).toBeInTheDocument();
expect(screen.getByText('CSV Report')).toBeInTheDocument();
});
it('clicking new template button shows editor with default content', () => {
renderWithProviders(<ReportsPage />);
const newBtn = screen.getByTestId('btn-new-template');
fireEvent.click(newBtn);
// Editor should be visible with template name input
expect(screen.getByTestId('input-template-name')).toBeInTheDocument();
expect(screen.getByTestId('textarea-template-content')).toBeInTheDocument();
expect(screen.getByTestId('btn-save-template')).toBeInTheDocument();
});
it('selecting a template loads it into the editor', () => {
renderWithProviders(<ReportsPage />);
// Click on the first template
fireEvent.click(screen.getByText('My Report'));
// Editor should show the template name
const nameInput = screen.getByTestId('input-template-name') as HTMLInputElement;
expect(nameInput.value).toBe('My Report');
});
it('shows download history section', () => {
renderWithProviders(<ReportsPage />);
expect(screen.getByTestId('download-history')).toBeInTheDocument();
});
});