176 lines
5.6 KiB
TypeScript
176 lines
5.6 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { createServiceClient } from "@/lib/supabase";
|
|
import { requireAdmin } from "@/lib/admin-auth";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export async function GET() {
|
|
const check = await requireAdmin();
|
|
if (check instanceof NextResponse) return check;
|
|
|
|
const db = createServiceClient();
|
|
|
|
const heute = new Date();
|
|
heute.setHours(0, 0, 0, 0);
|
|
const heuteISO = heute.toISOString().slice(0, 10);
|
|
|
|
// Fenster: 12 Monate zurück für Statistiken
|
|
const vor12Monaten = new Date(heute);
|
|
vor12Monaten.setMonth(vor12Monaten.getMonth() - 12);
|
|
const vor12ISO = vor12Monaten.toISOString().slice(0, 10);
|
|
|
|
// Fenster: 3 Monate voraus für Planungsübersicht
|
|
const in3Monaten = new Date(heute);
|
|
in3Monaten.setMonth(in3Monaten.getMonth() + 3);
|
|
in3Monaten.setDate(in3Monaten.getDate() + 1);
|
|
const in3ISO = in3Monaten.toISOString().slice(0, 10);
|
|
|
|
// ── Alle Anfragen (Status + Datum) ─────────────────────────────────────
|
|
const { data: anfragen } = await db
|
|
.from("anfragen")
|
|
.select("id, status, created_at, firma, telefon, email")
|
|
.order("created_at", { ascending: false });
|
|
|
|
// ── Alle Positionen (Statistik: letzte 12 Monate) ──────────────────────
|
|
const { data: allePositionen } = await db
|
|
.from("anfragen_positionen")
|
|
.select(
|
|
"id, anfrage_id, maschine_name, maschine_kategorie, mietbeginn, mietende, gesamt_tage, tagessatz, lieferung"
|
|
)
|
|
.gte("mietbeginn", vor12ISO)
|
|
.order("mietbeginn");
|
|
|
|
// ── Planungs-Positionen (heute bis +3 Monate) ──────────────────────────
|
|
const { data: planPositionen } = 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])
|
|
);
|
|
|
|
// ── KPI-Berechnung (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<string, boolean>();
|
|
(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)
|
|
);
|
|
|
|
const kpiPositionen = (allePositionen ?? []).filter((p) =>
|
|
bestaetigtIds.has(p.anfrage_id)
|
|
);
|
|
|
|
const umsatzGesamt = kpiPositionen.reduce(
|
|
(sum, p) => sum + (p.tagessatz != null ? p.tagessatz * p.gesamt_tage : 0),
|
|
0
|
|
);
|
|
const mietTageGesamt = kpiPositionen.reduce(
|
|
(sum, p) => sum + (p.gesamt_tage ?? 0),
|
|
0
|
|
);
|
|
|
|
// ── Auslastung pro Maschine ─────────────────────────────────────────────
|
|
const maschinenMap: Record<
|
|
string,
|
|
{ name: string; kategorie: string; tage: number; umsatz: number; anfragen: 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,
|
|
anfragen: 0,
|
|
};
|
|
}
|
|
maschinenMap[key].tage += p.gesamt_tage ?? 0;
|
|
maschinenMap[key].umsatz +=
|
|
p.tagessatz != null ? p.tagessatz * p.gesamt_tage : 0;
|
|
maschinenMap[key].anfragen += 1;
|
|
}
|
|
|
|
const maschinenStats = Object.values(maschinenMap).sort(
|
|
(a, b) => b.tage - a.tage
|
|
);
|
|
|
|
// ── Monatliche Entwicklung (letzte 6 Monate) ───────────────────────────
|
|
const monatsStats: {
|
|
monat: string;
|
|
label: string;
|
|
anfragen: 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 mPositionen = (allePositionen ?? []).filter(
|
|
(p) =>
|
|
bestaetigtIds.has(p.anfrage_id) &&
|
|
p.mietbeginn?.startsWith(monat)
|
|
);
|
|
|
|
monatsStats.push({
|
|
monat,
|
|
label,
|
|
anfragen: mPositionen.length,
|
|
tage: mPositionen.reduce((s, p) => s + (p.gesamt_tage ?? 0), 0),
|
|
umsatz: mPositionen.reduce(
|
|
(s, p) =>
|
|
s + (p.tagessatz != null ? p.tagessatz * p.gesamt_tage : 0),
|
|
0
|
|
),
|
|
});
|
|
}
|
|
|
|
// ── Planungs-Daten mit Anfragestatus anreichern ─────────────────────────
|
|
const planDaten = (planPositionen ?? []).map((p) => {
|
|
const anfrage = anfrageMap.get(p.anfrage_id);
|
|
return {
|
|
...p,
|
|
anfrage_status: anfrage?.status ?? "offen",
|
|
firma: anfrage?.firma ?? "",
|
|
};
|
|
});
|
|
|
|
return NextResponse.json({
|
|
kpi: {
|
|
anfragenGesamt: anfragen?.length ?? 0,
|
|
bestaetigtGesamt: bestaetigtIds.size,
|
|
umsatzGesamt,
|
|
mietTageGesamt,
|
|
},
|
|
maschinenStats,
|
|
monatsStats,
|
|
planDaten,
|
|
heuteISO,
|
|
in3ISO,
|
|
});
|
|
}
|