Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions src/components/KpiTiles.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
export default function KpiTiles({ kpis = {}, windowDays = 7 }) {
export default function KpiTiles({ kpis = {}, previousKpis = {}, windowDays = 7 }) {
const suffix = `(${windowDays}d)`
const accText = typeof kpis.acc === 'number' ? `${kpis.acc}%` : '0%'
const volText = typeof kpis.volume === 'number' ? kpis.volume : 0
const bestText = kpis.bestZone ? `${toZoneAbbr(kpis.bestZone.label)} - ${kpis.bestZone.acc}%` : '-'
const hasPrevBaseline = Number(previousKpis.volume || 0) > 0

const accDelta = toDelta(kpis.acc, previousKpis.acc, '%', hasPrevBaseline)
const volDelta = toDelta(kpis.volume, previousKpis.volume, '', hasPrevBaseline)
const bestDelta = toBestZoneDelta(kpis.bestZone, previousKpis.bestZone, hasPrevBaseline)

return (
<div className="grid grid-cols-3 gap-1.5">
<KpiCard title={`Accuracy ${suffix}`} value={accText} />
<KpiCard title={`Volume ${suffix}`} value={volText} />
<KpiCard title="Best Zone" value={bestText} />
<KpiCard title={`Accuracy ${suffix}`} value={accText} delta={accDelta} />
<KpiCard title={`Volume ${suffix}`} value={volText} delta={volDelta} />
<KpiCard title="Best Zone" value={bestText} delta={bestDelta} />
</div>
)
}
Expand All @@ -30,11 +35,36 @@ function toZoneAbbr(label) {
}
}

function KpiCard({ title, value }) {
function toDelta(current, previous, suffix = '', hasPrevBaseline = true) {
if (!hasPrevBaseline || typeof current !== 'number' || typeof previous !== 'number') {
return { text: 'vs prev n/a', tone: 'text-neutral-500' }
}
const diff = current - previous
if (diff === 0) return { text: 'vs prev 0', tone: 'text-neutral-500' }
const sign = diff > 0 ? '+' : ''
return {
text: `vs prev ${sign}${diff}${suffix}`,
tone: diff > 0 ? 'text-emerald-500' : 'text-rose-500',
}
}

function toBestZoneDelta(current, previous, hasPrevBaseline = true) {
if (!hasPrevBaseline || !previous) return { text: 'vs prev n/a', tone: 'text-neutral-500' }
if (!current) return { text: `prev ${toZoneAbbr(previous.label)} ${previous.acc}%`, tone: 'text-neutral-500' }
if (current.key !== previous.key) {
return { text: `prev ${toZoneAbbr(previous.label)} ${previous.acc}%`, tone: 'text-neutral-500' }
}
return toDelta(current.acc, previous.acc, 'pp')
}

function KpiCard({ title, value, delta }) {
return (
<div className="border rounded-xl p-2 md:p-3 min-w-0">
<div className="text-[10px] md:text-xs uppercase tracking-wide opacity-70 truncate">{title}</div>
<div className="text-base md:text-xl font-bold mt-0.5 leading-tight truncate">{value}</div>
<div className={`text-[10px] md:text-xs mt-0.5 truncate ${delta?.tone || 'text-neutral-500'}`}>
{delta?.text || 'vs prev n/a'}
</div>
</div>
)
}
26 changes: 25 additions & 1 deletion src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,18 +213,41 @@ export default function Dashboard() {
() =>
filterSessions(rows, {
from: filters.dateFrom,
to: addDays(filters.dateTo, 1),
to: filters.dateTo,
types: filters.types && filters.types.length ? filters.types : undefined,
}),
[rows, filters.dateFrom, filters.dateTo, filters.types]
)

const periodDays = useMemo(() => {
const fromTs = new Date(`${filters.dateFrom}T00:00:00Z`).getTime()
const toTs = new Date(`${filters.dateTo}T00:00:00Z`).getTime()
if (Number.isFinite(fromTs) && Number.isFinite(toTs) && toTs >= fromTs) {
return Math.floor((toTs - fromTs) / 86400000) + 1
}
return Number(filters.windowDays || 90)
}, [filters.dateFrom, filters.dateTo, filters.windowDays])

const previousTo = useMemo(() => addDays(filters.dateFrom, -1), [filters.dateFrom])
const previousFrom = useMemo(() => addDays(filters.dateFrom, -periodDays), [filters.dateFrom, periodDays])

const previousFiltered = useMemo(
() =>
filterSessions(rows, {
from: previousFrom,
to: previousTo,
types: filters.types && filters.types.length ? filters.types : undefined,
}),
[rows, previousFrom, previousTo, filters.types]
)

const aggOpts = useMemo(
() => ({ direction: normalizeDir(filters.direction), range: filters.range }),
[filters.direction, filters.range]
)

const kpis = useMemo(() => computeKpis(filtered, aggOpts), [filtered, aggOpts])
const previousKpis = useMemo(() => computeKpis(previousFiltered, aggOpts), [previousFiltered, aggOpts])
const byPos = useMemo(() => aggregateByPosition(filtered, aggOpts), [filtered, aggOpts])
const trend = useMemo(() => aggregateAccuracyByDate(filtered, aggOpts), [filtered, aggOpts])
const byType = useMemo(() => aggregateByType(filtered, aggOpts), [filtered, aggOpts])
Expand Down Expand Up @@ -285,6 +308,7 @@ export default function Dashboard() {
filters={filters}
setFilters={setFilters}
kpis={kpis}
previousKpis={previousKpis}
zonesForUi={zonesForUi}
trend={trend}
byType={byType}
Expand Down
3 changes: 2 additions & 1 deletion src/pages/dashboard/AnalyticsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default function AnalyticsSection({
filters,
setFilters,
kpis,
previousKpis,
zonesForUi,
trend,
byType,
Expand Down Expand Up @@ -54,7 +55,7 @@ export default function AnalyticsSection({
))}
</div>
) : (
<KpiTiles kpis={kpis} windowDays={filters.windowDays || 7} />
<KpiTiles kpis={kpis} previousKpis={previousKpis} windowDays={filters.windowDays || 90} />
)}
</div>
</div>
Expand Down