import { createServiceClient } from "@/lib/supabase"; import { AdminNav } from "@/components/admin/AdminNav"; import { GanttKalender } from "./GanttKalender"; import type { Metadata } from "next"; export const metadata: Metadata = { title: "Statistik & Planung" }; export const dynamic = "force-dynamic"; function fmt(n: number, nachkomma = 0) { return n.toLocaleString("de-DE", { minimumFractionDigits: nachkomma, maximumFractionDigits: nachkomma, }); } // Maximale Balkenbreite in % relativ zum Top-Wert function balkenBreite(wert: number, max: number): string { if (max === 0) return "0%"; return `${Math.max(4, Math.round((wert / max) * 100))}%`; } export default async function StatistikPage() { const db = createServiceClient(); const heute = new Date(); heute.setHours(0, 0, 0, 0); const heuteISO = heute.toISOString().slice(0, 10); const vor12Monaten = new Date(heute); vor12Monaten.setMonth(vor12Monaten.getMonth() - 12); const vor12ISO = vor12Monaten.toISOString().slice(0, 10); const in3Monaten = new Date(heute); in3Monaten.setMonth(in3Monaten.getMonth() + 3); in3Monaten.setDate(in3Monaten.getDate() + 1); const in3ISO = in3Monaten.toISOString().slice(0, 10); // ── DB-Abfragen ────────────────────────────────────────────────────────── const { data: anfragen } = await db .from("anfragen") .select("id, status, created_at, firma"); const { data: allePositionen } = await db .from("anfragen_positionen") .select( "id, anfrage_id, maschine_name, maschine_kategorie, mietbeginn, mietende, gesamt_tage, tagessatz" ) .gte("mietbeginn", vor12ISO) .order("mietbeginn"); const { data: planRoh } = await db .from("anfragen_positionen") .select( "id, anfrage_id, maschine_name, maschine_kategorie, mietbeginn, mietende, gesamt_tage, tagessatz" ) .lte("mietbeginn", in3ISO) .gte("mietende", heuteISO) .order("mietbeginn"); const anfrageMap = new Map( (anfragen ?? []).map((a) => [a.id, a]) ); // Status-Sets (nur abgeschlossene Mietvorgänge zählen, die zuvor bestätigt waren) // Lade Audit Log um zu verifizieren: bestätigt → abgeschlossenen Workflow const { data: auditLogs } = await db .from("anfrage_status_audit") .select("anfrage_id, status_zu"); // Map: anfrage_id → hat bestätigt Status durchlaufen? const hatBestaetigt = new Map(); (auditLogs ?? []).forEach((log: any) => { if (log.status_zu === "bestaetigt") { hatBestaetigt.set(log.anfrage_id, true); } }); const bestaetigtIds = new Set( (anfragen ?? []) .filter((a) => a.status === "abgeschlossen" && hatBestaetigt.has(a.id)) .map((a) => a.id) ); // ── KPIs ──────────────────────────────────────────────────────────────── const kpiPos = (allePositionen ?? []).filter((p) => bestaetigtIds.has(p.anfrage_id)); const umsatzGesamt = kpiPos.reduce( (s, p) => s + (p.tagessatz != null ? p.tagessatz * p.gesamt_tage : 0), 0 ); const mietTageGesamt = kpiPos.reduce((s, p) => s + (p.gesamt_tage ?? 0), 0); const offenAnzahl = (anfragen ?? []).filter((a) => a.status === "offen").length; // ── Auslastung pro Maschine ────────────────────────────────────────────── const maschinenMap: Record< string, { name: string; kategorie: string; tage: number; umsatz: number; einsaetze: number } > = {}; for (const p of allePositionen ?? []) { if (!bestaetigtIds.has(p.anfrage_id)) continue; const key = p.maschine_name; if (!maschinenMap[key]) { maschinenMap[key] = { name: p.maschine_name, kategorie: p.maschine_kategorie ?? "", tage: 0, umsatz: 0, einsaetze: 0, }; } maschinenMap[key].tage += p.gesamt_tage ?? 0; maschinenMap[key].umsatz += p.tagessatz != null ? p.tagessatz * p.gesamt_tage : 0; maschinenMap[key].einsaetze += 1; } const maschinenStats = Object.values(maschinenMap).sort((a, b) => b.tage - a.tage); const maxTage = maschinenStats[0]?.tage ?? 1; // ── Monatliche Entwicklung (letzte 6 Monate) ──────────────────────────── const monatsStats: { monat: string; label: string; einsaetze: number; tage: number; umsatz: number; }[] = []; for (let i = 5; i >= 0; i--) { const d = new Date(heute); d.setMonth(d.getMonth() - i); const monat = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`; const label = d.toLocaleDateString("de-DE", { month: "short", year: "2-digit" }); const mPos = (allePositionen ?? []).filter( (p) => bestaetigtIds.has(p.anfrage_id) && p.mietbeginn?.startsWith(monat) ); monatsStats.push({ monat, label, einsaetze: mPos.length, tage: mPos.reduce((s, p) => s + (p.gesamt_tage ?? 0), 0), umsatz: mPos.reduce( (s, p) => s + (p.tagessatz != null ? p.tagessatz * p.gesamt_tage : 0), 0 ), }); } const maxUmsatz = Math.max(...monatsStats.map((m) => m.umsatz), 1); // ── Planungs-Daten anreichern ───────────────────────────────────────── const planDaten = (planRoh ?? []).map((p) => { const a = anfrageMap.get(p.anfrage_id); return { ...p, anfrage_status: a?.status ?? "offen", firma: a?.firma ?? "–", }; }); return (

Statistik & Planung

Letzte 12 Monate · Bestätigte Vermietungen

{/* ── KPI-Cards ─────────────────────────────────────────────────── */}
{[ { label: "Anfragen gesamt", wert: fmt(anfragen?.length ?? 0), sub: `${offenAnzahl} offen`, farbe: "border-l-slate-400", }, { label: "Bestätigte Mieten", wert: fmt(bestaetigtIds.size), sub: "letzte 12 Monate", farbe: "border-l-green-500", }, { label: "Vermietete Tage", wert: fmt(mietTageGesamt), sub: `Ø ${fmt(mietTageGesamt / Math.max(bestaetigtIds.size, 1), 1)} Tage/Miete`, farbe: "border-l-[#f7d334]", }, { label: "Netto-Umsatz", wert: `${fmt(umsatzGesamt)} €`, sub: `Ø ${fmt(umsatzGesamt / Math.max(6, 1))} €/Monat`, farbe: "border-l-blue-500", }, ].map((k) => (

{k.label}

{k.wert}

{k.sub}

))}
{/* ── Monatsüberblick (Balkendiagramm) ─────────────────────────── */}

Umsatz letzte 6 Monate (netto)

{monatsStats.map((m) => (
{m.umsatz > 0 ? `${fmt(m.umsatz)} €` : "–"}
0 ? 8 : 0, Math.round((m.umsatz / maxUmsatz) * 80))}px`, }} />
{m.label} {m.einsaetze} Eins.
))}
{/* ── Gantt-Kalender ───────────────────────────────────────────── */}

Vermietungsplanung – aktuelle & nächste 3 Monate

{planDaten.filter((p) => p.anfrage_status !== "abgelehnt").length} Positionen
{/* ── Auslastungstabelle pro Maschine ─────────────────────────── */}

Auslastung pro Maschine letzte 12 Monate

{maschinenStats.length === 0 ? (

Noch keine bestätigten Vermietungen vorhanden.

) : (
{maschinenStats.map((m, i) => ( ))}
Maschine Kategorie Einsätze Miettage Auslastung Umsatz netto
{m.name} {m.kategorie} {m.einsaetze}× {fmt(m.tage)}
{Math.round((m.tage / maxTage) * 100)}%
{m.umsatz > 0 ? `${fmt(m.umsatz)} €` : "–"}
Gesamt {fmt(maschinenStats.reduce((s, m) => s + m.einsaetze, 0))}× {fmt(mietTageGesamt)} {fmt(umsatzGesamt)} €
)}
); }