refactor(ts): CalendarPage → TypeScript (money map + grid + cashflow + snowball glance)
This commit is contained in:
parent
82bd13036a
commit
dbf3c42d62
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState, type ComponentType, type ReactNode } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Banknote,
|
||||
|
|
@ -15,7 +15,8 @@ import {
|
|||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
import { cn, fmt, fmtDate, todayStr } from '@/lib/utils';
|
||||
import { cn, fmtDate, todayStr, errMessage } from '@/lib/utils';
|
||||
import { formatUSD, asDollars } from '@/lib/money';
|
||||
import { isPaidStatus } from '@/lib/trackerUtils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
|
@ -23,6 +24,123 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
|||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { CalendarFeedManager } from '@/components/CalendarFeedManager';
|
||||
|
||||
function fmt(v: number | null | undefined): string {
|
||||
return formatUSD(asDollars(Number(v) || 0));
|
||||
}
|
||||
|
||||
interface StatusSummary {
|
||||
paid_count: number;
|
||||
due_count: number;
|
||||
skipped_count: number;
|
||||
missed_count: number;
|
||||
total_due?: number;
|
||||
total_paid?: number;
|
||||
}
|
||||
|
||||
interface CalDayBill {
|
||||
bill_id: number;
|
||||
name: string;
|
||||
category_name?: string | null;
|
||||
status?: string;
|
||||
effective_amount?: number;
|
||||
paid_amount?: number;
|
||||
due_date?: string;
|
||||
}
|
||||
|
||||
interface CalDayPayment {
|
||||
payment_id: number;
|
||||
bill_name?: string;
|
||||
method?: string;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
interface CalDay {
|
||||
date: string;
|
||||
day: number;
|
||||
status_summary: StatusSummary;
|
||||
payments: CalDayPayment[];
|
||||
bills_due: CalDayBill[];
|
||||
}
|
||||
|
||||
interface SummaryInfo {
|
||||
bill_count?: number;
|
||||
paid_count?: number;
|
||||
skipped_count?: number;
|
||||
missed_count?: number;
|
||||
paid_total?: number;
|
||||
expected_total?: number;
|
||||
remaining_total?: number;
|
||||
paid_percent?: number;
|
||||
expense_total?: number;
|
||||
expense_count?: number;
|
||||
result?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Cashflow {
|
||||
has_data?: boolean;
|
||||
period?: string;
|
||||
period_projected?: number;
|
||||
month_projected?: number;
|
||||
uses_bank_balance?: boolean;
|
||||
period_end_label?: string;
|
||||
period_starting?: number;
|
||||
period_bills_total?: number;
|
||||
period_paid?: number;
|
||||
period_paid_count?: number;
|
||||
period_total_count?: number;
|
||||
month_starting?: number;
|
||||
month_bills_total?: number;
|
||||
month_paid?: number;
|
||||
month_paid_count?: number;
|
||||
month_total_count?: number;
|
||||
}
|
||||
|
||||
interface CalendarData {
|
||||
year: number;
|
||||
month: number;
|
||||
days: CalDay[];
|
||||
summary?: SummaryInfo;
|
||||
cashflow?: Cashflow;
|
||||
}
|
||||
|
||||
interface BankTracking {
|
||||
enabled?: boolean;
|
||||
effective_balance?: number;
|
||||
account_name?: string;
|
||||
balance?: number;
|
||||
pending_payments?: number;
|
||||
pending_days?: number;
|
||||
unpaid_this_month?: number;
|
||||
remaining?: number;
|
||||
last_updated?: string;
|
||||
}
|
||||
|
||||
interface SummaryDataShape {
|
||||
starting_amounts?: {
|
||||
first_amount?: number;
|
||||
fifteenth_amount?: number;
|
||||
other_amount?: number;
|
||||
combined_amount?: number;
|
||||
};
|
||||
summary?: SummaryInfo;
|
||||
bank_tracking?: BankTracking;
|
||||
income?: { amount?: number };
|
||||
}
|
||||
|
||||
interface SnowballDebt { name?: string; months?: number; payoff_display?: string }
|
||||
interface SnowballProjection {
|
||||
snowball?: {
|
||||
debts?: SnowballDebt[];
|
||||
months_to_freedom?: number;
|
||||
payoff_display?: string;
|
||||
total_interest_paid?: number;
|
||||
};
|
||||
comparison?: { months_saved?: number; interest_saved?: number };
|
||||
}
|
||||
|
||||
interface MoneyMarker { label: string; amount: number }
|
||||
|
||||
const MONTHS = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December',
|
||||
|
|
@ -34,26 +152,26 @@ function currentMonth() {
|
|||
return { year: now.getFullYear(), month: now.getMonth() + 1 };
|
||||
}
|
||||
|
||||
function shiftMonth(year, month, delta) {
|
||||
function shiftMonth(year: number, month: number, delta: number) {
|
||||
const next = new Date(year, month - 1 + delta, 1);
|
||||
return { year: next.getFullYear(), month: next.getMonth() + 1 };
|
||||
}
|
||||
|
||||
function displayStatus(status) {
|
||||
function displayStatus(status?: string): string {
|
||||
if (status === 'due_soon') return 'Due';
|
||||
if (status === 'late') return 'Late';
|
||||
return status ? status.charAt(0).toUpperCase() + status.slice(1) : 'Due';
|
||||
}
|
||||
|
||||
function statusTone(status) {
|
||||
if (isPaidStatus(status)) return 'border-emerald-500/30 bg-emerald-500/15 text-emerald-700 dark:text-emerald-300';
|
||||
function statusTone(status?: string): string {
|
||||
if (isPaidStatus(status || '')) return 'border-emerald-500/30 bg-emerald-500/15 text-emerald-700 dark:text-emerald-300';
|
||||
if (status === 'skipped') return 'border-border bg-muted/80 text-muted-foreground';
|
||||
if (status === 'late') return 'border-orange-400/60 bg-orange-500/25 text-orange-800 shadow-sm shadow-orange-950/10 dark:text-orange-100';
|
||||
if (status === 'missed') return 'border-rose-400/60 bg-rose-500/30 text-rose-800 shadow-sm shadow-rose-950/10 dark:text-rose-100';
|
||||
return 'border-primary/30 bg-primary/15 text-primary';
|
||||
}
|
||||
|
||||
function LegendItem({ className, label }) {
|
||||
function LegendItem({ className, label }: { className?: string; label: ReactNode }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-foreground/75">
|
||||
<span className={cn('h-2.5 w-2.5 rounded-full border', className)} />
|
||||
|
|
@ -62,7 +180,13 @@ function LegendItem({ className, label }) {
|
|||
);
|
||||
}
|
||||
|
||||
function MoneyMetric({ icon: Icon, label, value, hint, valueClassName }) {
|
||||
function MoneyMetric({ icon: Icon, label, value, hint, valueClassName }: {
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
label: ReactNode;
|
||||
value: number | null | undefined;
|
||||
hint?: ReactNode;
|
||||
valueClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-w-0 rounded-lg border border-border/70 bg-background/70 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -77,7 +201,7 @@ function MoneyMetric({ icon: Icon, label, value, hint, valueClassName }) {
|
|||
);
|
||||
}
|
||||
|
||||
function MoneyMap({ summaryData, loading }) {
|
||||
function MoneyMap({ summaryData, loading }: { summaryData: SummaryDataShape | null; loading: boolean }) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
|
|
@ -94,7 +218,7 @@ function MoneyMap({ summaryData, loading }) {
|
|||
const summary = summaryData?.summary || {};
|
||||
const bt = summaryData?.bank_tracking;
|
||||
const bankMode = bt?.enabled === true;
|
||||
const available = bankMode ? Number(bt.effective_balance || 0) : Number(starting.combined_amount || 0);
|
||||
const available = bankMode ? Number(bt?.effective_balance || 0) : Number(starting.combined_amount || 0);
|
||||
const assigned = Number(summary.expense_total || 0);
|
||||
const paid = Number(summary.paid_total || 0);
|
||||
const remaining = Number(summary.result || 0);
|
||||
|
|
@ -108,7 +232,7 @@ function MoneyMap({ summaryData, loading }) {
|
|||
<CardTitle className="text-base">Monthly Money Map</CardTitle>
|
||||
<CardDescription>
|
||||
{bankMode
|
||||
? `Live bank balance · ${bt.account_name}`
|
||||
? `Live bank balance · ${bt?.account_name}`
|
||||
: 'Available money, extra income, assigned bills, and what remains.'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
|
@ -126,15 +250,15 @@ function MoneyMap({ summaryData, loading }) {
|
|||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{bankMode ? (
|
||||
<>
|
||||
<MoneyMetric icon={Banknote} label="Bank Balance" value={Number(bt.balance || 0)} hint={`as of last sync`} />
|
||||
<MoneyMetric icon={PiggyBank} label="Pending" value={Number(bt.pending_payments || 0)} hint={`paid, not yet cleared (${bt.pending_days}d window)`} valueClassName="text-amber-600 dark:text-amber-400" />
|
||||
<MoneyMetric icon={CalendarDays} label="Unpaid Bills" value={Number(bt.unpaid_this_month || 0)} hint={`${summary.expense_count || 0} active bills`} />
|
||||
<MoneyMetric icon={Banknote} label="Bank Balance" value={Number(bt?.balance || 0)} hint={`as of last sync`} />
|
||||
<MoneyMetric icon={PiggyBank} label="Pending" value={Number(bt?.pending_payments || 0)} hint={`paid, not yet cleared (${bt?.pending_days}d window)`} valueClassName="text-amber-600 dark:text-amber-400" />
|
||||
<MoneyMetric icon={CalendarDays} label="Unpaid Bills" value={Number(bt?.unpaid_this_month || 0)} hint={`${summary.expense_count || 0} active bills`} />
|
||||
<MoneyMetric
|
||||
icon={CircleDollarSign}
|
||||
label="After Bills"
|
||||
value={Number(bt.remaining || 0)}
|
||||
value={Number(bt?.remaining || 0)}
|
||||
hint="effective balance − unpaid"
|
||||
valueClassName={Number(bt.remaining || 0) >= 0 ? 'text-emerald-600 dark:text-emerald-300' : 'text-destructive'}
|
||||
valueClassName={Number(bt?.remaining || 0) >= 0 ? 'text-emerald-600 dark:text-emerald-300' : 'text-destructive'}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -173,7 +297,7 @@ function MoneyMap({ summaryData, loading }) {
|
|||
{bankMode && (
|
||||
<div className={cn(
|
||||
'flex items-center justify-between gap-3 rounded-xl border px-4 py-3',
|
||||
Number(bt.remaining || 0) >= 0
|
||||
Number(bt?.remaining || 0) >= 0
|
||||
? 'border-emerald-500/25 bg-emerald-500/5'
|
||||
: 'border-destructive/25 bg-destructive/5',
|
||||
)}>
|
||||
|
|
@ -182,21 +306,21 @@ function MoneyMap({ summaryData, loading }) {
|
|||
Projected Month-End Balance
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground/70">
|
||||
{fmt(bt.balance || 0)} bank
|
||||
{Number(bt.pending_payments || 0) > 0 && ` − ${fmt(bt.pending_payments)} pending`}
|
||||
{` − ${fmt(bt.unpaid_this_month || 0)} remaining bills`}
|
||||
{fmt(bt?.balance || 0)} bank
|
||||
{Number(bt?.pending_payments || 0) > 0 && ` − ${fmt(bt?.pending_payments)} pending`}
|
||||
{` − ${fmt(bt?.unpaid_this_month || 0)} remaining bills`}
|
||||
</p>
|
||||
</div>
|
||||
<p className={cn(
|
||||
'tracker-number text-2xl font-bold tabular-nums shrink-0',
|
||||
Number(bt.remaining || 0) >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive',
|
||||
Number(bt?.remaining || 0) >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive',
|
||||
)}>
|
||||
{Number(bt.remaining || 0) < 0 ? '−' : ''}{fmt(Math.abs(Number(bt.remaining || 0)))}
|
||||
{Number(bt?.remaining || 0) < 0 ? '−' : ''}{fmt(Math.abs(Number(bt?.remaining || 0)))}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bankMode && bt.last_updated && (
|
||||
{bankMode && bt?.last_updated && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Balance last updated: {new Date(bt.last_updated).toLocaleString()}
|
||||
</p>
|
||||
|
|
@ -206,7 +330,7 @@ function MoneyMap({ summaryData, loading }) {
|
|||
);
|
||||
}
|
||||
|
||||
function SummaryProgress({ summary }) {
|
||||
function SummaryProgress({ summary }: { summary?: SummaryInfo }) {
|
||||
const percent = Number(summary?.paid_percent || 0);
|
||||
|
||||
return (
|
||||
|
|
@ -252,7 +376,7 @@ function SummaryProgress({ summary }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DayIndicators({ day, moneyMarker }) {
|
||||
function DayIndicators({ day, moneyMarker }: { day: CalDay; moneyMarker: MoneyMarker | null }) {
|
||||
const summary = day.status_summary;
|
||||
const hasPaid = summary.paid_count > 0;
|
||||
const hasDue = summary.due_count > summary.paid_count + summary.skipped_count + summary.missed_count;
|
||||
|
|
@ -271,11 +395,16 @@ function DayIndicators({ day, moneyMarker }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CalendarGrid({ data, selectedDate, onSelectDay, moneyMarkers }) {
|
||||
function CalendarGrid({ data, selectedDate, onSelectDay, moneyMarkers }: {
|
||||
data: CalendarData;
|
||||
selectedDate?: string;
|
||||
onSelectDay: (day: CalDay) => void;
|
||||
moneyMarkers?: Record<string, MoneyMarker>;
|
||||
}) {
|
||||
const firstWeekday = new Date(data.year, data.month - 1, 1).getDay();
|
||||
const cells = [
|
||||
...Array.from({ length: firstWeekday }, (_, index) => ({ type: 'blank', key: `blank-${index}` })),
|
||||
...data.days.map(day => ({ type: 'day', key: day.date, day })),
|
||||
const cells: ({ type: 'blank'; key: string } | { type: 'day'; key: string; day: CalDay })[] = [
|
||||
...Array.from({ length: firstWeekday }, (_, index) => ({ type: 'blank' as const, key: `blank-${index}` })),
|
||||
...data.days.map(day => ({ type: 'day' as const, key: day.date, day })),
|
||||
];
|
||||
const today = todayStr();
|
||||
|
||||
|
|
@ -363,11 +492,24 @@ function CalendarGrid({ data, selectedDate, onSelectDay, moneyMarkers }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ProjectionPanel({ label, projected, starting, billsTotal, paid, paidCount, totalCount, period, year, month }) {
|
||||
function ProjectionPanel({ label, projected, starting, billsTotal, paid, paidCount, totalCount, period, year, month }: {
|
||||
label: ReactNode;
|
||||
projected: number;
|
||||
starting?: number;
|
||||
billsTotal?: number;
|
||||
paid?: number;
|
||||
paidCount?: number;
|
||||
totalCount?: number;
|
||||
period?: string;
|
||||
year: number;
|
||||
month: number;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const billsTotalN = Number(billsTotal || 0);
|
||||
const paidN = Number(paid || 0);
|
||||
const isNegative = projected < 0;
|
||||
const amountPct = billsTotal > 0 ? Math.min(100, Math.round((paid / billsTotal) * 100)) : 0;
|
||||
const unpaidCount = totalCount - paidCount;
|
||||
const amountPct = billsTotalN > 0 ? Math.min(100, Math.round((paidN / billsTotalN) * 100)) : 0;
|
||||
const unpaidCount = Number(totalCount || 0) - Number(paidCount || 0);
|
||||
const bucketParam = period === '1st' ? 'b1=1' : 'b2=1';
|
||||
|
||||
function goToUnpaid() {
|
||||
|
|
@ -393,7 +535,7 @@ function ProjectionPanel({ label, projected, starting, billsTotal, paid, paidCou
|
|||
<div className="space-y-1.5">
|
||||
{/* Amount-based progress bar */}
|
||||
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
|
||||
<span>{fmt(paid)} of {fmt(billsTotal)} paid</span>
|
||||
<span>{fmt(paidN)} of {fmt(billsTotalN)} paid</span>
|
||||
{unpaidCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -404,7 +546,7 @@ function ProjectionPanel({ label, projected, starting, billsTotal, paid, paidCou
|
|||
{unpaidCount} unpaid →
|
||||
</button>
|
||||
)}
|
||||
{unpaidCount === 0 && totalCount > 0 && (
|
||||
{unpaidCount === 0 && Number(totalCount || 0) > 0 && (
|
||||
<span className="text-emerald-600 dark:text-emerald-400">All paid ✓</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -418,14 +560,14 @@ function ProjectionPanel({ label, projected, starting, billsTotal, paid, paidCou
|
|||
/>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
{fmt(starting)} starting · {fmt(billsTotal)} due
|
||||
{fmt(starting)} starting · {fmt(billsTotalN)} due
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CashFlowCard({ cashflow, year, month }) {
|
||||
function CashFlowCard({ cashflow, year, month }: { cashflow?: Cashflow; year: number; month: number }) {
|
||||
if (!cashflow?.has_data) return null;
|
||||
|
||||
const periodProjected = Number(cashflow.period_projected ?? 0);
|
||||
|
|
@ -503,7 +645,7 @@ function CashFlowCard({ cashflow, year, month }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DebtPayoffGlance({ projection }) {
|
||||
function DebtPayoffGlance({ projection }: { projection: SnowballProjection | null }) {
|
||||
const snowball = projection?.snowball;
|
||||
const comparison = projection?.comparison;
|
||||
const targetDebt = snowball?.debts?.[0] || null;
|
||||
|
|
@ -626,7 +768,12 @@ function DebtPayoffGlance({ projection }) {
|
|||
);
|
||||
}
|
||||
|
||||
function DayDetailDialog({ day, open, onOpenChange, moneyMarker }) {
|
||||
function DayDetailDialog({ day, open, onOpenChange, moneyMarker }: {
|
||||
day: CalDay | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
moneyMarker: MoneyMarker | null;
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg border-border/60 bg-card/95 backdrop-blur-xl">
|
||||
|
|
@ -730,7 +877,7 @@ function DayDetailDialog({ day, open, onOpenChange, moneyMarker }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CalendarSubscribeDialog({ open, onOpenChange }) {
|
||||
function CalendarSubscribeDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="border-border/60 bg-card/95 backdrop-blur-xl sm:max-w-3xl">
|
||||
|
|
@ -758,12 +905,12 @@ export default function CalendarPage() {
|
|||
const initial = currentMonth();
|
||||
const [year, setYear] = useState(initial.year);
|
||||
const [month, setMonth] = useState(initial.month);
|
||||
const [data, setData] = useState(null);
|
||||
const [summaryData, setSummaryData] = useState(null);
|
||||
const [snowballProjection, setSnowballProjection] = useState(null);
|
||||
const [data, setData] = useState<CalendarData | null>(null);
|
||||
const [summaryData, setSummaryData] = useState<SummaryDataShape | null>(null);
|
||||
const [snowballProjection, setSnowballProjection] = useState<SnowballProjection | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [selectedDay, setSelectedDay] = useState(null);
|
||||
const [selectedDay, setSelectedDay] = useState<CalDay | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [calendarFeedOpen, setCalendarFeedOpen] = useState(false);
|
||||
|
||||
|
|
@ -781,14 +928,14 @@ export default function CalendarPage() {
|
|||
throw calendarResult.reason;
|
||||
}
|
||||
|
||||
const result = calendarResult.value;
|
||||
const result = calendarResult.value as CalendarData;
|
||||
setData(result);
|
||||
setSummaryData(summaryResult.status === 'fulfilled' ? summaryResult.value : null);
|
||||
setSnowballProjection(snowballResult.status === 'fulfilled' ? snowballResult.value : null);
|
||||
setSummaryData(summaryResult.status === 'fulfilled' ? summaryResult.value as SummaryDataShape : null);
|
||||
setSnowballProjection(snowballResult.status === 'fulfilled' ? snowballResult.value as SnowballProjection : null);
|
||||
setSelectedDay(current => current ? result.days.find(day => day.date === current.date) || null : null);
|
||||
} catch (err) {
|
||||
setError(err.message || 'Calendar data could not be loaded.');
|
||||
toast.error(err.message || 'Calendar data could not be loaded.');
|
||||
setError(errMessage(err, 'Calendar data could not be loaded.'));
|
||||
toast.error(errMessage(err, 'Calendar data could not be loaded.'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -798,11 +945,11 @@ export default function CalendarPage() {
|
|||
|
||||
const monthLabel = useMemo(() => `${MONTHS[month - 1]} ${year}`, [year, month]);
|
||||
const hasAnyBills = Number(data?.summary?.bill_count || 0) + Number(data?.summary?.skipped_count || 0) > 0;
|
||||
const moneyMarkers = useMemo(() => {
|
||||
const moneyMarkers = useMemo<Record<string, MoneyMarker>>(() => {
|
||||
const starting = summaryData?.starting_amounts;
|
||||
if (!starting) return {};
|
||||
|
||||
const markers = {};
|
||||
const markers: Record<string, MoneyMarker> = {};
|
||||
const firstAmount = Number(starting.first_amount || 0);
|
||||
const fifteenthAmount = Number(starting.fifteenth_amount || 0);
|
||||
|
||||
|
|
@ -823,7 +970,7 @@ export default function CalendarPage() {
|
|||
}, [month, summaryData, year]);
|
||||
const selectedMoneyMarker = selectedDay ? moneyMarkers[selectedDay.date] || null : null;
|
||||
|
||||
function navigate(delta) {
|
||||
function navigate(delta: number) {
|
||||
const next = shiftMonth(year, month, delta);
|
||||
setYear(next.year);
|
||||
setMonth(next.month);
|
||||
Loading…
Reference in New Issue