style(orion): align report header footer and page layout
This commit is contained in:
parent
302e542fda
commit
503b343070
40 changed files with 2010 additions and 148 deletions
|
|
@ -1,20 +1,37 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AlertTriangle, CheckCircle2, ClipboardList, Clock, FilePenLine } from "lucide-react";
|
||||
import { AlertTriangle, CheckCircle2, ClipboardList, Clock, FilePenLine, Gauge, Plus, Stethoscope, TestTube2, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useAuth } from "@/components/auth";
|
||||
import { apiGet } from "@/lib/api";
|
||||
import { BrandLogo } from "@/components/brand/brand-logo";
|
||||
|
||||
type DashboardData = {
|
||||
customers: number;
|
||||
devices: number;
|
||||
equipment: number;
|
||||
validations: number;
|
||||
validation_drafts: number;
|
||||
validation_ready: number;
|
||||
validation_in_review: number;
|
||||
validation_approved: number;
|
||||
validation_completed: number;
|
||||
validation_overdue: number;
|
||||
equipment_green: number;
|
||||
equipment_yellow: number;
|
||||
equipment_red: number;
|
||||
};
|
||||
|
||||
const cards = [
|
||||
const primaryCards = [
|
||||
{ label: "Neue Validierung", value: "Starten", href: "/validations/new", icon: Plus },
|
||||
{ label: "Validierungen", key: "validations", href: "/validations", icon: ClipboardList },
|
||||
{ label: "Kunden", key: "customers", href: "/customers", icon: Users },
|
||||
{ label: "Geraete", key: "devices", href: "/devices", icon: Stethoscope },
|
||||
{ label: "Pruefmittel", key: "equipment", href: "/equipment", icon: TestTube2 }
|
||||
] as const;
|
||||
|
||||
const workflowCards = [
|
||||
{ label: "Entwuerfe", key: "validation_drafts", href: "/validations?status=ENTWURF", icon: FilePenLine },
|
||||
{ label: "Bereit zur Pruefung", key: "validation_ready", href: "/validations?status=BEREIT_ZUR_PRUEFUNG", icon: ClipboardList },
|
||||
{ label: "In Pruefung", key: "validation_in_review", href: "/validations?status=IN_PRUEFUNG", icon: Clock },
|
||||
|
|
@ -32,27 +49,63 @@ export default function DashboardPage() {
|
|||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<header>
|
||||
<h1 className="text-3xl font-semibold text-text">Dashboard</h1>
|
||||
<header className="rounded-lg border border-border bg-surface p-6 shadow-soft">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-center">
|
||||
<BrandLogo />
|
||||
<div className="text-sm text-text-light">
|
||||
<p>Benutzer: angemeldet</p>
|
||||
<p>Speicherstatus: synchronisiert</p>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-6 text-3xl font-semibold text-text">Dashboard</h1>
|
||||
<p className="mt-2 text-text-light">Validierungsworkflow und faellige Revalidierungen.</p>
|
||||
</header>
|
||||
<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||
{cards.map((item) => {
|
||||
{primaryCards.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const value = "key" in item ? query.data?.[item.key] ?? 0 : item.value;
|
||||
return (
|
||||
<Link key={item.key} href={item.href} className="rounded-lg border border-border bg-surface p-6 shadow-soft transition hover:-translate-y-0.5 hover:border-primary/40">
|
||||
<Link key={item.label} href={item.href} className="rounded-lg border border-border bg-surface p-6 shadow-soft transition hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-text-light">{item.label}</p>
|
||||
<Icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<p className="mt-6 text-4xl font-semibold text-text">{query.data?.[item.key] ?? 0}</p>
|
||||
<p className="mt-6 text-4xl font-semibold text-text">{value}</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<section className="rounded-lg border border-border bg-surface p-6 shadow-soft">
|
||||
<h2 className="text-xl font-semibold">Zuletzt bearbeitete Validierungen</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-text-light">Die Validierungsverwaltung bietet Suche, Filter, Sortierung, Vorschau, Export und Workflow-Aktionen.</p>
|
||||
<section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||
{workflowCards.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link key={item.key} href={item.href} className="rounded-lg border border-border bg-surface p-5 shadow-soft transition hover:border-primary/40 hover:bg-background">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-text-light">{item.label}</p>
|
||||
<Icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<p className="mt-4 text-3xl font-semibold text-text">{query.data?.[item.key] ?? 0}</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
<section className="grid gap-4 xl:grid-cols-4">
|
||||
<Link href="/validations?sort_by=updated_at&sort_order=desc" className="rounded-lg border border-border bg-surface p-6 shadow-soft transition hover:border-primary/40 hover:bg-background">
|
||||
<h2 className="text-xl font-semibold">Zuletzt bearbeitet</h2>
|
||||
<p className="mt-3 text-sm leading-6 text-text-light">Aktuelle Validierungen nach Bearbeitungsdatum oeffnen.</p>
|
||||
</Link>
|
||||
<Link href="/validations?overdue_only=true" className="rounded-lg border border-border bg-surface p-6 shadow-soft transition hover:border-danger/40 hover:bg-background">
|
||||
<div className="flex items-center justify-between"><h2 className="text-xl font-semibold">Ueberfaellige Revalidierungen</h2><AlertTriangle className="h-5 w-5 text-danger" /></div>
|
||||
<p className="mt-4 text-3xl font-semibold text-text">{query.data?.validation_overdue ?? 0}</p>
|
||||
</Link>
|
||||
<Link href="/equipment" className="rounded-lg border border-border bg-surface p-6 shadow-soft transition hover:border-primary/40 hover:bg-background">
|
||||
<div className="flex items-center justify-between"><h2 className="text-xl font-semibold">Kalibrierstatus</h2><Gauge className="h-5 w-5 text-primary" /></div>
|
||||
<p className="mt-4 text-sm text-text-light">Gruen {query.data?.equipment_green ?? 0} · Gelb {query.data?.equipment_yellow ?? 0} · Rot {query.data?.equipment_red ?? 0}</p>
|
||||
</Link>
|
||||
<Link href="/validations?status=ABGESCHLOSSEN" className="rounded-lg border border-border bg-surface p-6 shadow-soft transition hover:border-primary/40 hover:bg-background">
|
||||
<h2 className="text-xl font-semibold">Letzte Berichte</h2>
|
||||
<p className="mt-4 text-3xl font-semibold text-text">{query.data?.validation_completed ?? 0}</p>
|
||||
</Link>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default function ValidationPreviewPage() {
|
|||
const params = useParams<{ id: string }>();
|
||||
const [html, setHtml] = useState("");
|
||||
const [message, setMessage] = useState("Vorschau wird geladen.");
|
||||
const [toast, setToast] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || !params.id) return;
|
||||
|
|
@ -33,6 +34,11 @@ export default function ValidationPreviewPage() {
|
|||
const response = await fetch(`${API_BASE}/validations/${params.id}/report.pdf`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => null);
|
||||
setToast(body?.detail ?? "PDF konnte nicht erzeugt werden.");
|
||||
return;
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
|
@ -54,6 +60,7 @@ export default function ValidationPreviewPage() {
|
|||
<button type="button" onClick={downloadPdf} className="btn btn-primary h-12"><Download className="h-4 w-4" /> PDF herunterladen</button>
|
||||
</div>
|
||||
</header>
|
||||
{toast && <div className="fixed right-4 top-4 z-50 max-w-md rounded-lg border border-danger/30 bg-white p-4 text-sm text-danger shadow-soft">{toast}</div>}
|
||||
{message ? <div className="rounded-lg border border-border bg-surface p-6 shadow-soft">{message}</div> : <iframe title="Validierungsbericht" srcDoc={html} className="h-[78vh] w-full rounded-lg border border-border bg-white shadow-soft" />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue