From 02a3023f7a1cbbd52b044eb87d736b7928cf5e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Mon, 11 Aug 2025 10:00:07 +0200 Subject: [PATCH 01/12] add CLAUDE.md with development guidance for Claude Code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provides architectural overview, development commands, and key patterns for future Claude instances working with this codebase. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bcaa9d7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +- `npm run dev` - Start development server (Next.js) +- `npm run build` - Build production version +- `npm start` - Start production server +- `npm run prettier` - Format code with Prettier +- `npm run prettier:check` - Check code formatting +- `npm run seed` - Seed database with initial data + +## Deployment Commands + +- `./build.sh` - Build Docker image for deployment +- `./deploy.sh` - Deploy Docker service to production +- `./debug-deploy.sh` - Deploy with debug configuration + +## Architecture Overview + +This is a Next.js 14 utility bills tracking application ("Evidencija Režija") with the following key components: + +### Tech Stack +- **Framework**: Next.js 14 with App Router and standalone output for Docker +- **Authentication**: NextAuth v5 with Google OAuth +- **Database**: MongoDB with connection pooling +- **Internationalization**: next-intl (Croatian/English) +- **Styling**: Tailwind CSS with DaisyUI components +- **Deployment**: Docker with MongoDB 4.4.27 (AVX compatibility) + +### Core Architecture Patterns + +**Multi-user Data Isolation**: All database operations use the `withUser` higher-order function from `app/lib/auth.ts:102` to automatically inject authenticated user ID into queries, ensuring data isolation between users. + +**Server Actions Pattern**: Form handling uses Next.js Server Actions with Zod validation. Actions are defined in `app/lib/actions/` and follow the pattern: +```typescript +export const actionName = withUser(async (user: AuthenticatedUser, ...args) => { + // Server action implementation with automatic user context +}); +``` + +**Internationalization**: Uses next-intl with locale-based routing. Messages are in `messages/` directory. The middleware handles both auth and i18n routing. + +### Key Files & Responsibilities + +- `middleware.ts` - Handles authentication and i18n routing, defines public pages +- `app/lib/auth.ts` - NextAuth configuration, `withUser` HOF for user context +- `app/lib/dbClient.ts` - MongoDB connection with development/production handling +- `app/lib/actions/` - Server actions for data mutations (locations, bills, months) +- `app/i18n.ts` - Internationalization configuration (Croatian default) +- `next.config.js` - Standalone build config with `serverActions.allowedOrigins` for Docker deployment + +### Database Schema +- **Collections**: Locations, Bills, Months (year-month periods) +- **User Association**: All documents include `userId` field for multi-tenant isolation +- **Database Name**: "utility-bills" + +### Docker Deployment Notes +- Uses standalone Next.js build for Docker optimization +- MongoDB 4.4.27 required for older CPU compatibility (no AVX instructions) +- `HOSTNAME=0.0.0.0` with `serverActions.allowedOrigins` config to handle reverse proxy headers +- Environment variables: `MONGODB_URI`, `GOOGLE_ID`, `GOOGLE_SECRET`, `AUTH_SECRET` + +### Barcode/QR Code Feature +- Uses `@zxing/library` and `@zxing/browser` for PDF document scanning +- Heavy barcode decoding operations should be moved to background threads if performance issues arise +- PDF processing with `pdfjs-dist` for utility bill scanning + +### Testing & Code Quality +- ESLint with Next.js and Prettier configurations +- No specific test framework configured - check with user before assuming testing approach \ No newline at end of file From eef93528e3e20e9e7752af71a65ed26de6f70c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Mon, 11 Aug 2025 10:10:45 +0200 Subject: [PATCH 02/12] set current year as default active year MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes default year from first available year in database to current year (Date.now) when no year parameter is provided in URL. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/ui/HomePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/HomePage.tsx b/app/ui/HomePage.tsx index 2f3f59a..df76dc6 100644 --- a/app/ui/HomePage.tsx +++ b/app/ui/HomePage.tsx @@ -32,7 +32,7 @@ export const HomePage:FC = async ({ searchParams }) => { return (); } - const currentYear = Number(searchParams?.year) || availableYears[0]; + const currentYear = Number(searchParams?.year) || new Date().getFullYear(); const locations = await fetchAllLocations(currentYear); From 78a6c18ba53745384896ec6214bfbade42c7c3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Mon, 11 Aug 2025 10:27:31 +0200 Subject: [PATCH 03/12] add option to create location in all subsequent months MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added toggle in LocationEditForm for adding locations to future months (disabled by default) - Modified updateOrAddLocation action to support batch creation across subsequent months - Only creates locations in months where no location with same name exists - Added translations for toggle text in Croatian and English - Fixed unused variable warning in deleteLocationById - Improved auth.ts development comments for clarity 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/lib/actions/locationActions.ts | 78 +++++++++++++++++++++++++++++- app/lib/auth.ts | 32 ++++++++---- app/ui/LocationEditForm.tsx | 10 ++++ messages/en.json | 1 + messages/hr.json | 1 + 5 files changed, 112 insertions(+), 10 deletions(-) diff --git a/app/lib/actions/locationActions.ts b/app/lib/actions/locationActions.ts index d1ef4c4..d1b29ed 100644 --- a/app/lib/actions/locationActions.ts +++ b/app/lib/actions/locationActions.ts @@ -27,6 +27,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ _id: z.string(), locationName: z.coerce.string().min(1, t("location-name-required")), locationNotes: z.string(), + addToSubsequentMonths: z.boolean().optional(), }) // dont include the _id field in the response .omit({ _id: true }); @@ -47,6 +48,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat const validatedFields = FormSchema(t).safeParse({ locationName: formData.get('locationName'), locationNotes: formData.get('locationNotes'), + addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on', }); // If form validation fails, return errors early. Otherwise, continue... @@ -60,6 +62,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat const { locationName, locationNotes, + addToSubsequentMonths, } = validatedFields.data; // update the bill in the mongodb @@ -80,6 +83,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat } }); } else if(yearMonth) { + // Always add location to the specified month await dbClient.collection("lokacije").insertOne({ _id: (new ObjectId()).toHexString(), userId, @@ -89,6 +93,78 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat yearMonth: yearMonth, bills: [], }); + + // If addToSubsequentMonths is enabled, add to all subsequent months + if (addToSubsequentMonths) { + // Find all subsequent months that exist in the database + const subsequentMonths = await dbClient.collection("lokacije") + .aggregate([ + { + $match: { + userId, + $or: [ + { "yearMonth.year": { $gt: yearMonth.year } }, + { + "yearMonth.year": yearMonth.year, + "yearMonth.month": { $gt: yearMonth.month } + } + ] + } + }, + { + $group: { + _id: { + year: "$yearMonth.year", + month: "$yearMonth.month" + } + } + }, + { + $project: { + _id: 0, + year: "$_id.year", + month: "$_id.month" + } + }, + { + $sort: { + year: 1, + month: 1 + } + } + ]) + .toArray(); + + // For each subsequent month, check if location with same name already exists + const locationsToInsert = []; + for (const monthData of subsequentMonths) { + const existingLocation = await dbClient.collection("lokacije") + .findOne({ + userId, + name: locationName, + "yearMonth.year": monthData.year, + "yearMonth.month": monthData.month + }); + + // Only add if location with same name doesn't already exist in that month + if (!existingLocation) { + locationsToInsert.push({ + _id: (new ObjectId()).toHexString(), + userId, + userEmail, + name: locationName, + notes: locationNotes, + yearMonth: { year: monthData.year, month: monthData.month }, + bills: [], + }); + } + } + + // Insert all new locations at once if any + if (locationsToInsert.length > 0) { + await dbClient.collection("lokacije").insertMany(locationsToInsert); + } + } } if(yearMonth) await gotoHome(yearMonth); @@ -235,7 +311,7 @@ export const deleteLocationById = withUser(async (user:AuthenticatedUser, locati const { id: userId } = user; // find a location with the given locationID - const post = await dbClient.collection("lokacije").deleteOne({ _id: locationID, userId }); + await dbClient.collection("lokacije").deleteOne({ _id: locationID, userId }); await gotoHome(yearMonth) }) \ No newline at end of file diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 501c647..b63a207 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -7,17 +7,31 @@ import { defaultLocale } from '../i18n'; export const myAuth = () => { - // Ovo koristim u developmentu + /** - // const session:Session = { - // user: { - // id: "109754742613069927799", - // name: "Nikola Derežić", - // }, - // expires: "123", - // }; + Google auth does not work in development environment + - this is a hack to make it work in development environment + - it returns a fake session object which is used by the Next-Auth middleware + + Instructions: when in dev environment, uncomment the following code snippet + - this will return a fake session object which is used by the Next-Auth middleware + - when in production environment, comment the code snippet back + + Note: this is not a secure way to handle authentication, it is only for development purposes + - in production environment, the auth should be handled by the Next-Auth middleware + + Code snippet: - // return(Promise.resolve(session)); + const session:Session = { + user: { + id: "109754742613069927799", + name: "Nikola Derežić", + }, + expires: "123", + }; + + return(Promise.resolve(session)); + */ return(auth()); } diff --git a/app/ui/LocationEditForm.tsx b/app/ui/LocationEditForm.tsx index a64ea24..b3020f9 100644 --- a/app/ui/LocationEditForm.tsx +++ b/app/ui/LocationEditForm.tsx @@ -60,6 +60,16 @@ export const LocationEditForm:FC = ({ location, yearMonth ))} + {/* Show toggle only when adding a new location (not editing) */} + {!location && ( +
+ +
+ )} +
{ state.message && diff --git a/messages/en.json b/messages/en.json index 98edd7c..caa76ac 100644 --- a/messages/en.json +++ b/messages/en.json @@ -98,6 +98,7 @@ "save-button": "Save", "cancel-button": "Cancel", "delete-tooltip": "Delete realestate", + "add-to-subsequent-months": "Add to all subsequent months", "validation": { "location-name-required": "Relaestate name is required" } diff --git a/messages/hr.json b/messages/hr.json index cdc916b..41c8fb4 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -97,6 +97,7 @@ "save-button": "Spremi", "cancel-button": "Odbaci", "delete-tooltip": "Brisanje nekretnine", + "add-to-subsequent-months": "Dodaj u sve mjesece koji slijede", "validation": { "location-name-required": "Ime nekretnine je obavezno" } From 2cf338c50aad14eb6b51437b7d87c920997ef1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Mon, 11 Aug 2025 10:47:55 +0200 Subject: [PATCH 04/12] improve development authentication setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replaced manual code commenting with environment variable controlled mock auth - Added development env vars to VS Code launch.json for automatic mock authentication - Removed unused LinkedIn provider import - Authentication now automatically uses mock session when launched via VS Code debug - Zero security impact on production (env vars only exist during debug sessions) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .vscode/launch.json | 5 +++++ app/lib/auth.ts | 42 ++++++++++++++---------------------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 41839db..2772a38 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,6 +9,11 @@ "type": "node", "request": "launch", "envFile": "${workspaceFolder}/.env", + "env": { + "USE_MOCK_AUTH": "true", + "MOCK_USER_ID": "109754742613069927799", + "MOCK_USER_NAME": "Nikola Derežić" + }, "runtimeArgs": [ "run", // this is `run` from `npm run` "dev" // this is `dev` from `npm run dev` diff --git a/app/lib/auth.ts b/app/lib/auth.ts index b63a207..63e1fd2 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -1,39 +1,25 @@ import NextAuth, { NextAuthConfig } from 'next-auth'; import GoogleProvider from 'next-auth/providers/google'; -import LinkedinProvider from 'next-auth/providers/linkedin'; +// import LinkedinProvider from 'next-auth/providers/linkedin'; import { Session } from 'next-auth'; import { AuthenticatedUser } from './types/next-auth'; import { defaultLocale } from '../i18n'; export const myAuth = () => { + // Use mock authentication in development when enabled via environment variable + if (process.env.NODE_ENV === 'development' && process.env.USE_MOCK_AUTH === 'true') { + const session: Session = { + user: { + id: process.env.MOCK_USER_ID || "109754742613069927799", + name: process.env.MOCK_USER_NAME || "Nikola Derežić", + }, + expires: "123", + }; + + return Promise.resolve(session); + } - /** - - Google auth does not work in development environment - - this is a hack to make it work in development environment - - it returns a fake session object which is used by the Next-Auth middleware - - Instructions: when in dev environment, uncomment the following code snippet - - this will return a fake session object which is used by the Next-Auth middleware - - when in production environment, comment the code snippet back - - Note: this is not a secure way to handle authentication, it is only for development purposes - - in production environment, the auth should be handled by the Next-Auth middleware - - Code snippet: - - const session:Session = { - user: { - id: "109754742613069927799", - name: "Nikola Derežić", - }, - expires: "123", - }; - - return(Promise.resolve(session)); - */ - - return(auth()); + return auth(); } export const authConfig: NextAuthConfig = { From 1eac116a5533d3df1c19c688c6c1abeb48fb937f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Mon, 11 Aug 2025 11:01:16 +0200 Subject: [PATCH 05/12] add option to delete location in all subsequent months MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added toggle in LocationDeleteForm for deleting locations from future months (disabled by default) - Modified deleteLocationById action to support batch deletion across subsequent months - When enabled, deletes all locations with same name in current and future months/years - Only deletes user's own locations with proper data isolation - Added translations for toggle text in Croatian and English - Removed unused imports and variables to fix TypeScript warnings - Uses efficient MongoDB deleteMany operation for bulk deletions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/lib/actions/locationActions.ts | 35 ++++++++++++++++++++++++++---- app/ui/LocationDeleteForm.tsx | 15 +++++++------ messages/en.json | 5 +++-- messages/hr.json | 5 +++-- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/app/lib/actions/locationActions.ts b/app/lib/actions/locationActions.ts index d1b29ed..88f21e4 100644 --- a/app/lib/actions/locationActions.ts +++ b/app/lib/actions/locationActions.ts @@ -302,7 +302,7 @@ export const fetchLocationById = async (locationID:string) => { return(billLocation); }; -export const deleteLocationById = withUser(async (user:AuthenticatedUser, locationID:string, yearMonth:YearMonth) => { +export const deleteLocationById = withUser(async (user:AuthenticatedUser, locationID:string, yearMonth:YearMonth, _prevState:any, formData: FormData) => { noStore(); @@ -310,8 +310,35 @@ export const deleteLocationById = withUser(async (user:AuthenticatedUser, locati const { id: userId } = user; - // find a location with the given locationID - await dbClient.collection("lokacije").deleteOne({ _id: locationID, userId }); + const deleteInSubsequentMonths = formData.get('deleteInSubsequentMonths') === 'on'; + + if (deleteInSubsequentMonths) { + // Get the location name first to find all locations with the same name + const location = await dbClient.collection("lokacije") + .findOne({ _id: locationID, userId }); + + if (location) { + // Delete all locations with the same name in current and subsequent months + await dbClient.collection("lokacije").deleteMany({ + userId, + name: location.name, + $or: [ + { "yearMonth.year": { $gt: yearMonth.year } }, + { + "yearMonth.year": yearMonth.year, + "yearMonth.month": { $gte: yearMonth.month } + } + ] + }); + } + } else { + // Delete only the specific location (current behavior) + await dbClient.collection("lokacije").deleteOne({ _id: locationID, userId }); + } - await gotoHome(yearMonth) + await gotoHome(yearMonth); + + return { + message: null + }; }) \ No newline at end of file diff --git a/app/ui/LocationDeleteForm.tsx b/app/ui/LocationDeleteForm.tsx index 0524aa9..1fc694b 100644 --- a/app/ui/LocationDeleteForm.tsx +++ b/app/ui/LocationDeleteForm.tsx @@ -4,7 +4,6 @@ import { FC, ReactNode } from "react"; import { BillingLocation } from "../lib/db-types"; import { deleteLocationById } from "../lib/actions/locationActions"; import { useFormState } from "react-dom"; -import { gotoUrl } from "../lib/actions/navigationActions"; import Link from "next/link"; import { useTranslations } from "next-intl"; @@ -16,14 +15,9 @@ export interface LocationDeleteFormProps { export const LocationDeleteForm:FC = ({ location }) => { const handleAction = deleteLocationById.bind(null, location._id, location.yearMonth); - const [ state, dispatch ] = useFormState(handleAction, null); + const [ , dispatch ] = useFormState(handleAction, null); const t = useTranslations("location-delete-form"); - - const handleCancel = () => { - gotoUrl(`/location/${location._id}/edit/`); - }; - return(
@@ -36,6 +30,13 @@ export const LocationDeleteForm:FC = ({ location }) => }) }

+ +
+ +
{t("cancel-button")} diff --git a/messages/en.json b/messages/en.json index caa76ac..7c61f48 100644 --- a/messages/en.json +++ b/messages/en.json @@ -88,9 +88,10 @@ "back-button": "Back" }, "location-delete-form": { - "text": "Please confirm deletion of realestate “{name}””.", + "text": "Please confirm deletion of realestate “{name}“.", "cancel-button": "Cancel", - "confirm-button": "Confirm" + "confirm-button": "Confirm", + "delete-in-subsequent-months": "Delete in all subsequent months" }, "location-edit-form": { "location-name-placeholder": "Realestate name", diff --git a/messages/hr.json b/messages/hr.json index 41c8fb4..b4e7bec 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -87,9 +87,10 @@ "back-button": "Nazad" }, "location-delete-form": { - "text": "Molim potvrdi brisanje nekretnine “{name}””.", + "text": "Molim potvrdi brisanje nekretnine “{name}“.", "cancel-button": "Odustani", - "confirm-button": "Potvrdi" + "confirm-button": "Potvrdi", + "delete-in-subsequent-months": "Obriši u svim mjesecima koji slijede" }, "location-edit-form": { "location-name-placeholder": "Ime nekretnine", From 131dfe793b2e3eed9f56ad6d26365aefa2423917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Mon, 11 Aug 2025 11:05:37 +0200 Subject: [PATCH 06/12] improve LocationDeleteForm toggle layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Center the delete toggle horizontally within the form using justify-center - Add consistent spacing between label and toggle input with gap-4 - Improves visual balance and readability of the delete confirmation form 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/ui/LocationDeleteForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/LocationDeleteForm.tsx b/app/ui/LocationDeleteForm.tsx index 1fc694b..0347b28 100644 --- a/app/ui/LocationDeleteForm.tsx +++ b/app/ui/LocationDeleteForm.tsx @@ -32,7 +32,7 @@ export const LocationDeleteForm:FC = ({ location }) =>

-