initial commit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
130
src/admin/components/dashboard/DashKpiCards.tsx
Normal file
130
src/admin/components/dashboard/DashKpiCards.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user