diff --git a/backend/app/api/reports.py b/backend/app/api/reports.py index 847fcd8..e3e2380 100644 --- a/backend/app/api/reports.py +++ b/backend/app/api/reports.py @@ -211,6 +211,39 @@ def download_report( ) +@router.delete("/delete") +def delete_reports( + ids: list[int], + db: Session = Depends(get_db), + user: User = Depends(require_admin), +): + """Delete one or more reports by ID (admin only). + + Removes both the database record and the file on disk. + """ + deleted = 0 + for report_id in ids: + report = db.query(WeeklyReport).filter(WeeklyReport.id == report_id).first() + if not report: + continue + if report.report_file_path and os.path.exists(report.report_file_path): + os.remove(report.report_file_path) + db.delete(report) + deleted += 1 + + if deleted: + db.commit() + log_action( + db, + user_id=user.id, + action="reports_deleted", + entity_type="report", + new_values={"ids": ids, "deleted": deleted}, + ) + + return {"deleted": deleted} + + @router.get("/list", response_model=ReportListResponse) def list_reports( db: Session = Depends(get_db), diff --git a/frontend/src/pages/ReportsPage.tsx b/frontend/src/pages/ReportsPage.tsx index 83ec8da..179c086 100644 --- a/frontend/src/pages/ReportsPage.tsx +++ b/frontend/src/pages/ReportsPage.tsx @@ -1,10 +1,11 @@ import { useState, useEffect } from 'react' -import { Download, FileSpreadsheet, Loader2, Plus } from 'lucide-react' +import { Download, FileSpreadsheet, Loader2, Plus, Trash2 } from 'lucide-react' import api from '@/services/api' import type { ReportMeta } from '@/types' import { useAuth } from '@/context/AuthContext' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { @@ -29,8 +30,12 @@ export function ReportsPage() { const [genError, setGenError] = useState('') const [genSuccess, setGenSuccess] = useState('') + // Selection state for deletion + const [selectedIds, setSelectedIds] = useState>(new Set()) + const [deleting, setDeleting] = useState(false) + // Fetch reports list - useEffect(() => { + const fetchReports = () => { setLoading(true) api.get<{ items: ReportMeta[]; total: number }>('/reports/list') .then((res) => { @@ -42,6 +47,10 @@ export function ReportsPage() { setTotalReports(0) }) .finally(() => setLoading(false)) + } + + useEffect(() => { + fetchReports() }, []) const generateReport = async () => { @@ -53,10 +62,7 @@ export function ReportsPage() { params: { jahr: genJahr, kw: genKw }, }) setGenSuccess(`Bericht für KW ${res.data.kw}/${res.data.jahr} wurde generiert.`) - // Refresh list - const listRes = await api.get<{ items: ReportMeta[]; total: number }>('/reports/list') - setReports(listRes.data.items) - setTotalReports(listRes.data.total) + fetchReports() } catch { setGenError('Fehler beim Generieren des Berichts.') } finally { @@ -67,7 +73,6 @@ export function ReportsPage() { const downloadReport = (reportId: number) => { const token = localStorage.getItem('access_token') const url = `/api/reports/download/${reportId}` - // Use a temporary link with token in header via fetch + blob api.get(url, { responseType: 'blob' }) .then((res) => { const blob = new Blob([res.data as BlobPart], { @@ -76,7 +81,6 @@ export function ReportsPage() { const blobUrl = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = blobUrl - // Extract filename from content-disposition or use default const contentDisposition = res.headers['content-disposition'] let filename = `bericht_${reportId}.xlsx` if (contentDisposition) { @@ -90,13 +94,46 @@ export function ReportsPage() { window.URL.revokeObjectURL(blobUrl) }) .catch(() => { - // Fallback: open in new tab with token param if (token) { window.open(`${url}?token=${encodeURIComponent(token)}`, '_blank') } }) } + const toggleSelect = (id: number) => { + setSelectedIds((prev) => { + const next = new Set(prev) + if (next.has(id)) { + next.delete(id) + } else { + next.add(id) + } + return next + }) + } + + const toggleSelectAll = () => { + if (selectedIds.size === reports.length) { + setSelectedIds(new Set()) + } else { + setSelectedIds(new Set(reports.map((r) => r.id))) + } + } + + const deleteSelected = async () => { + if (selectedIds.size === 0) return + setDeleting(true) + try { + await api.delete('/reports/delete', { data: Array.from(selectedIds) }) + setSelectedIds(new Set()) + fetchReports() + } catch { + setGenError('Fehler beim Löschen der Berichte.') + } finally { + setDeleting(false) + } + } + return (

Berichte

@@ -167,9 +204,26 @@ export function ReportsPage() { {/* Reports table */} - - Bisherige Berichte ({totalReports}) - +
+ + Bisherige Berichte ({totalReports}) + + {isAdmin && selectedIds.size > 0 && ( + + )} +
{loading ? ( @@ -182,6 +236,14 @@ export function ReportsPage() { + {isAdmin && ( + + 0 && selectedIds.size === reports.length} + onCheckedChange={toggleSelectAll} + /> + + )} Berichtsdatum Jahr KW @@ -192,6 +254,14 @@ export function ReportsPage() { {reports.map((r) => ( + {isAdmin && ( + + toggleSelect(r.id)} + /> + + )} {formatDate(r.report_date)} {r.jahr} KW {r.kw}