feat(customers): add dashboard and customer management

This commit is contained in:
Schubert Ferenc 2026-07-02 23:37:46 +02:00
parent 694b7bd09a
commit 92cb8d1286
28 changed files with 2521 additions and 13 deletions

View file

@ -1,15 +1,111 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import SummaryCard from "@/components/common/SummaryCard";
import { api } from "@/lib/api";
import type { DashboardSummary, EmptyWidget } from "@/types/dashboard";
function getErrorMessage(error: unknown) {
if (typeof error === "object" && error !== null && "response" in error) {
const response = (error as { response?: { data?: { detail?: string } } }).response;
return response?.data?.detail ?? "Dashboard konnte nicht geladen werden";
}
return "Dashboard konnte nicht geladen werden";
}
export default function DashboardPage() {
return (
<main className="min-h-screen bg-slate-100">
<div className="mx-auto max-w-7xl p-8">
<h1 className="text-4xl font-bold">
Olympus Dashboard
</h1>
<p className="mt-4 text-slate-600">
Willkommen bei Funktechnik Schubert.
</p>
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const loadSummary = useCallback(async () => {
try {
const response = await api.get<DashboardSummary>("/dashboard/summary");
setSummary(response.data);
} catch (err) {
setError(getErrorMessage(err));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
queueMicrotask(() => {
void loadSummary();
});
}, [loadSummary]);
if (loading) {
return <div className="rounded-lg border bg-white p-8 text-slate-500">Dashboard wird geladen...</div>;
}
if (error || !summary) {
return <div className="rounded-lg border bg-white p-8 text-red-600">{error || "Keine Dashboarddaten verfügbar"}</div>;
}
const cards = [...summary.customers, ...summary.users, ...summary.roles];
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-slate-950">Dashboard</h1>
<p className="mt-1 text-sm text-slate-500">Operative Übersicht für Olympus CRM</p>
</div>
{cards.length > 0 ? (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{cards.map((card) => (
<SummaryCard key={card.label} label={card.label} value={card.value} />
))}
</div>
</main>
);
}
) : (
<EmptyPanel widget={{ title: "Keine Kennzahlen", message: "Für deine Berechtigungen sind aktuell keine Kennzahlen verfügbar." }} />
)}
<section className="rounded-lg border bg-white p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-slate-950">Letzte Kunden</h2>
{summary.latest_customers.length > 0 && (
<Link href="/customers" className="text-sm font-medium text-slate-600 hover:text-slate-950">
Alle Kunden
</Link>
)}
</div>
{summary.latest_customers.length === 0 ? (
<p className="text-sm text-slate-500">Noch keine Kundendaten vorhanden.</p>
) : (
<div className="divide-y">
{summary.latest_customers.map((customer) => (
<Link key={customer.id} href={`/customers/${customer.id}`} className="flex items-center justify-between py-3 hover:bg-slate-50">
<div>
<p className="font-medium text-slate-950">{customer.company_name}</p>
<p className="text-sm text-slate-500">{customer.customer_number}</p>
</div>
<span className="text-sm text-slate-500">{customer.status}</span>
</Link>
))}
</div>
)}
</section>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<EmptyPanel widget={summary.activities} />
<EmptyPanel widget={summary.tasks} />
<EmptyPanel widget={summary.tickets} />
<EmptyPanel widget={summary.projects} />
</div>
</div>
);
}
function EmptyPanel({ widget }: { widget: EmptyWidget }) {
return (
<div className="rounded-lg border bg-white p-5">
<h2 className="font-semibold text-slate-950">{widget.title}</h2>
<p className="mt-2 text-sm text-slate-500">{widget.message}</p>
</div>
);
}