From 71895229eca40017d43af627cdd25bf5c8d1f6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Mon, 11 Aug 2025 12:01:18 +0200 Subject: [PATCH] add bulk bill creation across subsequent months MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement ability to add newly created bills to all subsequent months with toggle option (disabled by default). Includes duplicate checking and efficient bulk operations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/lib/actions/billActions.ts | 89 +++++++++++++++++++++++++++++----- app/ui/BillEditForm.tsx | 10 ++++ messages/en.json | 1 + messages/hr.json | 1 + 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/app/lib/actions/billActions.ts b/app/lib/actions/billActions.ts index 757999b..1bb5c15 100644 --- a/app/lib/actions/billActions.ts +++ b/app/lib/actions/billActions.ts @@ -28,6 +28,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ _id: z.string(), billName: z.coerce.string().min(1, t("bill-name-required")), billNotes: z.string(), + addToSubsequentMonths: z.boolean().optional(), payedAmount: z.string().nullable().transform((val, ctx) => { if(!val || val === '') { @@ -123,6 +124,7 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI .safeParse({ billName: formData.get('billName'), billNotes: formData.get('billNotes'), + addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on', payedAmount: formData.get('payedAmount'), }); @@ -138,6 +140,7 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI const { billName, billNotes, + addToSubsequentMonths, payedAmount, } = validatedFields.data; @@ -183,25 +186,89 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI ] }); } else { - // find a location with the given locationID - const post = await dbClient.collection("lokacije").updateOne( + // Create new bill - add to current location first + const newBill = { + _id: (new ObjectId()).toHexString(), + name: billName, + paid: billPaid, + attachment: billAttachment, + notes: billNotes, + payedAmount, + barcodeImage, + }; + + // Add to current location + await dbClient.collection("lokacije").updateOne( { _id: locationId, // find a location with the given locationID userId // make sure that the location belongs to the user }, { $push: { - bills: { - _id: (new ObjectId()).toHexString(), - name: billName, - paid: billPaid, - attachment: billAttachment, - notes: billNotes, - payedAmount, - barcodeImage, - } + bills: newBill } }); + + // If addToSubsequentMonths is enabled, add to subsequent months + if (addToSubsequentMonths && billYear && billMonth) { + // Get the current location to find its name + const currentLocation = await dbClient.collection("lokacije") + .findOne({ _id: locationId, userId }, { projection: { bills: 0 } }); + + if (currentLocation) { + // Find all subsequent months that have the same location name + const subsequentLocations = await dbClient.collection("lokacije") + .find({ + userId, + name: currentLocation.name, + $or: [ + { "yearMonth.year": { $gt: billYear } }, + { + "yearMonth.year": billYear, + "yearMonth.month": { $gt: billMonth } + } + ] + }, { projection: { bills: 0 } }) + .toArray(); + + // For each subsequent location, check if bill with same name already exists + const updateOperations = []; + for (const location of subsequentLocations) { + const existingBill = await dbClient.collection("lokacije") + .findOne({ + _id: location._id, + "bills.name": billName + }, { projection: { "bills.$": 1 } }); + + // Only add if bill with same name doesn't already exist + if (!existingBill) { + updateOperations.push({ + updateOne: { + filter: { _id: location._id, userId }, + update: { + $push: { + bills: { + _id: (new ObjectId()).toHexString(), + name: billName, + paid: false, // New bills in subsequent months are unpaid + attachment: null, // No attachment for subsequent months + notes: billNotes, + payedAmount: null, + barcodeImage: undefined, + } + } + } + } + }); + } + } + + // Execute all update operations at once if any + if (updateOperations.length > 0) { + await dbClient.collection("lokacije").bulkWrite(updateOperations); + } + } + } } if(billYear && billMonth ) { await gotoHome({ year: billYear, month: billMonth }); diff --git a/app/ui/BillEditForm.tsx b/app/ui/BillEditForm.tsx index 9f605e5..29a61a1 100644 --- a/app/ui/BillEditForm.tsx +++ b/app/ui/BillEditForm.tsx @@ -202,6 +202,16 @@ export const BillEditForm:FC = ({ location, bill }) => { ))} + {/* Show toggle only when adding a new bill (not editing) */} + {!bill && ( +
+ +
+ )} +
{t("cancel-button")} diff --git a/messages/en.json b/messages/en.json index b783ce6..341a7a0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -77,6 +77,7 @@ "save-button": "Save", "cancel-button": "Cancel", "delete-tooltip": "Delete bill", + "add-to-subsequent-months": "Add to all subsequent months", "validation": { "bill-name-required": "Bill name is required", "payed-amount-required": "Payed amount is required", diff --git a/messages/hr.json b/messages/hr.json index 05cf605..ebcb4e2 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -77,6 +77,7 @@ "save-button": "Spremi", "cancel-button": "Odbaci", "delete-tooltip": "Obriši račun", + "add-to-subsequent-months": "Dodaj u sve mjesece koji slijede", "validation": { "bill-name-required": "Ime računa je obavezno", "not-a-number": "Vrijednost mora biti brojka",