feat: add checkbox selection and bulk delete for reports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-24 10:40:19 +00:00
parent 9650889d24
commit 7ef1fc9335
2 changed files with 115 additions and 12 deletions

View file

@ -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),

View file

@ -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<Set<number>>(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 (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">Berichte</h1>
@ -167,9 +204,26 @@ export function ReportsPage() {
{/* Reports table */}
<Card>
<CardHeader>
<CardTitle className="text-lg">
Bisherige Berichte ({totalReports})
</CardTitle>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">
Bisherige Berichte ({totalReports})
</CardTitle>
{isAdmin && selectedIds.size > 0 && (
<Button
variant="destructive"
size="sm"
onClick={deleteSelected}
disabled={deleting}
>
{deleting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Trash2 className="mr-2 h-4 w-4" />
)}
{selectedIds.size} Bericht{selectedIds.size > 1 ? 'e' : ''} löschen
</Button>
)}
</div>
</CardHeader>
<CardContent>
{loading ? (
@ -182,6 +236,14 @@ export function ReportsPage() {
<Table>
<TableHeader>
<TableRow>
{isAdmin && (
<TableHead className="w-10">
<Checkbox
checked={reports.length > 0 && selectedIds.size === reports.length}
onCheckedChange={toggleSelectAll}
/>
</TableHead>
)}
<TableHead>Berichtsdatum</TableHead>
<TableHead>Jahr</TableHead>
<TableHead>KW</TableHead>
@ -192,6 +254,14 @@ export function ReportsPage() {
<TableBody>
{reports.map((r) => (
<TableRow key={r.id}>
{isAdmin && (
<TableCell>
<Checkbox
checked={selectedIds.has(r.id)}
onCheckedChange={() => toggleSelect(r.id)}
/>
</TableCell>
)}
<TableCell>{formatDate(r.report_date)}</TableCell>
<TableCell>{r.jahr}</TableCell>
<TableCell>KW {r.kw}</TableCell>