add bulk bill creation across subsequent months
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 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({
|
|||||||
_id: z.string(),
|
_id: z.string(),
|
||||||
billName: z.coerce.string().min(1, t("bill-name-required")),
|
billName: z.coerce.string().min(1, t("bill-name-required")),
|
||||||
billNotes: z.string(),
|
billNotes: z.string(),
|
||||||
|
addToSubsequentMonths: z.boolean().optional(),
|
||||||
payedAmount: z.string().nullable().transform((val, ctx) => {
|
payedAmount: z.string().nullable().transform((val, ctx) => {
|
||||||
|
|
||||||
if(!val || val === '') {
|
if(!val || val === '') {
|
||||||
@@ -123,6 +124,7 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI
|
|||||||
.safeParse({
|
.safeParse({
|
||||||
billName: formData.get('billName'),
|
billName: formData.get('billName'),
|
||||||
billNotes: formData.get('billNotes'),
|
billNotes: formData.get('billNotes'),
|
||||||
|
addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on',
|
||||||
payedAmount: formData.get('payedAmount'),
|
payedAmount: formData.get('payedAmount'),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,6 +140,7 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI
|
|||||||
const {
|
const {
|
||||||
billName,
|
billName,
|
||||||
billNotes,
|
billNotes,
|
||||||
|
addToSubsequentMonths,
|
||||||
payedAmount,
|
payedAmount,
|
||||||
} = validatedFields.data;
|
} = validatedFields.data;
|
||||||
|
|
||||||
@@ -183,25 +186,89 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// find a location with the given locationID
|
// Create new bill - add to current location first
|
||||||
const post = await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
const newBill = {
|
||||||
|
_id: (new ObjectId()).toHexString(),
|
||||||
|
name: billName,
|
||||||
|
paid: billPaid,
|
||||||
|
attachment: billAttachment,
|
||||||
|
notes: billNotes,
|
||||||
|
payedAmount,
|
||||||
|
barcodeImage,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to current location
|
||||||
|
await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
||||||
{
|
{
|
||||||
_id: locationId, // find a location with the given locationID
|
_id: locationId, // find a location with the given locationID
|
||||||
userId // make sure that the location belongs to the user
|
userId // make sure that the location belongs to the user
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$push: {
|
$push: {
|
||||||
bills: {
|
bills: newBill
|
||||||
_id: (new ObjectId()).toHexString(),
|
|
||||||
name: billName,
|
|
||||||
paid: billPaid,
|
|
||||||
attachment: billAttachment,
|
|
||||||
notes: billNotes,
|
|
||||||
payedAmount,
|
|
||||||
barcodeImage,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("lokacije").bulkWrite(updateOperations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(billYear && billMonth ) {
|
if(billYear && billMonth ) {
|
||||||
await gotoHome({ year: billYear, month: billMonth });
|
await gotoHome({ year: billYear, month: billMonth });
|
||||||
|
|||||||
@@ -202,6 +202,16 @@ export const BillEditForm:FC<BillEditFormProps> = ({ location, bill }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Show toggle only when adding a new bill (not editing) */}
|
||||||
|
{!bill && (
|
||||||
|
<div className="form-control mt-4">
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span className="label-text">{t("add-to-subsequent-months")}</span>
|
||||||
|
<input type="checkbox" name="addToSubsequentMonths" className="toggle toggle-primary" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<button type="submit" className="btn btn-primary">{t("save-button")}</button>
|
<button type="submit" className="btn btn-primary">{t("save-button")}</button>
|
||||||
<Link className="btn btn-neutral ml-3" href={`/?year=${billYear}&month=${billMonth}`}>{t("cancel-button")}</Link>
|
<Link className="btn btn-neutral ml-3" href={`/?year=${billYear}&month=${billMonth}`}>{t("cancel-button")}</Link>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
"save-button": "Save",
|
"save-button": "Save",
|
||||||
"cancel-button": "Cancel",
|
"cancel-button": "Cancel",
|
||||||
"delete-tooltip": "Delete bill",
|
"delete-tooltip": "Delete bill",
|
||||||
|
"add-to-subsequent-months": "Add to all subsequent months",
|
||||||
"validation": {
|
"validation": {
|
||||||
"bill-name-required": "Bill name is required",
|
"bill-name-required": "Bill name is required",
|
||||||
"payed-amount-required": "Payed amount is required",
|
"payed-amount-required": "Payed amount is required",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
"save-button": "Spremi",
|
"save-button": "Spremi",
|
||||||
"cancel-button": "Odbaci",
|
"cancel-button": "Odbaci",
|
||||||
"delete-tooltip": "Obriši račun",
|
"delete-tooltip": "Obriši račun",
|
||||||
|
"add-to-subsequent-months": "Dodaj u sve mjesece koji slijede",
|
||||||
"validation": {
|
"validation": {
|
||||||
"bill-name-required": "Ime računa je obavezno",
|
"bill-name-required": "Ime računa je obavezno",
|
||||||
"not-a-number": "Vrijednost mora biti brojka",
|
"not-a-number": "Vrijednost mora biti brojka",
|
||||||
|
|||||||
Reference in New Issue
Block a user