// Dashboard.jsx (function () { const { useState, useEffect, useRef } = React; // ── Helpers ────────────────────────────────────────────────────── function fmtVal(v, unit) { if (unit === 'zł') { if (v >= 1000000) return (v / 1000000).toFixed(2).replace('.',',') + ' M zł'; if (v >= 1000) return Math.round(v / 1000) + ' k zł'; return v + ' zł'; } if (unit === '%') return v.toFixed(1).replace('.',',') + '%'; return v.toLocaleString('pl-PL'); } function fmtNum(v, unit) { if (unit === 'zł') { if (v >= 1000000) return (v / 1000000).toFixed(2).replace('.',',') + ' M'; if (v >= 1000) return Math.round(v / 1000) + ' k'; return Math.round(v).toLocaleString('pl-PL'); } if (unit === '%') return v.toFixed(1).replace('.',','); return Math.round(v).toLocaleString('pl-PL'); } // ── CountUp hook ──────────────────────────────────────────────── function useCountUp(target, duration = 1400) { const [val, setVal] = useState(0); useEffect(() => { let raf; const t0 = performance.now(); const tick = (now) => { const p = Math.min((now - t0) / duration, 1); const e = 1 - Math.pow(1 - p, 3); setVal(e * target); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [target]); return val; } // ── Glass Tooltip ─────────────────────────────────────────────── function GlassTooltip({ active, payload, label, unit }) { if (!active || !payload?.length) return null; return (
{label}
{payload.map((p, i) => (
{fmtVal(p.value, unit || 'zł')}
))}
); } // ── Sparkline ─────────────────────────────────────────────────── function Sparkline({ data, color }) { const { AreaChart, Area, ResponsiveContainer } = window.Recharts || {}; const d = data.map((v, i) => ({ v, i })); const gradId = 'sg' + color.replace(/[^a-z0-9]/gi, ''); return ( ); } // ── KPI Card ──────────────────────────────────────────────────── function KPICard({ kpi }) { const animated = useCountUp(kpi.value); const up = kpi.trend >= 0; return (
{/* glow blob */}
{kpi.label}
{fmtNum(animated, kpi.unit)} {kpi.unit === '%' && %} {kpi.unit === 'zł' && }
{up ? '↑' : '↓'} {Math.abs(kpi.trend).toFixed(1).replace('.',',')}% vs poprzedni okres
); } // ── Main Sales Chart ──────────────────────────────────────────── function SalesChart({ data }) { const { AreaChart, Area, ResponsiveContainer, XAxis, YAxis, Tooltip, CartesianGrid } = window.Recharts || {}; const [period, setPeriod] = useState('30d'); const d = data[period]; return (
Sprzedaż w czasie
Przychód vs poprzedni okres
{['7d','30d','12m'].map(p => ( ))}
v >= 1000 ? `${v/1000}k` : v} width={42} /> } cursor={{ stroke:'rgba(124,92,255,.25)', strokeWidth:1 }} />
); } // ── Donut Chart ───────────────────────────────────────────────── function DonutChart({ data }) { const { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } = window.Recharts || {}; const RADIAN = Math.PI / 180; const total = data.reduce((s, d) => s + d.value, 0); return (
Leady wg źródła
Łącznie {total}%
{data.map((d, i) => )} { if (!active || !payload?.length) return null; const p = payload[0].payload; return (
{p.name}: {p.value}%
); }} />
{data.map((d, i) => (
{d.name} {d.value}%
))}
); } // ── Goal Gauge ────────────────────────────────────────────────── function GoalGauge({ value }) { const { RadialBarChart, RadialBar, ResponsiveContainer } = window.Recharts || {}; const animated = useCountUp(value); const d = [{ value: Math.round(animated), fill: '#7C5CFF' }, { value: 100 - Math.round(animated), fill: 'rgba(124,92,255,0.08)' }]; return (
Cel miesięczny
Realizacja planu Q2
{Math.round(animated)}%
wykonania
); } // ── Activity Feed ─────────────────────────────────────────────── function ActivityFeed({ items }) { return (
Ostatnia aktywność
{items.map((a, i) => (
{i < items.length-1 &&
}
{a.text}
{a.time}
))}
); } // ── Today Tasks ───────────────────────────────────────────────── function TodayTasks({ tasks, setTasks }) { const priorityColor = { high: '#EF4444', medium: '#F59E0B', low: '#22C55E' }; const toggle = (id) => setTasks(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t)); return (
Zadania na dziś
{tasks.filter(t => !t.done).length} pozostało
{tasks.map(t => (
toggle(t.id)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px', borderRadius: 10, cursor: 'pointer', transition: 'background .15s', background: t.done ? 'rgba(255,255,255,0.02)' : 'var(--bg-hover)', opacity: t.done ? 0.55 : 1, }} onMouseEnter={e => e.currentTarget.style.background='var(--bg2)'} onMouseLeave={e => e.currentTarget.style.background=t.done?'rgba(255,255,255,0.02)':'var(--bg-hover)'} >
{t.done && }
{t.text}
))}
); } // ── Dashboard (main export) ───────────────────────────────────── window.Dashboard = function Dashboard({ todayTasks, setTodayTasks, kanbanLeads }) { const S = window.SEED; const leads = kanbanLeads || S.kanbanLeads; // Derive KPIs from the sales funnel (Kanban) data const totalLeads = leads.length; const wonLeads = leads.filter(l => l.column === 'won'); const lostLeads = leads.filter(l => l.column === 'lost'); const pipelineLeads= leads.filter(l => l.column !== 'won' && l.column !== 'lost'); const pipelineValue= pipelineLeads.reduce((s,l) => s + l.value, 0); const revenue = wonLeads.reduce((s,l) => s + l.value, 0); const decided = wonLeads.length + lostLeads.length; const conversion = decided > 0 ? (wonLeads.length / decided) * 100 : 0; const goalTarget = S.monthlyGoalTarget || 1000000; const goalProgress = Math.min(100, Math.round((revenue / goalTarget) * 100)); const kpis = S.kpis.map(k => { if (k.id === 'leads') return { ...k, value: totalLeads }; if (k.id === 'pipeline') return { ...k, value: pipelineValue }; if (k.id === 'conversion') return { ...k, value: conversion }; if (k.id === 'revenue') return { ...k, value: revenue }; return k; }); // Leads by source, derived from live Kanban data const sourceColors = { 'Google':'#7C5CFF', 'Polecenia':'#4DA3FF', 'Social':'#00D9FF', 'Formularz':'#22C55E' }; const bySourceCount = {}; leads.forEach(l => { bySourceCount[l.source] = (bySourceCount[l.source]||0) + 1; }); const leadsBySource = Object.entries(bySourceCount).map(([name, count]) => ({ name: name === 'Google' ? 'Google Ads' : name === 'Social' ? 'Meta Ads' : name, value: totalLeads > 0 ? Math.round((count / totalLeads) * 100) : 0, color: sourceColors[name] || '#7C5CFF', })).sort((a,b) => b.value - a.value); return (
{/* KPIs */}
{kpis.map(k => )}
{/* Charts row */}
{/* Bottom row */}
); }; })();