mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-18 00:13:41 +00:00
feat: add checkbox selection and bulk delete for reports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9650889d24
commit
7ef1fc9335
2 changed files with 115 additions and 12 deletions
|
|
@ -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)
|
@router.get("/list", response_model=ReportListResponse)
|
||||||
def list_reports(
|
def list_reports(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState, useEffect } from 'react'
|
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 api from '@/services/api'
|
||||||
import type { ReportMeta } from '@/types'
|
import type { ReportMeta } from '@/types'
|
||||||
import { useAuth } from '@/context/AuthContext'
|
import { useAuth } from '@/context/AuthContext'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
|
|
@ -29,8 +30,12 @@ export function ReportsPage() {
|
||||||
const [genError, setGenError] = useState('')
|
const [genError, setGenError] = useState('')
|
||||||
const [genSuccess, setGenSuccess] = 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
|
// Fetch reports list
|
||||||
useEffect(() => {
|
const fetchReports = () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
api.get<{ items: ReportMeta[]; total: number }>('/reports/list')
|
api.get<{ items: ReportMeta[]; total: number }>('/reports/list')
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
|
@ -42,6 +47,10 @@ export function ReportsPage() {
|
||||||
setTotalReports(0)
|
setTotalReports(0)
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false))
|
.finally(() => setLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchReports()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const generateReport = async () => {
|
const generateReport = async () => {
|
||||||
|
|
@ -53,10 +62,7 @@ export function ReportsPage() {
|
||||||
params: { jahr: genJahr, kw: genKw },
|
params: { jahr: genJahr, kw: genKw },
|
||||||
})
|
})
|
||||||
setGenSuccess(`Bericht für KW ${res.data.kw}/${res.data.jahr} wurde generiert.`)
|
setGenSuccess(`Bericht für KW ${res.data.kw}/${res.data.jahr} wurde generiert.`)
|
||||||
// Refresh list
|
fetchReports()
|
||||||
const listRes = await api.get<{ items: ReportMeta[]; total: number }>('/reports/list')
|
|
||||||
setReports(listRes.data.items)
|
|
||||||
setTotalReports(listRes.data.total)
|
|
||||||
} catch {
|
} catch {
|
||||||
setGenError('Fehler beim Generieren des Berichts.')
|
setGenError('Fehler beim Generieren des Berichts.')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -67,7 +73,6 @@ export function ReportsPage() {
|
||||||
const downloadReport = (reportId: number) => {
|
const downloadReport = (reportId: number) => {
|
||||||
const token = localStorage.getItem('access_token')
|
const token = localStorage.getItem('access_token')
|
||||||
const url = `/api/reports/download/${reportId}`
|
const url = `/api/reports/download/${reportId}`
|
||||||
// Use a temporary link with token in header via fetch + blob
|
|
||||||
api.get(url, { responseType: 'blob' })
|
api.get(url, { responseType: 'blob' })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const blob = new Blob([res.data as BlobPart], {
|
const blob = new Blob([res.data as BlobPart], {
|
||||||
|
|
@ -76,7 +81,6 @@ export function ReportsPage() {
|
||||||
const blobUrl = window.URL.createObjectURL(blob)
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = blobUrl
|
link.href = blobUrl
|
||||||
// Extract filename from content-disposition or use default
|
|
||||||
const contentDisposition = res.headers['content-disposition']
|
const contentDisposition = res.headers['content-disposition']
|
||||||
let filename = `bericht_${reportId}.xlsx`
|
let filename = `bericht_${reportId}.xlsx`
|
||||||
if (contentDisposition) {
|
if (contentDisposition) {
|
||||||
|
|
@ -90,13 +94,46 @@ export function ReportsPage() {
|
||||||
window.URL.revokeObjectURL(blobUrl)
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Fallback: open in new tab with token param
|
|
||||||
if (token) {
|
if (token) {
|
||||||
window.open(`${url}?token=${encodeURIComponent(token)}`, '_blank')
|
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 (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<h1 className="text-2xl font-bold">Berichte</h1>
|
<h1 className="text-2xl font-bold">Berichte</h1>
|
||||||
|
|
@ -167,9 +204,26 @@ export function ReportsPage() {
|
||||||
{/* Reports table */}
|
{/* Reports table */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-lg">
|
<CardTitle className="text-lg">
|
||||||
Bisherige Berichte ({totalReports})
|
Bisherige Berichte ({totalReports})
|
||||||
</CardTitle>
|
</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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
@ -182,6 +236,14 @@ export function ReportsPage() {
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
{isAdmin && (
|
||||||
|
<TableHead className="w-10">
|
||||||
|
<Checkbox
|
||||||
|
checked={reports.length > 0 && selectedIds.size === reports.length}
|
||||||
|
onCheckedChange={toggleSelectAll}
|
||||||
|
/>
|
||||||
|
</TableHead>
|
||||||
|
)}
|
||||||
<TableHead>Berichtsdatum</TableHead>
|
<TableHead>Berichtsdatum</TableHead>
|
||||||
<TableHead>Jahr</TableHead>
|
<TableHead>Jahr</TableHead>
|
||||||
<TableHead>KW</TableHead>
|
<TableHead>KW</TableHead>
|
||||||
|
|
@ -192,6 +254,14 @@ export function ReportsPage() {
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{reports.map((r) => (
|
{reports.map((r) => (
|
||||||
<TableRow key={r.id}>
|
<TableRow key={r.id}>
|
||||||
|
{isAdmin && (
|
||||||
|
<TableCell>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIds.has(r.id)}
|
||||||
|
onCheckedChange={() => toggleSelect(r.id)}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
<TableCell>{formatDate(r.report_date)}</TableCell>
|
<TableCell>{formatDate(r.report_date)}</TableCell>
|
||||||
<TableCell>{r.jahr}</TableCell>
|
<TableCell>{r.jahr}</TableCell>
|
||||||
<TableCell>KW {r.kw}</TableCell>
|
<TableCell>KW {r.kw}</TableCell>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue