diff --git a/cannamanage-frontend/messages/de.json b/cannamanage-frontend/messages/de.json index 48af0de..67372b2 100644 --- a/cannamanage-frontend/messages/de.json +++ b/cannamanage-frontend/messages/de.json @@ -66,7 +66,15 @@ "today": "Heute", "trend": "+{value}% ggü. Vormonat", "quotaUsed": "{value}% verbraucht", - "distributionCount": "{count} Ausgaben, {grams}g" + "distributionCount": "{count} Ausgaben, {grams}g", + "outstandingPayments": "Offene Zahlungen", + "monthlyIncome": "Monatliches Einkommen", + "thisMonth": "Diesen Monat", + "strainsAvailable": "Sorten verfügbar", + "upcomingEvents": "Nächste Termine", + "latestAnnouncements": "Neueste Beiträge", + "rsvps": "Zusagen", + "viewAll": "Alle anzeigen" }, "members": { "title": "Mitgliederverwaltung", @@ -1004,5 +1012,100 @@ "positions": "Positionen", "active": "Aktiv", "inactive": "Inaktiv" + }, + "reportsCenter": { + "title": "Berichtszentrale", + "subtitle": "Alle gesetzlichen und internen Berichte an einem Ort generieren und verwalten.", + "generate": "Generieren", + "cancel": "Abbrechen", + "authorityExport": { + "title": "Behörden-Export", + "description": "Gebündelter Export aller behördlich relevanten Dokumente für ein Kalenderjahr.", + "button": "Behörden-Export starten", + "dialogTitle": "Behörden-Export erstellen", + "dialogDescription": "Erstellt ein passwortgeschütztes Archiv mit allen compliance-relevanten Berichten für das gewählte Jahr.", + "warning": "Dieser Export enthält sensible Daten und wird im Audit-Protokoll erfasst. Bitte nur bei berechtigter Anforderung durchführen.", + "year": "Berichtsjahr", + "password": "Passwort für Archiv", + "passwordPlaceholder": "Sicheres Passwort eingeben", + "confirm": "Export erstellen" + }, + "categories": { + "finance": "Finanzen", + "kcang": "KCanG-Compliance", + "dsgvo": "DSGVO", + "admin": "Verwaltung" + }, + "reports": { + "EUER": { + "name": "EÜR", + "description": "Einnahmenüberschussrechnung für den gewählten Zeitraum" + }, + "KASSENBUCH_EXPORT": { + "name": "Kassenbuch-Export", + "description": "Vollständige Kassenbuchführung als PDF oder CSV" + }, + "BEITRAGSBESCHEINIGUNG": { + "name": "Beitragsbescheinigung", + "description": "Bescheinigung über gezahlte Mitgliedsbeiträge" + }, + "JAHRESBERICHT_BEHOERDE": { + "name": "Jahresbericht Behörde", + "description": "Gesetzlich vorgeschriebener Bericht an die zuständige Behörde" + }, + "AUSGABEPROTOKOLL": { + "name": "Ausgabeprotokoll", + "description": "Protokoll aller Ausgaben im Zeitraum mit Mengen und Empfängern" + }, + "VERNICHTUNGSPROTOKOLL": { + "name": "Vernichtungsprotokoll", + "description": "Dokumentation der ordnungsgemäßen Vernichtung von Cannabis" + }, + "TRANSPORTZERTIFIKAT": { + "name": "Transportzertifikat", + "description": "Zertifikat für den genehmigten Transport von Cannabis" + }, + "BESTANDSFUEHRUNG": { + "name": "Bestandsführung", + "description": "Aktueller Lagerbestand mit allen Ein- und Ausgängen" + }, + "VERARBEITUNGSVERZEICHNIS": { + "name": "Verarbeitungsverzeichnis", + "description": "Verzeichnis aller Verarbeitungstätigkeiten gem. Art. 30 DSGVO" + }, + "TOM": { + "name": "TOM", + "description": "Technische und organisatorische Maßnahmen gem. Art. 32 DSGVO" + }, + "DSFA": { + "name": "DSFA", + "description": "Datenschutz-Folgenabschätzung gem. Art. 35 DSGVO" + }, + "LOESCHKONZEPT": { + "name": "Löschkonzept", + "description": "Konzept zur fristgerechten Datenlöschung" + }, + "DATENPANNEN_MELDUNG": { + "name": "Datenpannen-Meldung", + "description": "Vorlage zur Meldung einer Datenschutzverletzung" + }, + "MITGLIEDERLISTE_REGISTER": { + "name": "Mitgliederliste Register", + "description": "Offizielle Mitgliederliste für das Vereinsregister" + }, + "VORSTANDSAENDERUNG": { + "name": "Vorstandsänderung", + "description": "Meldung einer Vorstandsänderung ans Vereinsregister" + } + }, + "history": { + "title": "Generierte Berichte", + "empty": "Noch keine Berichte generiert. Wähle oben einen Bericht aus, um zu beginnen.", + "report": "Bericht", + "format": "Format", + "date": "Datum", + "user": "Erstellt von", + "size": "Größe" + } } } \ No newline at end of file diff --git a/cannamanage-frontend/messages/en.json b/cannamanage-frontend/messages/en.json index dc893fd..8d4f077 100644 --- a/cannamanage-frontend/messages/en.json +++ b/cannamanage-frontend/messages/en.json @@ -66,7 +66,15 @@ "today": "Today", "trend": "+{value}% vs last month", "quotaUsed": "{value}% used", - "distributionCount": "{count} distributions, {grams}g" + "distributionCount": "{count} distributions, {grams}g", + "outstandingPayments": "Outstanding Payments", + "monthlyIncome": "Monthly Income", + "thisMonth": "This month", + "strainsAvailable": "strains available", + "upcomingEvents": "Upcoming Events", + "latestAnnouncements": "Latest Announcements", + "rsvps": "RSVPs", + "viewAll": "View all" }, "members": { "title": "Member Management", @@ -1004,5 +1012,100 @@ "positions": "Positions", "active": "Active", "inactive": "Inactive" + }, + "reportsCenter": { + "title": "Reports Center", + "subtitle": "Generate and manage all regulatory and internal reports in one place.", + "generate": "Generate", + "cancel": "Cancel", + "authorityExport": { + "title": "Authority Export", + "description": "Bundled export of all authority-relevant documents for a calendar year.", + "button": "Start Authority Export", + "dialogTitle": "Create Authority Export", + "dialogDescription": "Creates a password-protected archive with all compliance-relevant reports for the selected year.", + "warning": "This export contains sensitive data and will be logged in the audit trail. Only proceed with a legitimate request.", + "year": "Report Year", + "password": "Archive Password", + "passwordPlaceholder": "Enter secure password", + "confirm": "Create Export" + }, + "categories": { + "finance": "Finance", + "kcang": "KCanG Compliance", + "dsgvo": "GDPR", + "admin": "Administration" + }, + "reports": { + "EUER": { + "name": "Income Statement", + "description": "Simplified income statement for the selected period" + }, + "KASSENBUCH_EXPORT": { + "name": "Cash Book Export", + "description": "Complete cash book as PDF or CSV" + }, + "BEITRAGSBESCHEINIGUNG": { + "name": "Membership Fee Certificate", + "description": "Certificate of paid membership fees" + }, + "JAHRESBERICHT_BEHOERDE": { + "name": "Annual Authority Report", + "description": "Legally required annual report to the responsible authority" + }, + "AUSGABEPROTOKOLL": { + "name": "Distribution Log", + "description": "Log of all distributions in the period with amounts and recipients" + }, + "VERNICHTUNGSPROTOKOLL": { + "name": "Destruction Protocol", + "description": "Documentation of proper cannabis destruction" + }, + "TRANSPORTZERTIFIKAT": { + "name": "Transport Certificate", + "description": "Certificate for approved cannabis transport" + }, + "BESTANDSFUEHRUNG": { + "name": "Inventory Report", + "description": "Current stock with all inflows and outflows" + }, + "VERARBEITUNGSVERZEICHNIS": { + "name": "Processing Register", + "description": "Register of all processing activities per Art. 30 GDPR" + }, + "TOM": { + "name": "TOM", + "description": "Technical and organizational measures per Art. 32 GDPR" + }, + "DSFA": { + "name": "DPIA", + "description": "Data Protection Impact Assessment per Art. 35 GDPR" + }, + "LOESCHKONZEPT": { + "name": "Deletion Policy", + "description": "Policy for timely data deletion" + }, + "DATENPANNEN_MELDUNG": { + "name": "Data Breach Report", + "description": "Template for reporting a data protection violation" + }, + "MITGLIEDERLISTE_REGISTER": { + "name": "Member Registry List", + "description": "Official member list for the association register" + }, + "VORSTANDSAENDERUNG": { + "name": "Board Change Notification", + "description": "Notification of board change to the association register" + } + }, + "history": { + "title": "Generated Reports", + "empty": "No reports generated yet. Select a report above to get started.", + "report": "Report", + "format": "Format", + "date": "Date", + "user": "Created by", + "size": "Size" + } } } \ No newline at end of file diff --git a/cannamanage-frontend/src/app/(dashboard-layout)/dashboard/page.tsx b/cannamanage-frontend/src/app/(dashboard-layout)/dashboard/page.tsx index 3f71147..055fad6 100644 --- a/cannamanage-frontend/src/app/(dashboard-layout)/dashboard/page.tsx +++ b/cannamanage-frontend/src/app/(dashboard-layout)/dashboard/page.tsx @@ -15,7 +15,19 @@ import { XAxis, YAxis, } from "recharts" -import { Leaf, Package, Plus, TrendingUp, UserPlus, Users } from "lucide-react" +import { + AlertCircle, + Calendar, + CreditCard, + Leaf, + Megaphone, + Package, + Plus, + TrendingUp, + UserPlus, + Users, + Wallet, +} from "lucide-react" import { mockClubStats, @@ -27,6 +39,58 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { CardSkeleton, TableSkeleton } from "@/components/ui/data-skeleton" +// Mock data for new widgets (backend fallback) +const mockOutstandingPayments = { count: 3, totalCents: 14700 } +const mockMonthlyIncome = { totalCents: 234500 } +const mockUpcomingEvents = [ + { + id: "e1", + title: "Mitgliederversammlung", + date: "2026-06-20T18:00:00Z", + rsvpCount: 28, + }, + { + id: "e2", + title: "Grow-Workshop", + date: "2026-06-22T14:00:00Z", + rsvpCount: 12, + }, + { + id: "e3", + title: "Sommerfest", + date: "2026-06-28T16:00:00Z", + rsvpCount: 35, + }, +] +const mockLatestAnnouncements = [ + { + id: "a1", + title: "Neue Öffnungszeiten ab Juli", + createdAt: "2026-06-14T10:00:00Z", + }, + { + id: "a2", + title: "Sorte 'Blue Dream' wieder verfügbar", + createdAt: "2026-06-13T15:30:00Z", + }, +] +const mockAlerts: { + type: string + message: string + severity: "red" | "yellow" | "blue" +}[] = [ + { + type: "overdue_payments", + message: "3 überfällige Zahlungen", + severity: "red", + }, + { + type: "compliance_deadline", + message: "Jahresbericht fällig in 5 Tagen", + severity: "yellow", + }, +] + export default function DashboardPage() { const t = useTranslations("dashboard") @@ -45,16 +109,18 @@ export default function DashboardPage() { return (
- {/* KPI Cards */} + {/* KPI Cards — 6 total */} {statsLoading ? ( -
+
+ +
) : ( -
+
{/* Active Members */} @@ -106,7 +172,7 @@ export default function DashboardPage() { {t("grams")}

- {mockStockByStrain.length} Sorten verfügbar + {mockStockByStrain.length} {t("strainsAvailable")}

@@ -130,6 +196,49 @@ export default function DashboardPage() {

+ + {/* Outstanding Payments — NEW */} + + + + {t("outstandingPayments")} + + + + +
+ {mockOutstandingPayments.count} +
+

+ {(mockOutstandingPayments.totalCents / 100).toLocaleString( + "de-DE", + { + style: "currency", + currency: "EUR", + } + )} +

+
+
+ + {/* Monthly Income — NEW */} + + + + {t("monthlyIncome")} + + + + +
+ {(mockMonthlyIncome.totalCents / 100).toLocaleString("de-DE", { + style: "currency", + currency: "EUR", + })} +
+

{t("thisMonth")}

+
+
)} @@ -161,100 +270,182 @@ export default function DashboardPage() { - {/* Bottom section: Table + Chart */} -
+ {/* Alerts — conditional */} + {mockAlerts.length > 0 && ( +
+ {mockAlerts.map((alert, idx) => ( + + + + {alert.message} + + + ))} +
+ )} + + {/* Middle section: Recent Distributions + Upcoming Events + Announcements */} +
{/* Recent Distributions Table */} - + {t("recentDistributions")} {distributionsLoading ? ( - + ) : ( -
- - - - - - - - - - - - {recentDistributions.map((dist) => ( - - - - - - - - ))} - -
{t("date")}{t("member")}{t("strain")}{t("amount")}{t("staff")}
- {new Date(dist.recordedAt).toLocaleTimeString( - "de-DE", - { - hour: "2-digit", - minute: "2-digit", - } - )} - {dist.memberName}{dist.strainName}{dist.amountGrams}g{dist.recordedBy}
+
+ {recentDistributions.slice(0, 5).map((dist) => ( +
+
+ + {dist.memberName} + + + {dist.strainName} + +
+ + {dist.amountGrams}g + +
+ ))}
)} - {/* Stock Level Chart */} - + {/* Upcoming Events — NEW */} + - {t("stockByStrain")} +
+ + {t("upcomingEvents")} +
-
- - + {mockUpcomingEvents.map((event) => ( +
- - - [`${value}g`, "Bestand"]} - /> - - {chartData.map((_, index) => ( - - ))} - - - +
+ {event.title} + + {new Date(event.date).toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + hour: "2-digit", + minute: "2-digit", + })} + +
+ + {event.rsvpCount} {t("rsvps")} + +
+ ))} +
+
+
+ + {/* Latest Announcements — NEW */} + + +
+ + {t("latestAnnouncements")} +
+
+ +
+ {mockLatestAnnouncements.map((post) => ( +
+ {post.title} + + {new Date(post.createdAt).toLocaleDateString("de-DE")} + +
+ ))} +
+ + {/* Bottom section: Chart */} + + + {t("stockByStrain")} + + +
+ + + + + [`${value}g`, "Bestand"]} + /> + + {chartData.map((_, index) => ( + + ))} + + + +
+
+
) } diff --git a/cannamanage-frontend/src/app/(dashboard-layout)/reports-center/page.tsx b/cannamanage-frontend/src/app/(dashboard-layout)/reports-center/page.tsx new file mode 100644 index 0000000..a52e24e --- /dev/null +++ b/cannamanage-frontend/src/app/(dashboard-layout)/reports-center/page.tsx @@ -0,0 +1,472 @@ +"use client" + +import { useState } from "react" +import { + downloadReport, + useAuthorityExport, + useGenerateReport, + useGeneratedReports, +} from "@/services/compliance-reports" +import { useTranslations } from "next-intl" +import { + AlertTriangle, + Calendar, + ChartBar, + Download, + FileText, + Info, + Shield, + ShieldAlert, +} from "lucide-react" + +import type { GeneratedReport } from "@/services/compliance-reports" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" + +// Report definitions grouped by category +const reportCategories = [ + { + id: "finance", + icon: ChartBar, + reports: [ + { type: "EUER", formats: ["PDF", "CSV"], requiresDateRange: true }, + { + type: "KASSENBUCH_EXPORT", + formats: ["PDF", "CSV"], + requiresDateRange: true, + }, + { + type: "BEITRAGSBESCHEINIGUNG", + formats: ["PDF"], + requiresDateRange: true, + }, + ], + }, + { + id: "kcang", + icon: Shield, + reports: [ + { + type: "JAHRESBERICHT_BEHOERDE", + formats: ["PDF"], + requiresDateRange: true, + }, + { + type: "AUSGABEPROTOKOLL", + formats: ["PDF", "CSV"], + requiresDateRange: true, + }, + { + type: "VERNICHTUNGSPROTOKOLL", + formats: ["PDF"], + requiresDateRange: true, + }, + { + type: "TRANSPORTZERTIFIKAT", + formats: ["PDF"], + requiresDateRange: false, + }, + { + type: "BESTANDSFUEHRUNG", + formats: ["PDF", "CSV", "JSON"], + requiresDateRange: false, + }, + ], + }, + { + id: "dsgvo", + icon: ShieldAlert, + reports: [ + { + type: "VERARBEITUNGSVERZEICHNIS", + formats: ["PDF"], + requiresDateRange: false, + }, + { type: "TOM", formats: ["PDF"], requiresDateRange: false }, + { type: "DSFA", formats: ["PDF"], requiresDateRange: false }, + { type: "LOESCHKONZEPT", formats: ["PDF"], requiresDateRange: false }, + { + type: "DATENPANNEN_MELDUNG", + formats: ["PDF"], + requiresDateRange: false, + }, + ], + }, + { + id: "admin", + icon: FileText, + reports: [ + { + type: "MITGLIEDERLISTE_REGISTER", + formats: ["PDF", "CSV"], + requiresDateRange: false, + }, + { + type: "VORSTANDSAENDERUNG", + formats: ["PDF"], + requiresDateRange: false, + }, + ], + }, +] + +// Mock data for generated reports history +const mockGeneratedReports: GeneratedReport[] = [ + { + id: "gr-001", + type: "AUSGABEPROTOKOLL", + typeName: "Ausgabeprotokoll", + format: "PDF", + status: "COMPLETED", + generatedAt: "2026-06-14T10:30:00Z", + generatedBy: "Patrick Plate", + fileSize: 245000, + }, + { + id: "gr-002", + type: "BESTANDSFUEHRUNG", + typeName: "Bestandsführung", + format: "CSV", + status: "COMPLETED", + generatedAt: "2026-06-13T15:20:00Z", + generatedBy: "Anna Schmidt", + fileSize: 12400, + }, + { + id: "gr-003", + type: "MITGLIEDERLISTE_REGISTER", + typeName: "Mitgliederliste Register", + format: "PDF", + status: "COMPLETED", + generatedAt: "2026-06-10T09:00:00Z", + generatedBy: "Patrick Plate", + fileSize: 89000, + }, +] + +export default function ReportsCenterPage() { + const t = useTranslations("reportsCenter") + const [selectedFormats, setSelectedFormats] = useState< + Record + >({}) + const [dateRanges, setDateRanges] = useState< + Record + >({}) + const [authorityYear, setAuthorityYear] = useState( + new Date().getFullYear().toString() + ) + const [authorityPassword, setAuthorityPassword] = useState("") + const [authorityDialogOpen, setAuthorityDialogOpen] = useState(false) + + const { data: generatedReports } = useGeneratedReports(10) + const generateReport = useGenerateReport() + const authorityExport = useAuthorityExport() + + const reports = generatedReports ?? mockGeneratedReports + + const handleGenerate = (reportType: string, formats: string[]) => { + const format = (selectedFormats[reportType] || formats[0]) as + | "PDF" + | "CSV" + | "JSON" + const dateRange = dateRanges[reportType] + + generateReport.mutate({ + type: reportType, + format, + dateFrom: dateRange?.from, + dateTo: dateRange?.to, + }) + } + + const handleAuthorityExport = () => { + authorityExport.mutate({ + year: parseInt(authorityYear), + password: authorityPassword, + reason: "Behördenanfrage", + }) + setAuthorityDialogOpen(false) + setAuthorityPassword("") + } + + return ( +
+ {/* Header */} +
+

{t("title")}

+

{t("subtitle")}

+
+ + {/* Behörden-Export Hero */} + + +
+ +
+

+ {t("authorityExport.title")} +

+

+ {t("authorityExport.description")} +

+
+
+ + + + + + + {t("authorityExport.dialogTitle")} + + {t("authorityExport.dialogDescription")} + + +
+
+ + {t("authorityExport.warning")} +
+
+ + +
+
+ + setAuthorityPassword(e.target.value)} + placeholder={t("authorityExport.passwordPlaceholder")} + /> +
+
+ + + + +
+
+
+
+ + {/* Report Categories */} + {reportCategories.map((category) => { + const Icon = category.icon + return ( +
+
+ +

+ {t(`categories.${category.id}`)} +

+
+
+ {category.reports.map((report) => ( + + + + {t(`reports.${report.type}.name`)} + +

+ {t(`reports.${report.type}.description`)} +

+
+ + {/* Format badges */} +
+ {report.formats.map((fmt) => ( + + setSelectedFormats((prev) => ({ + ...prev, + [report.type]: fmt, + })) + } + > + {fmt} + + ))} +
+ + {/* Date range picker */} + {report.requiresDateRange && ( +
+ + setDateRanges((prev) => ({ + ...prev, + [report.type]: { + ...prev[report.type], + from: e.target.value, + to: prev[report.type]?.to || "", + }, + })) + } + /> + + setDateRanges((prev) => ({ + ...prev, + [report.type]: { + ...prev[report.type], + from: prev[report.type]?.from || "", + to: e.target.value, + }, + })) + } + /> +
+ )} + + {/* Generate button */} + +
+
+ ))} +
+
+ ) + })} + + {/* Generated Reports History */} + + + + + {t("history.title")} + + + + {reports.length === 0 ? ( +
+ +

+ {t("history.empty")} +

+
+ ) : ( +
+ + + + + + + + + + + + + {reports.map((report) => ( + + + + + + + + + ))} + +
{t("history.report")}{t("history.format")}{t("history.date")}{t("history.user")}{t("history.size")}
{report.typeName} + + {report.format} + + + {new Date(report.generatedAt).toLocaleDateString( + "de-DE" + )} + {report.generatedBy} + {report.fileSize + ? `${(report.fileSize / 1024).toFixed(0)} KB` + : "—"} + + +
+
+ )} +
+
+
+ ) +} diff --git a/cannamanage-frontend/src/app/(marketing)/pricing/page.tsx b/cannamanage-frontend/src/app/(marketing)/pricing/page.tsx index b3f9f02..aae3701 100644 --- a/cannamanage-frontend/src/app/(marketing)/pricing/page.tsx +++ b/cannamanage-frontend/src/app/(marketing)/pricing/page.tsx @@ -159,11 +159,11 @@ export default function PricingPage() {
{/* Feature Comparison Table */} -
+

{t("comparisonTitle")}

-
+
diff --git a/cannamanage-frontend/src/data/navigations.ts b/cannamanage-frontend/src/data/navigations.ts index f8f124a..40fe045 100644 --- a/cannamanage-frontend/src/data/navigations.ts +++ b/cannamanage-frontend/src/data/navigations.ts @@ -2,7 +2,7 @@ import type { NavigationType } from "@/types" export const navigationsData: NavigationType[] = [ { - title: "Main", + title: "Betrieb", items: [ { title: "Dashboard", @@ -29,16 +29,31 @@ export const navigationsData: NavigationType[] = [ href: "/grow", iconName: "Sprout", }, - { - title: "Berichte", - href: "/reports", - iconName: "FileText", - }, + ], + }, + { + title: "Kommunikation", + items: [ { title: "Schwarzes Brett", href: "/info-board", iconName: "Megaphone", }, + { + title: "Kalender", + href: "/calendar", + iconName: "Calendar", + }, + { + title: "Forum", + href: "/forum", + iconName: "MessageSquare", + }, + ], + }, + { + title: "Verwaltung", + items: [ { title: "Finanzen", href: "/finance", @@ -59,16 +74,6 @@ export const navigationsData: NavigationType[] = [ href: "/board", iconName: "Shield", }, - { - title: "Kalender", - href: "/calendar", - iconName: "Calendar", - }, - { - title: "Forum", - href: "/forum", - iconName: "MessageSquare", - }, { title: "Personal", href: "/settings/staff", @@ -79,11 +84,21 @@ export const navigationsData: NavigationType[] = [ { title: "Compliance", items: [ + { + title: "Berichtszentrale", + href: "/reports-center", + iconName: "ChartBar", + }, { title: "Protokoll", href: "/audit-log", iconName: "ScrollText", }, + { + title: "Berichte", + href: "/reports", + iconName: "FileText", + }, ], }, ] diff --git a/cannamanage-frontend/src/services/compliance-reports.ts b/cannamanage-frontend/src/services/compliance-reports.ts new file mode 100644 index 0000000..01a8aad --- /dev/null +++ b/cannamanage-frontend/src/services/compliance-reports.ts @@ -0,0 +1,147 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" + +import { apiClient, apiDownload } from "@/lib/api-client" + +// --- Types --- + +export interface ReportTypeInfo { + type: string + name: string + description: string + category: "FINANCE" | "KCANG_COMPLIANCE" | "DSGVO" | "ADMINISTRATION" + formats: ("PDF" | "CSV" | "JSON")[] + requiresDateRange: boolean + requiresPassword: boolean +} + +export interface GenerateReportParams { + type: string + format: "PDF" | "CSV" | "JSON" + dateFrom?: string + dateTo?: string + year?: number + additionalParams?: Record +} + +export interface GeneratedReport { + id: string + type: string + typeName: string + format: string + status: "PENDING" | "COMPLETED" | "FAILED" + generatedAt: string + generatedBy: string + fileSize?: number + downloadUrl?: string +} + +export interface AuthorityExportParams { + year: number + password: string + reason: string +} + +export interface AuthorityExportResult { + id: string + year: number + generatedAt: string + reports: { type: string; name: string; format: string }[] + downloadUrl: string +} + +export interface ComplianceDeadline { + id: string + title: string + description: string + dueDate: string + area: string + status: "PENDING" | "OVERDUE" | "COMPLETED" + daysRemaining: number +} + +// --- Query Hooks --- + +export function useReportTypes() { + return useQuery({ + queryKey: ["reports", "types"], + queryFn: () => apiClient("/reports/types"), + }) +} + +export function useGeneratedReports(limit?: number) { + return useQuery({ + queryKey: ["reports", "generated", limit], + queryFn: () => + apiClient("/reports/generated", { + params: { limit: limit?.toString() }, + }), + }) +} + +export function useComplianceDeadlines() { + return useQuery({ + queryKey: ["compliance", "deadlines"], + queryFn: () => apiClient("/compliance/deadlines"), + }) +} + +// --- Mutation Hooks --- + +export function useGenerateReport() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (params: GenerateReportParams) => + apiClient("/reports/generate", { + method: "POST", + body: JSON.stringify(params), + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["reports", "generated"] }) + }, + }) +} + +export function useAuthorityExport() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: (params: AuthorityExportParams) => + apiClient("/reports/authority-export", { + method: "POST", + body: JSON.stringify(params), + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["reports", "generated"] }) + }, + }) +} + +// --- Download Functions --- + +export async function downloadReport(reportId: string) { + const { blob, filename } = await apiDownload( + `/reports/generated/${reportId}/download` + ) + triggerDownload(blob, filename || `report-${reportId}`) +} + +export async function downloadAuthorityExport(exportId: string) { + const { blob, filename } = await apiDownload( + `/reports/authority-export/${exportId}/download` + ) + triggerDownload(blob, filename || `behoerden-export-${exportId}.zip`) +} + +// --- Helpers --- + +function triggerDownload(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) +}