updated some UI elements and calculations

This commit is contained in:
Deven Thiel 2026-03-05 22:35:34 -05:00
parent 409d2560bc
commit 55d7c736c9
13 changed files with 378 additions and 121 deletions

View file

@ -14,7 +14,7 @@ import { ChartSidebar } from '@/components/charts/ChartSidebar';
import { ResizableSplit } from '@/components/layout/ResizableSplit';
import { Modal, ConfirmDialog } from '@/components/common/Modal';
import { buildHierarchyForRange, buildHierarchy, aggregate } from '@/lib/stats/aggregate';
import { fmtMoney, todayISO } from '@/lib/format';
import { fmtMoney, todayISO, nowInTZ } from '@/lib/format';
function yearStart() { return `${new Date().getFullYear()}-01-01`; }
@ -148,6 +148,10 @@ function WorkTab({ startDate, setStartDate }: { startDate: string; setStartDate:
onView={(n) => setEditing(n.entry as WorkEntry)}
onEdit={(n) => setEditing(n.entry as WorkEntry)}
onDelete={(n) => setDeleting(n.entry!.id)}
onToggleOutstanding={(n) => {
const e = n.entry as WorkEntry;
updateWorkEntry(e.id, { paymentOutstanding: !e.paymentOutstanding });
}}
/>
</div>
@ -606,13 +610,19 @@ const LEDGER_TILE_LABELS: Record<LedgerTile, string> = {
avgMonth: 'Avg / month',
yearProj: 'Year projected',
thisMonth: 'This month',
avgDay: 'Avg / day',
avgDay: 'YTD daily avg',
monthProj: 'Month projected',
today: 'Today',
};
const ALL_LEDGER_TILES: LedgerTile[] = ['ytd', 'avgMonth', 'yearProj', 'thisMonth', 'avgDay', 'monthProj', 'today'];
const METRIC_COLOR: Record<'workValue' | 'payments' | 'expenses', string> = {
workValue: 'positive',
payments: 'positive',
expenses: 'negative',
};
function PeriodSummaryRow({
stats,
metric,
@ -626,10 +636,8 @@ function PeriodSummaryRow({
tiles: LedgerTile[];
onConfigure: () => void;
}) {
const now = new Date();
const currentYear = String(now.getFullYear());
const currentMonth = now.toISOString().slice(0, 7);
const today = now.toISOString().slice(0, 10);
const { year: nowYear, monthIdx, day: nowDay, isoDate: today, isoMonth: currentMonth } = nowInTZ();
const currentYear = String(nowYear);
const y = stats.years.find((x) => x.label === currentYear);
const m = stats.months.get(currentMonth);
@ -638,19 +646,23 @@ function PeriodSummaryRow({
const yValue = y?.[metric] ?? 0;
const mValue = m?.[metric] ?? 0;
const monthsElapsed = now.getMonth() + 1;
const dayOfMonth = now.getDate();
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
const yearStart = new Date(now.getFullYear(), 0, 1).getTime();
const yearEnd = new Date(now.getFullYear() + 1, 0, 1).getTime();
const yearFrac = (now.getTime() - yearStart) / (yearEnd - yearStart);
const monthsElapsed = monthIdx + 1;
const dayOfMonth = nowDay;
const daysInMonth = new Date(nowYear, monthIdx + 1, 0).getDate();
// Days elapsed since Jan 1 inclusive, using noon-UTC to avoid DST drift
const jan1Noon = Date.parse(`${nowYear}-01-01T12:00:00Z`);
const todayNoon = Date.parse(`${today}T12:00:00Z`);
const daysElapsed = Math.max(1, Math.floor((todayNoon - jan1Noon) / 86400000) + 1);
const daysInYear = (nowYear % 4 === 0 && (nowYear % 100 !== 0 || nowYear % 400 === 0)) ? 366 : 365;
const ytdDailyAvg = yValue / daysElapsed;
const tileValues: Record<LedgerTile, number | null> = {
ytd: yValue,
avgMonth: yValue > 0 ? yValue / monthsElapsed : null,
yearProj: yValue > 0 && yearFrac > 0 && yearFrac < 1 ? yValue / yearFrac : null,
yearProj: yValue > 0 ? ytdDailyAvg * daysInYear : null,
thisMonth: mValue,
avgDay: mValue > 0 ? mValue / dayOfMonth : null,
avgDay: yValue > 0 ? ytdDailyAvg : null,
monthProj: mValue > 0 && dayOfMonth > 0 && dayOfMonth < daysInMonth
? (mValue / dayOfMonth) * daysInMonth
: null,
@ -671,16 +683,16 @@ function PeriodSummaryRow({
{tiles.map((t) => {
const value = tileValues[t];
if (value == null) return null;
return <StatTile key={t} label={tileDisplayLabel[t]} value={value} />;
return <StatTile key={t} label={tileDisplayLabel[t]} value={value} className={METRIC_COLOR[metric]} />;
})}
</div>
</div>
);
}
function StatTile({ label, value }: { label: string; value: number }) {
function StatTile({ label, value, className = '' }: { label: string; value: number; className?: string }) {
return (
<div className="stat-card">
<div className={`stat-card ${className}`}>
<div className="stat-label">{label}</div>
<div className="stat-value">{fmtMoney(value)}</div>
</div>