diff --git a/app/lib/actions/locationActions.ts b/app/lib/actions/locationActions.ts index dc7192b..cd704c2 100644 --- a/app/lib/actions/locationActions.ts +++ b/app/lib/actions/locationActions.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; import clientPromise, { getDbClient } from '../dbClient'; -import { BillingLocation } from '../db-types'; +import { BillingLocation, YearMonth } from '../db-types'; import { ObjectId } from 'mongodb'; import { auth, withUser } from '@/app/lib/auth'; import { AuthenticatedUser } from '../types/next-auth'; @@ -33,7 +33,7 @@ const UpdateLocation = FormSchema.omit({ _id: true }); * @param formData form data * @returns */ -export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locationId?: string, year?: string, month?: string, prevState:State, formData: FormData) => { +export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locationId?: string, yearMonth?: YearMonth, prevState:State, formData: FormData) => { const validatedFields = UpdateLocation.safeParse({ locationName: formData.get('locationName'), @@ -70,15 +70,14 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat notes: locationNotes, } }); - } else if(year && month) { + } else if(yearMonth) { await dbClient.collection("lokacije").insertOne({ _id: (new ObjectId()).toHexString(), userId, userEmail, name: locationName, notes: locationNotes, - year: parseInt(year), // ToDo: get the current year and month - month: parseInt(month), // ToDo: get the current year and month + yearMonth: yearMonth, bills: [], }); } @@ -99,7 +98,7 @@ export const fetchAllLocations = withUser(async (user:AuthenticatedUser, pageIx: // fetch `pageSize` locations for the given page index const locations = await dbClient.collection("lokacije") .find({ userId }) - .sort({ year: -1, month: -1, name: 1 }) + .sort({ yearMonth: -1, name: 1 }) .skip(pageIx * pageSize) .limit(pageSize) .toArray(); diff --git a/app/lib/actions/monthActions.ts b/app/lib/actions/monthActions.ts index 7689d62..b6c083a 100644 --- a/app/lib/actions/monthActions.ts +++ b/app/lib/actions/monthActions.ts @@ -4,7 +4,7 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; import clientPromise, { getDbClient } from '../dbClient'; import { ObjectId } from 'mongodb'; -import { BillingLocation } from '../db-types'; +import { BillingLocation, YearMonth } from '../db-types'; import { AuthenticatedUser } from '../types/next-auth'; import { withUser } from '../auth'; @@ -16,14 +16,12 @@ import { withUser } from '../auth'; * @param formData form data * @returns */ -export const addMonth = withUser(async (user:AuthenticatedUser, yearString: string, monthString: string) => { +export const addMonth = withUser(async (user:AuthenticatedUser, { year, month }: YearMonth) => { const { id: userId } = user; // update the bill in the mongodb const dbClient = await getDbClient(); - const year = parseInt(yearString); - const month = parseInt(monthString); const prevYear = month === 1 ? year - 1 : year; const prevMonth = month === 1 ? 12 : month - 1; @@ -31,8 +29,10 @@ export const addMonth = withUser(async (user:AuthenticatedUser, yearString: stri // find all locations for the previous month const prevMonthLocations = await dbClient.collection("lokacije").find({ userId, // make sure that the locations belongs to the user - year: prevYear, - month: prevMonth, + yearMonth: { + year: prevYear, + month: prevMonth, + } }); const newMonthLocationsCursor = prevMonthLocations.map((prevLocation) => { @@ -41,8 +41,10 @@ export const addMonth = withUser(async (user:AuthenticatedUser, yearString: stri ...prevLocation, // assign a new ID _id: (new ObjectId()).toHexString(), - year: year, - month: month, + yearMonth: { + year: year, + month: month, + }, // copy bill array, but set all bills to unpaid and remove attachments and notes bills: prevLocation.bills.map((bill) => { return { diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index d7bdc20..a116986 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -9,6 +9,11 @@ export interface BillAttachment { fileContentsBase64: string; }; +export interface YearMonth { + year: number; + month: number; +}; + /** bill object in the form returned by MongoDB */ export interface BillingLocation { _id: string; @@ -18,10 +23,8 @@ export interface BillingLocation { userEmail?: string | null; /** name of the location */ name: string; - /** billing period year */ - year: number; - /** billing period month */ - month: number; + /** billing period year and month */ + yearMonth: YearMonth; /** array of bills */ bills: Bill[]; /** (optional) notes */ diff --git a/app/lib/format.ts b/app/lib/format.ts index cd0ea6a..def9c9d 100644 --- a/app/lib/format.ts +++ b/app/lib/format.ts @@ -1,4 +1,12 @@ +import { YearMonth } from "./db-types"; -export const formatYearMonth = (year: number, month:number): string => { +export const formatYearMonth = ({ year, month }: YearMonth): string => { return `${year}-${month<10?"0":""}${month}`; } + +export const parseYearMonth = (yearMonthString: string): YearMonth => { + + const [year, month] = yearMonthString.split("-").map((s) => parseInt(s, 10)); + + return({ year, month } as YearMonth); +} diff --git a/app/location/[id]/add/page.tsx b/app/location/[id]/add/page.tsx index 26036aa..24abc7a 100644 --- a/app/location/[id]/add/page.tsx +++ b/app/location/[id]/add/page.tsx @@ -1,7 +1,6 @@ +import { parseYearMonth } from '@/app/lib/format'; import { LocationEditForm } from '@/app/ui/LocationEditForm'; export default async function Page({ params:{ id } }: { params: { id:string } }) { - - const [year, month] = id.split("-"); - return (); + return (); } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 91f5f7e..3f90a56 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,18 +7,21 @@ import { isAuthErrorMessage } from '@/app/lib/auth'; import { fetchAllLocations } from './lib/actions/locationActions'; import { formatCurrency } from './lib/formatStrings'; import { fetchAvailableYears } from './lib/actions/monthActions'; +import { YearMonth } from './lib/db-types'; +import { formatYearMonth } from './lib/format'; -const getNextYearMonth = (year:number, month:number) => { +const getNextYearMonth = (yearMonth:YearMonth) => { + const {year, month} = yearMonth; return({ - year: month<12 ? year+1 : year, - month: month<12 ? month+1 : 1 - }); + year: month===12 ? year+1 : year, + month: month===12 ? 1 : month+1 + } as YearMonth); } export const Page = async () => { const locations = await fetchAllLocations(); - const availableYearMonths = await fetchAvailableYears(); + const availableYears = await fetchAvailableYears(); if(isAuthErrorMessage(locations)) { return ( @@ -30,13 +33,15 @@ export const Page = async () => { // if the database is in it's initial state, show the add location button for the current month if(locations.length === 0) { - const currentYear = new Date().getFullYear(); - const currentMonth = new Date().getMonth() + 1; + const currentYearMonth:YearMonth = { + year: new Date().getFullYear(), + month: new Date().getMonth() + 1 + }; return (
- - + +
); @@ -46,13 +51,17 @@ export const Page = async () => { return (
- + { locations.map((location, ix, array) => { - const isFirstLocationInMonth = ix === 0 || location.year !== array[ix-1].year || location.month !== array[ix-1].month; - const isLastLocationInMonth = location.year !== array[ix+1]?.year || location.month !== array[ix+1]?.month; - const isLastLocationOfFirstMonth = isLastLocationInMonth && location.year === array[0].year && location.month === array[0].month; + const { year, month } = location.yearMonth + const { year: prevYear, month: prevMonth } = array[ix-1]?.yearMonth ?? { year: undefined, month: undefined }; + const { year: nextYear, month: nextMonth } = array[ix+1]?.yearMonth ?? { year: undefined, month: undefined }; + + const isFirstLocationInMonth = ix === 0 || year !== prevYear || month !== prevMonth; + const isLastLocationInMonth = year !== nextYear || month !== nextMonth; + const isLastLocationOfFirstMonth = isLastLocationInMonth && year === array[0].yearMonth.year && month === array[0].yearMonth.month if(isFirstLocationInMonth) { monthlyExpense = 0; @@ -65,13 +74,13 @@ export const Page = async () => { { // show month title above the first LocationCard in the month isFirstLocationInMonth ? - : null + : null } { // show AddLocationButton as a last item in the firts month isLastLocationOfFirstMonth ? - : null + : null } { isLastLocationInMonth && monthlyExpense>0 ? @@ -88,7 +97,7 @@ export const Page = async () => { }) } - { availableYearMonths.map(ym =>

{ym}

) } + { availableYears.map(ym =>

{ym}

) }
); } diff --git a/app/ui/AddLocationButton.tsx b/app/ui/AddLocationButton.tsx index 7191385..9dbd30c 100644 --- a/app/ui/AddLocationButton.tsx +++ b/app/ui/AddLocationButton.tsx @@ -1,16 +1,16 @@ import { PlusCircleIcon } from "@heroicons/react/24/outline"; +import { YearMonth } from "../lib/db-types"; +import { formatYearMonth } from "../lib/format"; export interface AddLocationButtonProps { - /** year at which the new billing location should be addes */ - year: number - /** month at which the new billing location should be addes */ - month: number + /** year and month at which the new billing location should be addes */ + yearMonth: YearMonth } -export const AddLocationButton:React.FC = ({year,month}) => +export const AddLocationButton:React.FC = ({yearMonth}) =>
- + diff --git a/app/ui/AddMonthButton.tsx b/app/ui/AddMonthButton.tsx index 535b9bc..7f7b08a 100644 --- a/app/ui/AddMonthButton.tsx +++ b/app/ui/AddMonthButton.tsx @@ -1,12 +1,13 @@ import { PlusCircleIcon } from "@heroicons/react/24/outline"; import React from "react"; +import { formatYearMonth } from "../lib/format"; +import { YearMonth } from "../lib/db-types"; export interface AddMonthButtonProps { - year: number; - month: number; + yearMonth: YearMonth; } -export const AddMonthButton:React.FC = ({ year, month }) => - +export const AddMonthButton:React.FC = ({ yearMonth }) => + diff --git a/app/ui/LocationCard.tsx b/app/ui/LocationCard.tsx index d51eb1f..addc8ca 100644 --- a/app/ui/LocationCard.tsx +++ b/app/ui/LocationCard.tsx @@ -11,7 +11,7 @@ export interface LocationCardProps { location: BillingLocation } -export const LocationCard:FC = ({location: { _id, name, year, month, bills }}) => { +export const LocationCard:FC = ({location: { _id, name, yearMonth, bills }}) => { // sum all the billAmounts const monthlyExpense = bills.reduce((acc, bill) => acc + (bill.payedAmount ?? 0), 0); @@ -22,7 +22,7 @@ export const LocationCard:FC = ({location: { _id, name, year, -

{formatYearMonth(year, month)} {name}

+

{formatYearMonth(yearMonth)} {name}

{ bills.map(bill => ) diff --git a/app/ui/LocationEditForm.tsx b/app/ui/LocationEditForm.tsx index 64769fa..5fd3c28 100644 --- a/app/ui/LocationEditForm.tsx +++ b/app/ui/LocationEditForm.tsx @@ -2,7 +2,7 @@ import { TrashIcon } from "@heroicons/react/24/outline"; import { FC } from "react"; -import { BillingLocation } from "../lib/db-types"; +import { BillingLocation, YearMonth } from "../lib/db-types"; import { updateOrAddLocation } from "../lib/actions/locationActions"; import { useFormState } from "react-dom"; import { gotoHome } from "../lib/actions/billActions"; @@ -10,16 +10,14 @@ import { gotoHome } from "../lib/actions/billActions"; export interface LocationEditFormProps { /** location which should be edited */ location?: BillingLocation, - /** year at a new billing location should be assigned */ - year?: string - /** month at a new billing location should be assigned */ - month?: string + /** year adn month at a new billing location should be assigned */ + yearMonth?: YearMonth } -export const LocationEditForm:FC = ({ location, year, month }) => +export const LocationEditForm:FC = ({ location, yearMonth }) => { const initialState = { message: null, errors: {} }; - const handleAction = updateOrAddLocation.bind(null, location?._id, year, month); + const handleAction = updateOrAddLocation.bind(null, location?._id, yearMonth); const [ state, dispatch ] = useFormState(handleAction, initialState); // redirect to the main page diff --git a/app/ui/MonthTitle.tsx b/app/ui/MonthTitle.tsx index f02940f..ee46344 100644 --- a/app/ui/MonthTitle.tsx +++ b/app/ui/MonthTitle.tsx @@ -1,10 +1,10 @@ import { FC } from "react"; import { formatYearMonth } from "../lib/format"; +import { YearMonth } from "../lib/db-types"; export interface MonthTitleProps { - year: number; - month: number; + yearMonth: YearMonth } -export const MonthTitle:FC = ({year, month}) => -
{`${formatYearMonth(year, month)}`}
\ No newline at end of file +export const MonthTitle:FC = ({ yearMonth }) => +
{`${formatYearMonth(yearMonth)}`}
\ No newline at end of file diff --git a/app/year-month/[id]/add/page.tsx b/app/year-month/[id]/add/page.tsx index ed28252..f56be44 100644 --- a/app/year-month/[id]/add/page.tsx +++ b/app/year-month/[id]/add/page.tsx @@ -1,12 +1,11 @@ import { addMonth } from '@/app/lib/actions/monthActions'; +import { parseYearMonth } from '@/app/lib/format'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; export default async function Page({ params:{ id } }: { params: { id:string } }) { - const [year, month] = id.split("-"); - - await addMonth(year, month); + await addMonth(parseYearMonth(id)); revalidatePath('/'); redirect(`/`);