From d4f236a54ca6bf10defb804fb88738057c3c62f5 Mon Sep 17 00:00:00 2001 From: Dave Nguyen Date: Fri, 24 Oct 2025 21:00:36 +0200 Subject: [PATCH 1/2] feat(budget-tracker-table): add remaining amount --- .../components/budget-tracker-table.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/features/budget-tracker/components/budget-tracker-table.tsx b/src/features/budget-tracker/components/budget-tracker-table.tsx index 3eae2d2..9cd8b47 100644 --- a/src/features/budget-tracker/components/budget-tracker-table.tsx +++ b/src/features/budget-tracker/components/budget-tracker-table.tsx @@ -156,7 +156,12 @@ export function BudgetTrackerTable({ {item.name}
- {formatCzechNumber(effectiveAmount)} CZK + {formatCzechNumber(effectiveAmount)} CZK + {parseFloat(effectiveAmountPaid) > 0 && !item.paid && ( + + ({formatCzechNumber(parseFloat(effectiveAmount) - parseFloat(effectiveAmountPaid))} CZK) + + )} {!affordable && !item.paid && }
@@ -217,7 +222,12 @@ export function BudgetTrackerTable({
- {formatCzechNumber(subItem.amount)} CZK + {formatCzechNumber(subItem.amount)} CZK + {parseFloat(subItem.amountPaid) > 0 && !subItem.paid && ( + + ({formatCzechNumber(parseFloat(subItem.amount) - parseFloat(subItem.amountPaid))} CZK) + + )} {!subItemAffordable && !subItem.paid && ( )} From 64032ccc46f9e5e9ec55a397a059694a41fa5100 Mon Sep 17 00:00:00 2001 From: Dave Nguyen Date: Mon, 10 Nov 2025 20:51:45 +0100 Subject: [PATCH 2/2] feat(budget-tracker): add next month projection --- .../components/budget-tracker-list.tsx | 97 ++++++++++++++++--- .../components/budget-tracker-table.tsx | 15 ++- .../components/next-month-projection.tsx | 96 ++++++++++++++++++ src/store/use-budget-filters-store.ts | 53 ++++++++++ 4 files changed, 248 insertions(+), 13 deletions(-) create mode 100644 src/features/budget-tracker/components/next-month-projection.tsx create mode 100644 src/store/use-budget-filters-store.ts diff --git a/src/features/budget-tracker/components/budget-tracker-list.tsx b/src/features/budget-tracker/components/budget-tracker-list.tsx index df7ed99..01a9640 100644 --- a/src/features/budget-tracker/components/budget-tracker-list.tsx +++ b/src/features/budget-tracker/components/budget-tracker-list.tsx @@ -2,7 +2,8 @@ import { useState } from 'react' import { Button } from '@/components/ui/button' import { ConfirmationDialog } from '@/components/ui/confirmation-dialog' import { type BudgetItem, type BudgetSubItem } from '@/db/schema' -import { PlusIcon } from 'lucide-react' +import { useBudgetFiltersStore } from '@/store/use-budget-filters-store' +import { Eye, PlusIcon } from 'lucide-react' import { toast } from 'sonner' import { formatCzechNumber } from '@/lib/formatters' @@ -19,6 +20,7 @@ import { BalanceInput } from './balance-input' import { BudgetItemForm } from './budget-item-form' import { BudgetSubItemForm } from './budget-sub-item-form' import { BudgetTrackerTable } from './budget-tracker-table' +import { NextMonthProjection } from './next-month-projection' export function BudgetTrackerList() { const { data: balance } = useBudgetBalance() @@ -47,14 +49,23 @@ export function BudgetTrackerList() { // Selected items for filtering summary calculations const [selectedItemIds, setSelectedItemIds] = useState>(new Set()) + // Hidden items for visibility filtering (persisted via Zustand) + const hiddenItemIds = useBudgetFiltersStore((state) => state.hiddenItemIds) + const toggleHideItemStore = useBudgetFiltersStore((state) => state.toggleHideItem) + const showAllStore = useBudgetFiltersStore((state) => state.showAll) + if (isLoading) { return
Loading budget tracker...
} const currentBalance = balance ? parseFloat(balance.amount) : 0 + // Filter out hidden items first + const visibleItems = items.filter((item) => !hiddenItemIds.has(item.id)) + // Filter items based on selection (if any items are selected, only show those) - const filteredItems = selectedItemIds.size > 0 ? items.filter((item) => selectedItemIds.has(item.id)) : items + const filteredItems = + selectedItemIds.size > 0 ? visibleItems.filter((item) => selectedItemIds.has(item.id)) : visibleItems // Calculate total amount and total paid using effective amounts (includes sub-items) const totalAmount = filteredItems.reduce((sum, item) => { @@ -87,13 +98,29 @@ export function BudgetTrackerList() { } const handleSelectAll = () => { - if (selectedItemIds.size === items.length) { + if (selectedItemIds.size === visibleItems.length) { setSelectedItemIds(new Set()) } else { - setSelectedItemIds(new Set(items.map((item) => item.id))) + setSelectedItemIds(new Set(visibleItems.map((item) => item.id))) } } + const handleToggleHideItem = (itemId: string) => { + toggleHideItemStore(itemId) + // Also remove from selected if hiding + if (!hiddenItemIds.has(itemId)) { + setSelectedItemIds((selectedPrev) => { + const newSelected = new Set(selectedPrev) + newSelected.delete(itemId) + return newSelected + }) + } + } + + const handleShowAll = () => { + showAllStore() + } + const handleEditItem = (item: BudgetItem) => { setEditingItem(item) } @@ -155,19 +182,30 @@ export function BudgetTrackerList() { return (
{/* Balance Section */} - +
+ + +
{/* Summary Section */}

Summary {selectedItemIds.size > 0 && `(${selectedItemIds.size} selected)`} + {hiddenItemIds.size > 0 && ` • ${hiddenItemIds.size} hidden`}

- {items.length > 0 && ( - - )} +
+ {hiddenItemIds.size > 0 && ( + + )} + {visibleItems.length > 0 && ( + + )} +
@@ -196,10 +234,11 @@ export function BudgetTrackerList() { {/* Budget Items Table */} {items.length > 0 ? ( )} + {/* Hidden Items Section */} + {hiddenItemIds.size > 0 && ( +
+

Hidden Items ({hiddenItemIds.size})

+
+
+ {items + .filter((item) => hiddenItemIds.has(item.id)) + .map((item) => ( +
+
+ {item.name} + + {formatCzechNumber( + (item as typeof item & { _effectiveAmount?: number })._effectiveAmount || + parseFloat(item.amount) + )}{' '} + CZK + +
+ +
+ ))} +
+
+
+ )} + {/* Add/Edit Item Dialog */} diff --git a/src/features/budget-tracker/components/budget-tracker-table.tsx b/src/features/budget-tracker/components/budget-tracker-table.tsx index 9cd8b47..afc6855 100644 --- a/src/features/budget-tracker/components/budget-tracker-table.tsx +++ b/src/features/budget-tracker/components/budget-tracker-table.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { type BudgetItem, type BudgetSubItem } from '@/db/schema' -import { ChevronDown, ChevronRight, Pencil, Plus, Trash2, TriangleAlert } from 'lucide-react' +import { ChevronDown, ChevronRight, EyeOff, Pencil, Plus, Trash2, TriangleAlert } from 'lucide-react' import { formatCzechNumber } from '@/lib/formatters' import { cn } from '@/lib/utils' @@ -14,6 +14,7 @@ interface BudgetTrackerTableProps { balance: number selectedItemIds: Set onToggleSelectItem: (itemId: string) => void + onToggleHideItem: (itemId: string) => void onEditItem: (item: BudgetItem) => void onDeleteItem: (item: BudgetItem) => void onAddSubItem: (itemId: string) => void @@ -27,6 +28,7 @@ export function BudgetTrackerTable({ balance, selectedItemIds, onToggleSelectItem, + onToggleHideItem, onEditItem, onDeleteItem, onAddSubItem, @@ -80,7 +82,7 @@ export function BudgetTrackerTable({ Amount Paid Paid Note - Actions + Actions @@ -178,6 +180,15 @@ export function BudgetTrackerTable({
+ + )} +
+ + {isAdding ? ( +
+
+ + setAdditionalAmount(e.target.value)} + placeholder="60000" + /> +
+
+ + +
+
+ ) : hasProjection ? ( +
+
{formatCzechNumber(projectedBalance)} CZK
+
+ Current: {formatCzechNumber(currentBalance)} CZK + {formatCzechNumber(additionalAmount)} CZK +
+ +
+ ) : ( + + )} +
+
+ ) +} diff --git a/src/store/use-budget-filters-store.ts b/src/store/use-budget-filters-store.ts new file mode 100644 index 0000000..3cf52fd --- /dev/null +++ b/src/store/use-budget-filters-store.ts @@ -0,0 +1,53 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +interface BudgetFiltersState { + hiddenItemIds: Set + toggleHideItem: (id: string) => void + showAll: () => void +} + +export const useBudgetFiltersStore = create()( + persist( + (set) => ({ + hiddenItemIds: new Set(), + toggleHideItem: (id) => + set((state) => { + const newSet = new Set(state.hiddenItemIds) + if (newSet.has(id)) { + newSet.delete(id) + } else { + newSet.add(id) + } + return { hiddenItemIds: newSet } + }), + showAll: () => set({ hiddenItemIds: new Set() }), + }), + { + name: 'budget-filters', + storage: { + getItem: (name) => { + const str = localStorage.getItem(name) + if (!str) return null + const { state } = JSON.parse(str) + return { + state: { + ...state, + hiddenItemIds: new Set(state.hiddenItemIds || []), + }, + } + }, + setItem: (name, newValue) => { + const str = JSON.stringify({ + state: { + ...newValue.state, + hiddenItemIds: Array.from(newValue.state.hiddenItemIds), + }, + }) + localStorage.setItem(name, str) + }, + removeItem: (name) => localStorage.removeItem(name), + }, + } + ) +)