refactor(ts): SummaryPage → TypeScript (React Query summary + drag-reorder expenses)
This commit is contained in:
parent
c5d2aeda29
commit
8c9702adbe
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState, type DragEvent, type DragEventHandler } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useSummary } from '@/hooks/useQueries';
|
import { useSummary } from '@/hooks/useQueries';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
@ -22,22 +22,87 @@ import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { cn, fmt } from '@/lib/utils';
|
import { cn, errMessage } from '@/lib/utils';
|
||||||
|
import { formatUSD, asDollars } from '@/lib/money';
|
||||||
import { moveInArray, reorderPayload } from '@/lib/reorder';
|
import { moveInArray, reorderPayload } from '@/lib/reorder';
|
||||||
|
|
||||||
|
function fmt(v: number | null | undefined): string {
|
||||||
|
return formatUSD(asDollars(Number(v) || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummaryExpense {
|
||||||
|
bill_id: number;
|
||||||
|
name: string;
|
||||||
|
category_name?: string | null;
|
||||||
|
due_day?: number | null;
|
||||||
|
autopay_enabled?: number;
|
||||||
|
is_subscription?: number;
|
||||||
|
has_merchant_rule?: unknown;
|
||||||
|
has_linked_transactions?: unknown;
|
||||||
|
is_paid?: boolean;
|
||||||
|
is_skipped?: boolean;
|
||||||
|
actual_amount?: number | null;
|
||||||
|
display_amount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummaryChartRow {
|
||||||
|
type: string;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StartingAmounts {
|
||||||
|
first_amount?: number;
|
||||||
|
fifteenth_amount?: number;
|
||||||
|
other_amount?: number;
|
||||||
|
combined_amount?: number;
|
||||||
|
paid_total?: number;
|
||||||
|
combined_remaining?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummarySummary {
|
||||||
|
expense_total?: number;
|
||||||
|
result?: number;
|
||||||
|
paid_expense_count?: number;
|
||||||
|
expense_count?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SummaryResponse {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
summary?: SummarySummary;
|
||||||
|
expenses?: SummaryExpense[];
|
||||||
|
starting_amounts?: StartingAmounts;
|
||||||
|
income?: { amount?: number; label?: string };
|
||||||
|
chart?: SummaryChartRow[];
|
||||||
|
generated_at?: string;
|
||||||
|
previous_month?: { combined_remaining?: number };
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExpenseMoveControls {
|
||||||
|
enabled: boolean;
|
||||||
|
moving: boolean;
|
||||||
|
canMoveUp: boolean;
|
||||||
|
canMoveDown: boolean;
|
||||||
|
onMoveUp: () => void;
|
||||||
|
onMoveDown: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExpenseDragProps {
|
||||||
|
draggable: boolean;
|
||||||
|
isDragging?: boolean;
|
||||||
|
isDropTarget?: boolean;
|
||||||
|
onDragStart?: DragEventHandler<HTMLDivElement>;
|
||||||
|
onDragEnter?: DragEventHandler<HTMLDivElement>;
|
||||||
|
onDragOver?: DragEventHandler<HTMLDivElement>;
|
||||||
|
onDragEnd?: DragEventHandler<HTMLDivElement>;
|
||||||
|
onDrop?: DragEventHandler<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
const MONTHS = [
|
const MONTHS = [
|
||||||
'January',
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
'February',
|
'July', 'August', 'September', 'October', 'November', 'December',
|
||||||
'March',
|
|
||||||
'April',
|
|
||||||
'May',
|
|
||||||
'June',
|
|
||||||
'July',
|
|
||||||
'August',
|
|
||||||
'September',
|
|
||||||
'October',
|
|
||||||
'November',
|
|
||||||
'December',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function selectedFromToday() {
|
function selectedFromToday() {
|
||||||
|
|
@ -45,20 +110,20 @@ function selectedFromToday() {
|
||||||
return { year: now.getFullYear(), month: now.getMonth() + 1 };
|
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);
|
const next = new Date(year, month - 1 + delta, 1);
|
||||||
return { year: next.getFullYear(), month: next.getMonth() + 1 };
|
return { year: next.getFullYear(), month: next.getMonth() + 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
function monthLabel(year, month) {
|
function monthLabel(year: number, month: number): string {
|
||||||
return `${MONTHS[month - 1]} ${year}`;
|
return `${MONTHS[month - 1]} ${year}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function moneyClass(value) {
|
function moneyClass(value: number): string {
|
||||||
return value >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive';
|
return value >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-destructive';
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusMark({ expense }) {
|
function StatusMark({ expense }: { expense: SummaryExpense }) {
|
||||||
if (expense.is_skipped) {
|
if (expense.is_skipped) {
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex min-w-16 items-center justify-center rounded-full bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
|
<span className="inline-flex min-w-16 items-center justify-center rounded-full bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
|
||||||
|
|
@ -84,7 +149,7 @@ function StatusMark({ expense }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SummaryChart({ rows = [], onStartingClick }) {
|
function SummaryChart({ rows = [], onStartingClick }: { rows?: SummaryChartRow[]; onStartingClick?: () => void }) {
|
||||||
const max = Math.max(1, ...rows.map(row => Math.abs(Number(row.amount) || 0)));
|
const max = Math.max(1, ...rows.map(row => Math.abs(Number(row.amount) || 0)));
|
||||||
const chartRows = rows.map((row, index) => ({
|
const chartRows = rows.map((row, index) => ({
|
||||||
...row,
|
...row,
|
||||||
|
|
@ -138,7 +203,11 @@ function SummaryChart({ rows = [], onStartingClick }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExpenseRow({ expense, moveControls, dragProps }) {
|
function ExpenseRow({ expense, moveControls, dragProps }: {
|
||||||
|
expense: SummaryExpense;
|
||||||
|
moveControls?: ExpenseMoveControls;
|
||||||
|
dragProps?: ExpenseDragProps;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
draggable={dragProps?.draggable}
|
draggable={dragProps?.draggable}
|
||||||
|
|
@ -189,7 +258,7 @@ function ExpenseRow({ expense, moveControls, dragProps }) {
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
<span className="truncate text-sm font-semibold text-foreground">{expense.name}</span>
|
<span className="truncate text-sm font-semibold text-foreground">{expense.name}</span>
|
||||||
<TooltipProvider delayDuration={300}>
|
<TooltipProvider delayDuration={300}>
|
||||||
{expense.autopay_enabled && (
|
{expense.autopay_enabled ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="inline-flex shrink-0 rounded border border-sky-500/25 bg-sky-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-sky-600 dark:text-sky-300 cursor-default">
|
<span className="inline-flex shrink-0 rounded border border-sky-500/25 bg-sky-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-sky-600 dark:text-sky-300 cursor-default">
|
||||||
|
|
@ -198,8 +267,8 @@ function ExpenseRow({ expense, moveControls, dragProps }) {
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Autopay enabled</TooltipContent>
|
<TooltipContent>Autopay enabled</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
) : null}
|
||||||
{expense.is_subscription && (
|
{expense.is_subscription ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="inline-flex shrink-0 rounded border border-indigo-500/25 bg-indigo-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-indigo-600 dark:text-indigo-300 cursor-default">
|
<span className="inline-flex shrink-0 rounded border border-indigo-500/25 bg-indigo-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-indigo-600 dark:text-indigo-300 cursor-default">
|
||||||
|
|
@ -208,8 +277,8 @@ function ExpenseRow({ expense, moveControls, dragProps }) {
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Subscription</TooltipContent>
|
<TooltipContent>Subscription</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
) : null}
|
||||||
{(expense.has_merchant_rule || expense.has_linked_transactions) && (
|
{(expense.has_merchant_rule || expense.has_linked_transactions) ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span className="inline-flex shrink-0 rounded border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-emerald-600 dark:text-emerald-400 cursor-default">
|
<span className="inline-flex shrink-0 rounded border border-emerald-500/25 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-bold leading-none text-emerald-600 dark:text-emerald-400 cursor-default">
|
||||||
|
|
@ -218,7 +287,7 @@ function ExpenseRow({ expense, moveControls, dragProps }) {
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Linked to bank transactions</TooltipContent>
|
<TooltipContent>Linked to bank transactions</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
) : null}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
|
@ -238,9 +307,13 @@ function ExpenseRow({ expense, moveControls, dragProps }) {
|
||||||
export default function SummaryPage() {
|
export default function SummaryPage() {
|
||||||
const [selected, setSelected] = useState(selectedFromToday);
|
const [selected, setSelected] = useState(selectedFromToday);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data = null, isPending: loading, error: queryError } = useSummary(selected.year, selected.month);
|
const { data: dataRaw, isPending: loading, error: queryError } = useSummary(selected.year, selected.month);
|
||||||
const setData = useCallback((u) => queryClient.setQueryData(['summary', selected.year, selected.month],
|
const data = (dataRaw ?? null) as SummaryResponse | null;
|
||||||
prev => (typeof u === 'function' ? u(prev) : u)), [queryClient, selected.year, selected.month]);
|
const setData = useCallback(
|
||||||
|
(updater: (prev: SummaryResponse | null | undefined) => SummaryResponse | null | undefined) =>
|
||||||
|
queryClient.setQueryData<SummaryResponse | null>(['summary', selected.year, selected.month], updater),
|
||||||
|
[queryClient, selected.year, selected.month],
|
||||||
|
);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const error = queryError ? (queryError.message || 'Summary could not be loaded.') : '';
|
const error = queryError ? (queryError.message || 'Summary could not be loaded.') : '';
|
||||||
const [startingFirst, setStartingFirst] = useState('0');
|
const [startingFirst, setStartingFirst] = useState('0');
|
||||||
|
|
@ -250,9 +323,9 @@ export default function SummaryPage() {
|
||||||
const [incomeAmount, setIncomeAmount] = useState('0');
|
const [incomeAmount, setIncomeAmount] = useState('0');
|
||||||
const [incomeLabel, setIncomeLabel] = useState('Salary');
|
const [incomeLabel, setIncomeLabel] = useState('Salary');
|
||||||
const [editingIncome, setEditingIncome] = useState(false);
|
const [editingIncome, setEditingIncome] = useState(false);
|
||||||
const [draggingId, setDraggingId] = useState(null);
|
const [draggingId, setDraggingId] = useState<number | null>(null);
|
||||||
const [dropTargetId, setDropTargetId] = useState(null);
|
const [dropTargetId, setDropTargetId] = useState<number | null>(null);
|
||||||
const [movingBillId, setMovingBillId] = useState(null);
|
const [movingBillId, setMovingBillId] = useState<number | null>(null);
|
||||||
|
|
||||||
const loadSummary = useCallback(
|
const loadSummary = useCallback(
|
||||||
() => queryClient.invalidateQueries({ queryKey: ['summary', selected.year, selected.month] }),
|
() => queryClient.invalidateQueries({ queryKey: ['summary', selected.year, selected.month] }),
|
||||||
|
|
@ -260,8 +333,7 @@ export default function SummaryPage() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Seed the editable form fields (starting amounts, income) from the loaded
|
// Seed the editable form fields (starting amounts, income) from the loaded
|
||||||
// summary and reset transient edit/drag state whenever the data changes —
|
// summary and reset transient edit/drag state whenever the data changes.
|
||||||
// this is what loadSummary used to do inline.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
setStartingFirst(String(data.starting_amounts?.first_amount ?? 0));
|
setStartingFirst(String(data.starting_amounts?.first_amount ?? 0));
|
||||||
|
|
@ -307,7 +379,7 @@ export default function SummaryPage() {
|
||||||
toast.success('Starting amounts saved.');
|
toast.success('Starting amounts saved.');
|
||||||
await loadSummary();
|
await loadSummary();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'Starting amounts could not be saved.');
|
toast.error(errMessage(err, 'Starting amounts could not be saved.'));
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|
@ -327,13 +399,13 @@ export default function SummaryPage() {
|
||||||
setEditingIncome(false);
|
setEditingIncome(false);
|
||||||
await loadSummary();
|
await loadSummary();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'Income could not be saved.');
|
toast.error(errMessage(err, 'Income could not be saved.'));
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveMonth(delta) {
|
function moveMonth(delta: number) {
|
||||||
setSelected(current => shiftMonth(current.year, current.month, delta));
|
setSelected(current => shiftMonth(current.year, current.month, delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,7 +413,7 @@ export default function SummaryPage() {
|
||||||
setSelected(selectedFromToday());
|
setSelected(selectedFromToday());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persistExpenseOrder(nextExpenses, movedId) {
|
async function persistExpenseOrder(nextExpenses: SummaryExpense[], movedId: number | null) {
|
||||||
setData(prev => prev ? { ...prev, expenses: nextExpenses } : prev);
|
setData(prev => prev ? { ...prev, expenses: nextExpenses } : prev);
|
||||||
setMovingBillId(movedId);
|
setMovingBillId(movedId);
|
||||||
try {
|
try {
|
||||||
|
|
@ -349,20 +421,20 @@ export default function SummaryPage() {
|
||||||
toast.success('Summary order saved');
|
toast.success('Summary order saved');
|
||||||
await loadSummary();
|
await loadSummary();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'Failed to save summary order.');
|
toast.error(errMessage(err, 'Failed to save summary order.'));
|
||||||
await loadSummary();
|
await loadSummary();
|
||||||
} finally {
|
} finally {
|
||||||
setMovingBillId(null);
|
setMovingBillId(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorderExpenses(fromIndex, toIndex) {
|
function reorderExpenses(fromIndex: number, toIndex: number) {
|
||||||
if (!reorderEnabled || fromIndex === toIndex) return;
|
if (!reorderEnabled || fromIndex === toIndex) return;
|
||||||
const nextExpenses = moveInArray(expenses, fromIndex, toIndex);
|
const nextExpenses = moveInArray(expenses, fromIndex, toIndex);
|
||||||
persistExpenseOrder(nextExpenses, expenses[fromIndex]?.bill_id || null);
|
persistExpenseOrder(nextExpenses, expenses[fromIndex]?.bill_id || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveControlsFor(expense, index) {
|
function moveControlsFor(expense: SummaryExpense, index: number): ExpenseMoveControls {
|
||||||
return {
|
return {
|
||||||
enabled: reorderEnabled,
|
enabled: reorderEnabled,
|
||||||
moving: movingBillId === expense.bill_id,
|
moving: movingBillId === expense.bill_id,
|
||||||
|
|
@ -373,13 +445,13 @@ export default function SummaryPage() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragPropsFor(expense, index) {
|
function dragPropsFor(expense: SummaryExpense, index: number): ExpenseDragProps {
|
||||||
if (!reorderEnabled) return { draggable: false };
|
if (!reorderEnabled) return { draggable: false };
|
||||||
return {
|
return {
|
||||||
draggable: true,
|
draggable: true,
|
||||||
isDragging: draggingId === expense.bill_id,
|
isDragging: draggingId === expense.bill_id,
|
||||||
isDropTarget: dropTargetId === expense.bill_id && draggingId !== expense.bill_id,
|
isDropTarget: dropTargetId === expense.bill_id && draggingId !== expense.bill_id,
|
||||||
onDragStart: (event) => {
|
onDragStart: (event: DragEvent<HTMLDivElement>) => {
|
||||||
setDraggingId(expense.bill_id);
|
setDraggingId(expense.bill_id);
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
event.dataTransfer.setData('text/plain', String(expense.bill_id));
|
event.dataTransfer.setData('text/plain', String(expense.bill_id));
|
||||||
|
|
@ -387,12 +459,12 @@ export default function SummaryPage() {
|
||||||
onDragEnter: () => {
|
onDragEnter: () => {
|
||||||
if (draggingId && draggingId !== expense.bill_id) setDropTargetId(expense.bill_id);
|
if (draggingId && draggingId !== expense.bill_id) setDropTargetId(expense.bill_id);
|
||||||
},
|
},
|
||||||
onDragOver: (event) => {
|
onDragOver: (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer.dropEffect = 'move';
|
event.dataTransfer.dropEffect = 'move';
|
||||||
if (draggingId && draggingId !== expense.bill_id) setDropTargetId(expense.bill_id);
|
if (draggingId && draggingId !== expense.bill_id) setDropTargetId(expense.bill_id);
|
||||||
},
|
},
|
||||||
onDrop: (event) => {
|
onDrop: (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const sourceId = Number(event.dataTransfer.getData('text/plain') || draggingId);
|
const sourceId = Number(event.dataTransfer.getData('text/plain') || draggingId);
|
||||||
const fromIndex = expenses.findIndex(item => item.bill_id === sourceId);
|
const fromIndex = expenses.findIndex(item => item.bill_id === sourceId);
|
||||||
|
|
@ -551,33 +623,15 @@ export default function SummaryPage() {
|
||||||
<div className="summary-income-form grid gap-3 rounded-2xl border border-border/60 bg-background/80 p-3 md:grid-cols-[1fr_1fr_1fr_auto] md:items-end">
|
<div className="summary-income-form grid gap-3 rounded-2xl border border-border/60 bg-background/80 p-3 md:grid-cols-[1fr_1fr_1fr_auto] md:items-end">
|
||||||
<label className="space-y-1">
|
<label className="space-y-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground">1st</span>
|
<span className="text-xs font-medium text-muted-foreground">1st</span>
|
||||||
<Input
|
<Input type="number" min="0" step="0.01" value={startingFirst} onChange={event => setStartingFirst(event.target.value)} />
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
value={startingFirst}
|
|
||||||
onChange={event => setStartingFirst(event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<label className="space-y-1">
|
<label className="space-y-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground">15th</span>
|
<span className="text-xs font-medium text-muted-foreground">15th</span>
|
||||||
<Input
|
<Input type="number" min="0" step="0.01" value={startingFifteenth} onChange={event => setStartingFifteenth(event.target.value)} />
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
value={startingFifteenth}
|
|
||||||
onChange={event => setStartingFifteenth(event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<label className="space-y-1">
|
<label className="space-y-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground">Other</span>
|
<span className="text-xs font-medium text-muted-foreground">Other</span>
|
||||||
<Input
|
<Input type="number" min="0" step="0.01" value={startingOther} onChange={event => setStartingOther(event.target.value)} />
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
value={startingOther}
|
|
||||||
onChange={event => setStartingOther(event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<Button onClick={saveStartingAmounts} disabled={saving} className="summary-edit-actions w-full md:w-auto">
|
<Button onClick={saveStartingAmounts} disabled={saving} className="summary-edit-actions w-full md:w-auto">
|
||||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||||
|
|
@ -612,8 +666,8 @@ export default function SummaryPage() {
|
||||||
{Number(data?.income?.amount) > 0 && Number(summary?.expense_total) > 0 && (
|
{Number(data?.income?.amount) > 0 && Number(summary?.expense_total) > 0 && (
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs text-muted-foreground">After expenses</div>
|
<div className="text-xs text-muted-foreground">After expenses</div>
|
||||||
<div className={cn('tracker-number mt-1 text-lg font-bold', moneyClass(Number(data.income.amount) - Number(summary.expense_total)))}>
|
<div className={cn('tracker-number mt-1 text-lg font-bold', moneyClass(Number(data?.income?.amount) - Number(summary?.expense_total)))}>
|
||||||
{fmt(Number(data.income.amount) - Number(summary.expense_total))}
|
{fmt(Number(data?.income?.amount) - Number(summary?.expense_total))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -623,12 +677,7 @@ export default function SummaryPage() {
|
||||||
<div className="grid gap-3 rounded-2xl border border-border/60 bg-background/80 p-3 md:grid-cols-[1fr_2fr_auto] md:items-end">
|
<div className="grid gap-3 rounded-2xl border border-border/60 bg-background/80 p-3 md:grid-cols-[1fr_2fr_auto] md:items-end">
|
||||||
<label className="space-y-1">
|
<label className="space-y-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground">Label</span>
|
<span className="text-xs font-medium text-muted-foreground">Label</span>
|
||||||
<Input
|
<Input value={incomeLabel} onChange={e => setIncomeLabel(e.target.value)} placeholder="Salary" maxLength={80} />
|
||||||
value={incomeLabel}
|
|
||||||
onChange={e => setIncomeLabel(e.target.value)}
|
|
||||||
placeholder="Salary"
|
|
||||||
maxLength={80}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<label className="space-y-1">
|
<label className="space-y-1">
|
||||||
<span className="text-xs font-medium text-muted-foreground">Amount</span>
|
<span className="text-xs font-medium text-muted-foreground">Amount</span>
|
||||||
Loading…
Reference in New Issue