Restrict admin to system management

This commit is contained in:
michael committed 2026-08-06 08:07:26 +02:00
1 parent 0bef65ab75
commit 19e4621aeb
13 files changed
+634 -882

No files matched your search

+48 -9
View File
@@ -1,7 +1,8 @@
import { ChevronLeft, ChevronRight, Clock3, ExternalLink, RotateCcw, SlidersHorizontal, Ticket, UserCheck, Users } from "lucide-react";
import { ChevronLeft, ChevronRight, Clock3, Download, ExternalLink, RotateCcw, SlidersHorizontal, Ticket, UserCheck, Users } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { Alert } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -10,8 +11,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { CopyTicketButton } from "@/components/CopyTicketButton";
import { HelpLink } from "@/components/HelpLink";
import { getPeriodOverview } from "../api";
import { currentDay, currentMonth, formatMinutes } from "../format";
import { downloadMonthExport, getPeriodOverview } from "../api";
import { currentDay, currentMonth, formatDateTime, formatMinutes } from "../format";
import type { PeriodOverview, PeriodType, TicketSummary } from "../types";
type AnalysisPageProps = {
@@ -304,6 +305,7 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
const [period, setPeriod] = useState(currentMonth());
const [overview, setOverview] = useState<PeriodOverview | null>(null);
const [loading, setLoading] = useState(false);
const [exportingMonth, setExportingMonth] = useState(false);
const [ticketViewSettings, setTicketViewSettings] = useState<AnalysisTicketViewSettings>(() => readStoredTicketViewSettings());
const loadRequestId = useRef(0);
@@ -379,6 +381,35 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
}
async function exportMonth() {
if (periodType !== "month") {
return;
}
setExportingMonth(true);
try {
const { blob, filename } = await downloadMonthExport(period);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.success("Monatsexport erstellt", {
description: filename
});
} catch (error) {
toast.error("Monatsexport fehlgeschlagen", {
description: error instanceof Error ? error.message : "Unbekannter Fehler"
});
} finally {
setExportingMonth(false);
}
}
const totals = overview?.totals;
const periodLabel = periodType === "month" ? "Monat" : "Tag";
const metricCards: Array<{ label: string; value: string | number; detail?: string; icon: LucideIcon }> = [
@@ -493,9 +524,21 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
</Button>
</div>
</div>
{periodType === "month" ? (
<Button type="button" variant="secondary" className="sm:col-span-2" disabled={exportingMonth} onClick={() => void exportMonth()}>
<Download className="size-4" />
{exportingMonth ? "Exportiert..." : "Monat exportieren"}
</Button>
) : null}
</div>
</div>
{periodType === "month" && overview?.closed ? (
<Alert variant="success" className="py-3 text-sm">
Dieser Monat wurde am {formatDateTime(overview.closedAt!)} abgeschlossen. Bewertungen und Änderungen sind geschützt, bis der Monat im Monatsabschluss wieder geöffnet wird.
</Alert>
) : null}
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
{metricCards.map(({ label, value, detail, icon: Icon }) => (
<Card key={label}>
@@ -834,19 +877,15 @@ export function AnalysisPage({ onNavigate }: AnalysisPageProps) {
</div>
<TicketStatus ticket={ticket} />
</div>
<div className="grid grid-cols-3 gap-2 text-sm">
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="rounded-md bg-muted/50 p-2">
<p className="text-muted-foreground">Abr.</p>
<p className="font-medium">{formatMinutes(ticket.total_minutes)}</p>
</div>
<div className="rounded-md bg-muted/50 p-2">
<p className="text-muted-foreground">Abr.</p>
<p className="text-muted-foreground">Sessions</p>
<p className="font-medium">{ticket.billed_count}</p>
</div>
<div className="rounded-md bg-muted/50 p-2">
<p className="text-muted-foreground">Nicht</p>
<p className="font-medium">{formatMinutes(ticket.non_billable_minutes)}</p>
</div>
</div>
<Button className="w-full" size="sm" variant="secondary" onClick={() => onNavigate(`/analysis/${periodType}/${period}/tickets/${ticket.id}`)}>
Öffnen