(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 <noreply@anthropic.com>
This commit is contained in:
2026-01-09 17:36:10 +01:00
parent 1ca55ae820
commit 37f617683e
2 changed files with 53 additions and 48 deletions

View File

@@ -1,9 +1,8 @@
import { fetchAllLocations } from '@/app/lib/actions/locationActions'; import { fetchAllLocations } from '@/app/lib/actions/locationActions';
import { fetchAvailableYears } from '@/app/lib/actions/monthActions'; import { fetchAvailableYears } from '@/app/lib/actions/monthActions';
import { getUserSettings } from '@/app/lib/actions/userSettingsActions'; import { getUserSettings } from '@/app/lib/actions/userSettingsActions';
import { BillingLocation, YearMonth } from '@/app/lib/db-types';
import { FC } from 'react'; import { FC } from 'react';
import { MonthLocationList } from '@/app/ui/MonthLocationList'; import { MonthArray, MonthLocationList } from '@/app/ui/MonthLocationList';
import { ParamsYearInvalidMessage } from './ParamsYearInvalidMessage'; import { ParamsYearInvalidMessage } from './ParamsYearInvalidMessage';
export interface HomePageProps { export interface HomePageProps {
@@ -46,51 +45,59 @@ export const HomePage:FC<HomePageProps> = async ({ searchParams }) => {
} }
selectedYear = paramsYear; selectedYear = paramsYear;
} else { } else {
const currYear = new Date().getFullYear(); const currYear = new Date().getFullYear();
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 // IF current year is available in DB THEN use it
// ELSE use the latest year found in the DB // ELSE use the latest year found in the DB
selectedYear = availableYears.includes(currYear) ? currYear : availableYears[0]; selectedYear = availableYears.includes(currYear) ? currYear : availableYears[0];
} }
}
const locations = await fetchAllLocations(selectedYear); const locations = await fetchAllLocations(selectedYear);
const userSettings = await getUserSettings(); const userSettings = await getUserSettings();
// group locations by month // Create months object by grouping locations by yearMonth
const months = locations.reduce((acc, location) => { const { months } = locations.reduce((acc, location) => {
const {year, month} = location.yearMonth; const {year, month} = location.yearMonth;
const key = `${year}-${month}`; const key = `${year}-${month}`;
const locationsInMonth = acc[key]; const monthIx = acc.index[key];
if(locationsInMonth) { if(monthIx) {
return({
...acc, const existingMonth = acc.months[monthIx];
[key]: {
yearMonth: location.yearMonth, existingMonth.locations.push(location);
locations: [...locationsInMonth.locations, location], existingMonth.unpaidTotal += location.bills.reduce((acc, bill) => !bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0);
unpaidTotal: locationsInMonth.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);
payedTotal: locationsInMonth.payedTotal + location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
} return acc;
})
} }
return({ acc.months.push({
...acc,
[key]: {
yearMonth: location.yearMonth, yearMonth: location.yearMonth,
locations: [location], locations: [location],
unpaidTotal: location.bills.reduce((acc, bill) => !bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0), 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) payedTotal: location.bills.reduce((acc, bill) => bill.paid ? acc + (bill.payedAmount ?? 0) : acc, 0)
}
}); });
}, {} as {[key:string]:{
yearMonth: YearMonth, acc.index[key] = acc.months.length - 1;
locations: BillingLocation[],
unpaidTotal: number, return acc;
payedTotal: number }, {
} }); index: {},
months: []
} as {
index: Record<string, number>,
months: MonthArray
});
return ( return (
<MonthLocationList availableYears={availableYears} months={months} userSettings={userSettings} /> <MonthLocationList availableYears={availableYears} months={months} userSettings={userSettings} />

View File

@@ -22,22 +22,22 @@ const getNextYearMonth = (yearMonth:YearMonth) => {
} as YearMonth); } as YearMonth);
} }
export interface MonthLocationListProps { export type MonthArray = Array<{
availableYears?: number[];
months?: {
[key: string]: {
yearMonth: YearMonth; yearMonth: YearMonth;
locations: BillingLocation[]; locations: BillingLocation[];
payedTotal: number; payedTotal: number;
unpaidTotal: number; unpaidTotal: number;
}; }>;
};
export interface MonthLocationListProps {
availableYears: number[];
months: MonthArray;
userSettings?: UserSettings | null; userSettings?: UserSettings | null;
} }
export const MonthLocationList:React.FC<MonthLocationListProps > = ({ export const MonthLocationList:React.FC<MonthLocationListProps > = ({
availableYears, availableYears,
months, months: activeYearMonths,
userSettings, userSettings,
}) => { }) => {
@@ -93,7 +93,7 @@ export const MonthLocationList:React.FC<MonthLocationListProps > = ({
} }
}, [search, router, t]); }, [search, router, t]);
if(!availableYears || !months) { if(availableYears.length === 0 || activeYearMonths.length === 0) {
const currentYearMonth:YearMonth = { const currentYearMonth:YearMonth = {
year: new Date().getFullYear(), year: new Date().getFullYear(),
month: new Date().getMonth() + 1 month: new Date().getMonth() + 1
@@ -107,8 +107,6 @@ export const MonthLocationList:React.FC<MonthLocationListProps > = ({
</>) </>)
}; };
const monthsArray = Object.entries(months);
// when the month is toggled, update the URL // when the month is toggled, update the URL
// and set the the new expandedMonth // and set the the new expandedMonth
const handleMonthToggle = (yearMonth:YearMonth) => { const handleMonthToggle = (yearMonth:YearMonth) => {
@@ -123,10 +121,10 @@ export const MonthLocationList:React.FC<MonthLocationListProps > = ({
} }
return(<> return(<>
<AddMonthButton yearMonth={getNextYearMonth(monthsArray[0][1].locations[0].yearMonth)} /> <AddMonthButton yearMonth={getNextYearMonth(activeYearMonths[0].locations[0].yearMonth)} />
{ {
monthsArray.map(([monthKey, { yearMonth, locations, unpaidTotal, payedTotal }], monthIx) => activeYearMonths.map(({ yearMonth, locations, unpaidTotal, payedTotal }, monthIx) =>
<MonthCard yearMonth={yearMonth} key={`month-${monthKey}`} unpaidTotal={unpaidTotal} payedTotal={payedTotal} currency={userSettings?.currency} expanded={ yearMonth.month === expandedMonth } onToggle={handleMonthToggle} > <MonthCard yearMonth={yearMonth} key={`month-${yearMonth.year}-${yearMonth.month}`} unpaidTotal={unpaidTotal} payedTotal={payedTotal} currency={userSettings?.currency} expanded={ yearMonth.month === expandedMonth } onToggle={handleMonthToggle} >
{ {
yearMonth.month === expandedMonth ? yearMonth.month === expandedMonth ?
locations.map((location, ix) => <LocationCard key={`location-${location._id}`} location={location} currency={userSettings?.currency} />) locations.map((location, ix) => <LocationCard key={`location-${location._id}`} location={location} currency={userSettings?.currency} />)