Files
app/src/admin/components/dashboard/DashKpiCards.tsx
2026-03-24 19:59:14 +01:00

157 lines
4.0 KiB
TypeScript

import { motion } from "framer-motion";
import { formatCurrency } from "../../utils/formatters";
interface KpiCard {
label: string;
value: string;
sub?: string;
color: string;
footer: string | null;
}
interface RevenueItem {
amount: number;
currency: string;
}
interface InvoicesData {
revenue_this_month: RevenueItem[];
revenue_czk?: number | null;
unpaid_count: number;
}
interface DashData {
attendance?: {
present_today: number;
total_active: number;
on_leave: number;
};
offers?: {
open_count: number;
created_this_month: number;
};
invoices?: InvoicesData;
leave_pending?: {
count: number;
};
}
interface DashKpiCardsProps {
dashData: DashData | null;
}
function buildKpiCards(dashData: DashData | null): KpiCard[] {
const cards: KpiCard[] = [];
if (dashData?.attendance) {
cards.push({
label: "Přítomní dnes",
value: `${dashData.attendance.present_today}`,
sub: `/ ${dashData.attendance.total_active}`,
color: "success",
footer:
dashData.attendance.on_leave > 0
? `${dashData.attendance.on_leave} nepřítomných`
: null,
});
}
if (dashData?.offers) {
cards.push({
label: "Otevřené nabídky",
value: `${dashData.offers.open_count}`,
color: "info",
footer:
dashData.offers.created_this_month > 0
? `${dashData.offers.created_this_month} tento měsíc`
: null,
});
}
if (dashData?.invoices) {
cards.push(buildInvoiceKpi(dashData.invoices));
}
if (dashData?.leave_pending) {
cards.push({
label: "Žádosti o volno",
value: `${dashData.leave_pending.count}`,
color: "danger",
footer: dashData.leave_pending.count > 0 ? "čeká na schválení" : null,
});
}
return cards;
}
function buildInvoiceKpi(invoices: InvoicesData): KpiCard {
const rev = invoices.revenue_this_month || [];
const hasForeign = rev.some((r) => r.currency !== "CZK");
const hasCzkTotal =
hasForeign &&
invoices.revenue_czk !== null &&
invoices.revenue_czk !== undefined;
const fallbackText =
rev.length > 0
? rev.map((r) => formatCurrency(r.amount, r.currency)).join(" · ")
: "0 Kč";
const revenueText = hasCzkTotal
? formatCurrency(invoices.revenue_czk!, "CZK")
: fallbackText;
const detailText =
hasForeign && rev.length > 0
? rev.map((r) => formatCurrency(r.amount, r.currency)).join(" · ")
: null;
const unpaidText =
invoices.unpaid_count > 0 ? `${invoices.unpaid_count} neuhrazených` : null;
const footerParts = [detailText, unpaidText].filter(Boolean);
return {
label: "Tržby (měsíc)",
value: revenueText,
color: "warning",
footer: footerParts.length > 0 ? footerParts.join(" · ") : null,
};
}
const KPI_CLASS_MAP: Record<number, string> = {
4: "dash-kpi-4",
3: "dash-kpi-3",
2: "dash-kpi-2",
1: "dash-kpi-1",
};
export default function DashKpiCards({ dashData }: DashKpiCardsProps) {
const kpiCards = buildKpiCards(dashData);
if (kpiCards.length === 0) {
return null;
}
const kpiClass = KPI_CLASS_MAP[Math.min(kpiCards.length, 4)] || "dash-kpi-4";
return (
<motion.div
className={`dash-kpi-grid ${kpiClass}`}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.06 }}
>
{kpiCards.map((kpi) => (
<div key={kpi.label} className={`admin-stat-card ${kpi.color}`}>
<div className="admin-stat-label">{kpi.label}</div>
<div className="admin-stat-value admin-mono">
{kpi.value}
{kpi.sub && (
<small
className="text-muted"
style={{
fontSize: "0.75em",
fontWeight: 500,
marginLeft: "0.25rem",
}}
>
{kpi.sub}
</small>
)}
</div>
{kpi.footer && <div className="admin-stat-footer">{kpi.footer}</div>}
</div>
))}
</motion.div>
);
}