style(orion): align report header footer and page layout

This commit is contained in:
Schubert Ferenc 2026-07-11 12:19:10 +02:00
parent 302e542fda
commit 503b343070
40 changed files with 2010 additions and 148 deletions

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { AuthProvider, useAuth } from "@/components/auth";
import { BrandLogo } from "@/components/brand/brand-logo";
import { login } from "@/lib/api";
const schema = z.object({
@ -33,7 +34,7 @@ function LoginPanel() {
<main className="flex min-h-screen items-center justify-center bg-background px-4 py-10">
<section className="w-full max-w-md rounded-lg border border-border bg-surface p-8 shadow-soft">
<div className="mb-8">
<p className="text-sm font-medium text-primary">Validation Suite</p>
<BrandLogo />
<h1 className="mt-2 text-3xl font-semibold text-text">Anmelden</h1>
<p className="mt-3 text-sm leading-6 text-text-light">Sicherer Zugriff auf Atlas Workspace.</p>
</div>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 640 480">
<path d="M476,95 L483,94 L486,97 L486,99 L484,101 L477,102 Z" fill="#F60B2F"/>
<path d="M473,92 L473,112 L476,112 L477,104 L480,104 L485,112 L489,112 L485,106 L489,101 L488,93 Z" fill="#F60B2F"/>
<path d="M476,83 L487,84 L496,92 L498,97 L498,107 L496,111 L485,120 L475,120 L470,118 L464,112 L461,106 L461,98 L464,91 L469,86 Z" fill="#F60B2F"/>
<path d="M329,84 L328,399 L329,401 L338,401 L354,399 L358,396 L358,119 L363,118 L385,127 L391,133 L392,386 L403,381 L417,372 L421,365 L422,158 L432,170 L444,193 L450,211 L454,230 L454,250 L448,281 L441,297 L440,303 L460,316 L466,315 L478,284 L483,255 L483,232 L480,210 L470,178 L454,150 L431,124 L407,106 L378,92 L350,85 Z" fill="#F60B2F"/>
<path d="M308,83 L283,86 L256,94 L235,104 L211,121 L193,139 L178,160 L167,182 L159,208 L159,216 L162,219 L278,285 L281,289 L280,367 L269,364 L250,355 L229,340 L211,320 L203,308 L194,289 L190,285 L186,283 L163,283 L162,288 L168,305 L184,334 L195,348 L209,362 L236,381 L268,395 L298,401 L310,400 L310,271 L303,265 L201,207 L192,200 L207,170 L217,157 L229,145 L255,127 L280,117 L281,200 L284,202 L307,202 L310,200 L310,84 Z" fill="#F60B2F"/>
<path d="M473,80 L467,83 L461,89 L458,95 L458,108 L463,117 L468,121 L476,124 L487,123 L493,120 L498,115 L501,109 L501,94 L497,87 L491,82 L486,80 Z" fill="#F60B2F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,15 @@
import { BrandLogo } from "@/components/brand/brand-logo";
export default function Loading() {
return (
<main className="flex min-h-screen items-center justify-center bg-background">
<div className="rounded-lg border border-border bg-surface p-8 shadow-soft">
<BrandLogo />
<div className="mt-6 flex items-center justify-center gap-3 text-text-light">
<span className="spinner" />
<span>Validation Suite wird geladen.</span>
</div>
</div>
</main>
);
}

View file

@ -0,0 +1,17 @@
import Link from "next/link";
import { BrandLogo } from "@/components/brand/brand-logo";
export default function NotFound() {
return (
<main className="flex min-h-screen items-center justify-center bg-background px-4">
<section className="w-full max-w-lg rounded-lg border border-border bg-surface p-8 text-center shadow-soft">
<div className="flex justify-center">
<BrandLogo />
</div>
<h1 className="mt-8 text-3xl font-semibold">Seite nicht gefunden</h1>
<p className="mt-3 text-text-light">Die angeforderte Seite ist nicht vorhanden oder wurde verschoben.</p>
<Link href="/dashboard" className="btn btn-primary mt-6">Zum Dashboard</Link>
</section>
</main>
);
}