From 37f617683e35f22ba91692c5b62253ae07be761c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Fri, 9 Jan 2026 17:36:10 +0100 Subject: [PATCH] (refactor + bugfix) Improve data structure and handle empty database edge cases Refactored months data structure from object to array for better performance and cleaner iteration. Fixed crash when availableYears array is empty by adding proper guards and fallback to current year. Changes: - MonthLocationList: Changed months prop from object to array type - HomePage: Refactored reduce logic to build array instead of object - HomePage: Added empty database handling in year selection logic - HomePage: Added early returns for invalid year params in empty DB Co-Authored-By: Claude Sonnet 4.5 --- web-app/app/ui/HomePage.tsx | 71 +++++++++++++++------------- web-app/app/ui/MonthLocationList.tsx | 30 ++++++------ 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/web-app/app/ui/HomePage.tsx b/web-app/app/ui/HomePage.tsx index 2e46fcb..581608d 100644 --- a/web-app/app/ui/HomePage.tsx +++ b/web-app/app/ui/HomePage.tsx @@ -1,9 +1,8 @@ import { fetchAllLocations } from '@/app/lib/actions/locationActions'; import { fetchAvailableYears } from '@/app/lib/actions/monthActions'; import { getUserSettings } from '@/app/lib/actions/userSettingsActions'; -import { BillingLocation, YearMonth } from '@/app/lib/db-types'; import { FC } from 'react'; -import { MonthLocationList } from '@/app/ui/MonthLocationList'; +import { MonthArray, MonthLocationList } from '@/app/ui/MonthLocationList'; import { ParamsYearInvalidMessage } from './ParamsYearInvalidMessage'; export interface HomePageProps { @@ -46,51 +45,59 @@ export const HomePage:FC = async ({ searchParams }) => { } selectedYear = paramsYear; + } else { const currYear = new Date().getFullYear(); - // IF current year is available in DB THEN use it - // ELSE use the latest year found in the DB - selectedYear = availableYears.includes(currYear) ? currYear : availableYears[0]; + if(availableYears.length === 0) { + // Database is in it's initial state + // so just set selected year to current year + selectedYear = currYear; + } else { + // IF current year is available in DB THEN use it + // ELSE use the latest year found in the DB + selectedYear = availableYears.includes(currYear) ? currYear : availableYears[0]; + } } const locations = await fetchAllLocations(selectedYear); const userSettings = await getUserSettings(); - // group locations by month - const months = locations.reduce((acc, location) => { + // Create months object by grouping locations by yearMonth + const { months } = locations.reduce((acc, location) => { const {year, month} = location.yearMonth; const key = `${year}-${month}`; - const locationsInMonth = acc[key]; + const monthIx = acc.index[key]; - if(locationsInMonth) { - return({ - ...acc, - [key]: { - yearMonth: location.yearMonth, - locations: [...locationsInMonth.locations, location], - unpaidTotal: locationsInMonth.unpaidTotal + location.bills.reduce((acc, bill) => !bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0), - payedTotal: locationsInMonth.payedTotal + location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0) - } - }) + if(monthIx) { + + const existingMonth = acc.months[monthIx]; + + existingMonth.locations.push(location); + existingMonth.unpaidTotal += location.bills.reduce((acc, bill) => !bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0); + existingMonth.payedTotal += location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0); + + return acc; } - return({ - ...acc, - [key]: { - yearMonth: location.yearMonth, - locations: [location], - unpaidTotal: location.bills.reduce((acc, bill) => !bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0), - payedTotal: location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0) - } + acc.months.push({ + yearMonth: location.yearMonth, + locations: [location], + unpaidTotal: location.bills.reduce((acc, bill) => !bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0), + payedTotal: location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0) }); - }, {} as {[key:string]:{ - yearMonth: YearMonth, - locations: BillingLocation[], - unpaidTotal: number, - payedTotal: number - } }); + + acc.index[key] = acc.months.length - 1; + + return acc; + }, { + index: {}, + months: [] + } as { + index: Record, + months: MonthArray + }); return ( diff --git a/web-app/app/ui/MonthLocationList.tsx b/web-app/app/ui/MonthLocationList.tsx index 86bce23..ce94482 100644 --- a/web-app/app/ui/MonthLocationList.tsx +++ b/web-app/app/ui/MonthLocationList.tsx @@ -22,22 +22,22 @@ const getNextYearMonth = (yearMonth:YearMonth) => { } as YearMonth); } +export type MonthArray = Array<{ + yearMonth: YearMonth; + locations: BillingLocation[]; + payedTotal: number; + unpaidTotal: number; +}>; + export interface MonthLocationListProps { - availableYears?: number[]; - months?: { - [key: string]: { - yearMonth: YearMonth; - locations: BillingLocation[]; - payedTotal: number; - unpaidTotal: number; - }; - }; + availableYears: number[]; + months: MonthArray; userSettings?: UserSettings | null; } export const MonthLocationList:React.FC = ({ availableYears, - months, + months: activeYearMonths, userSettings, }) => { @@ -93,7 +93,7 @@ export const MonthLocationList:React.FC = ({ } }, [search, router, t]); - if(!availableYears || !months) { + if(availableYears.length === 0 || activeYearMonths.length === 0) { const currentYearMonth:YearMonth = { year: new Date().getFullYear(), month: new Date().getMonth() + 1 @@ -107,8 +107,6 @@ export const MonthLocationList:React.FC = ({ ) }; - const monthsArray = Object.entries(months); - // when the month is toggled, update the URL // and set the the new expandedMonth const handleMonthToggle = (yearMonth:YearMonth) => { @@ -123,10 +121,10 @@ export const MonthLocationList:React.FC = ({ } return(<> - + { - monthsArray.map(([monthKey, { yearMonth, locations, unpaidTotal, payedTotal }], monthIx) => - + activeYearMonths.map(({ yearMonth, locations, unpaidTotal, payedTotal }, monthIx) => + { yearMonth.month === expandedMonth ? locations.map((location, ix) => )