initial commit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-23 08:46:51 +01:00
commit 4608494a3f
130 changed files with 40361 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
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>
)
}