// 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ł' && 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) => (
))}
);
}
// ── 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) => (
))}
);
}
// ── 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.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 */}
);
};
})();