add bulk bill deletion across subsequent months
Implement ability to delete bills from all subsequent months with toggle option and warning message similar to location deletion. Includes centered warning box and efficient bulk operations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -352,27 +352,86 @@ export const fetchBillById = async (locationID:string, billID:string, includeAtt
|
|||||||
return([billLocation, bill] as [BillingLocation, Bill]);
|
return([billLocation, bill] as [BillingLocation, Bill]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteBillById = withUser(async (user:AuthenticatedUser, locationID:string, billID:string, year:number, month:number) => {
|
export const deleteBillById = withUser(async (user:AuthenticatedUser, locationID:string, billID:string, year:number, month:number, _prevState:any, formData?: FormData) => {
|
||||||
|
|
||||||
const { id: userId } = user;
|
const { id: userId } = user;
|
||||||
|
|
||||||
const dbClient = await getDbClient();
|
const dbClient = await getDbClient();
|
||||||
|
|
||||||
// find a location with the given locationID
|
const deleteInSubsequentMonths = formData?.get('deleteInSubsequentMonths') === 'on';
|
||||||
const post = await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
|
||||||
{
|
if (deleteInSubsequentMonths) {
|
||||||
_id: locationID, // find a location with the given locationID
|
// Get the current location and bill to find the bill name and location name
|
||||||
userId // make sure that the location belongs to the user
|
const location = await dbClient.collection<BillingLocation>("lokacije")
|
||||||
},
|
.findOne({ _id: locationID, userId });
|
||||||
{
|
|
||||||
// remove the bill with the given billID
|
if (location) {
|
||||||
$pull: {
|
const bill = location.bills.find(b => b._id === billID);
|
||||||
bills: {
|
|
||||||
_id: billID
|
if (bill) {
|
||||||
|
// Find all subsequent locations with the same name that have the same bill
|
||||||
|
const subsequentLocations = await dbClient.collection<BillingLocation>("lokacije")
|
||||||
|
.find({
|
||||||
|
userId,
|
||||||
|
name: location.name,
|
||||||
|
$or: [
|
||||||
|
{ "yearMonth.year": { $gt: year } },
|
||||||
|
{
|
||||||
|
"yearMonth.year": year,
|
||||||
|
"yearMonth.month": { $gt: month }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bills.name": bill.name
|
||||||
|
}, { projection: { bills: 0 } })
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
// Delete the bill from all subsequent locations (by name)
|
||||||
|
const updateOperations = subsequentLocations.map(loc => ({
|
||||||
|
updateOne: {
|
||||||
|
filter: { _id: loc._id, userId },
|
||||||
|
update: {
|
||||||
|
$pull: {
|
||||||
|
bills: { name: bill.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Also delete from current location (by ID for precision)
|
||||||
|
updateOperations.push({
|
||||||
|
updateOne: {
|
||||||
|
filter: { _id: locationID, userId },
|
||||||
|
update: {
|
||||||
|
$pull: {
|
||||||
|
bills: { _id: billID }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute all delete operations
|
||||||
|
if (updateOperations.length > 0) {
|
||||||
|
await dbClient.collection<BillingLocation>("lokacije").bulkWrite(updateOperations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
} else {
|
||||||
|
// Delete only from current location (original behavior)
|
||||||
|
await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
||||||
|
{
|
||||||
|
_id: locationID, // find a location with the given locationID
|
||||||
|
userId // make sure that the location belongs to the user
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// remove the bill with the given billID
|
||||||
|
$pull: {
|
||||||
|
bills: {
|
||||||
|
_id: billID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await gotoHome({year, month});
|
await gotoHome({year, month});
|
||||||
return(post.modifiedCount);
|
return { message: null };
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode, useState } from "react";
|
||||||
import { Bill, BillingLocation } from "../lib/db-types";
|
import { Bill, BillingLocation } from "../lib/db-types";
|
||||||
import { useFormState } from "react-dom";
|
import { useFormState } from "react-dom";
|
||||||
import { Main } from "./Main";
|
import { Main } from "./Main";
|
||||||
@@ -19,6 +19,7 @@ export const BillDeleteForm:FC<BillDeleteFormProps> = ({ bill, location }) => {
|
|||||||
const handleAction = deleteBillById.bind(null, location._id, bill._id, year, month);
|
const handleAction = deleteBillById.bind(null, location._id, bill._id, year, month);
|
||||||
const [ state, dispatch ] = useFormState(handleAction, null);
|
const [ state, dispatch ] = useFormState(handleAction, null);
|
||||||
const t = useTranslations("bill-delete-form");
|
const t = useTranslations("bill-delete-form");
|
||||||
|
const [deleteInSubsequentMonths, setDeleteInSubsequentMonths] = useState(false);
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div className="card card-compact card-bordered min-w-[20em] max-w-[90em] bg-base-100 shadow-s my-1">
|
<div className="card card-compact card-bordered min-w-[20em] max-w-[90em] bg-base-100 shadow-s my-1">
|
||||||
@@ -33,9 +34,35 @@ export const BillDeleteForm:FC<BillDeleteFormProps> = ({ bill, location }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer justify-center gap-4">
|
||||||
|
<span className="label-text">{t("delete-in-subsequent-months")}</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="deleteInSubsequentMonths"
|
||||||
|
className="toggle toggle-error"
|
||||||
|
checked={deleteInSubsequentMonths}
|
||||||
|
onChange={(e) => setDeleteInSubsequentMonths(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{deleteInSubsequentMonths && (
|
||||||
|
<div className="border-l-4 border-error bg-error/10 p-4 mt-4 rounded-r max-w-[24rem] mx-auto">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-xl flex-shrink-0">⚠️</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-bold text-error break-words">{t("warning-title")}</h3>
|
||||||
|
<div className="text-sm text-error/80 break-words">{t("warning-message")}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="pt-4 text-center">
|
<div className="pt-4 text-center">
|
||||||
<button className="btn btn-primary">{t("confirm-button")}</button>
|
<button className="btn btn-primary w-[5.5em]">{t("confirm-button")}</button>
|
||||||
<Link className="btn btn-neutral ml-3" href={`/bill/${location._id}-${bill._id}/edit/`}>{t("cancel-button")}</Link>
|
<Link className="btn btn-neutral w-[5.5em] ml-3" href={`/bill/${location._id}-${bill._id}/edit/`}>{t("cancel-button")}</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,9 +62,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bill-delete-form": {
|
"bill-delete-form": {
|
||||||
"text": "Please confirm deletion of bill “<strong>{bill_name}</strong>” at “<strong>{location_name}</strong>”.",
|
"text": "Please confirm deletion of bill \"<strong>{bill_name}</strong>\" at \"<strong>{location_name}</strong>\".",
|
||||||
"cancel-button": "Cancel",
|
"cancel-button": "Cancel",
|
||||||
"confirm-button": "Confirm"
|
"confirm-button": "Confirm",
|
||||||
|
"delete-in-subsequent-months": "Also delete in all subsequent months",
|
||||||
|
"warning-title": "Warning",
|
||||||
|
"warning-message": "This operation cannot be undone and will delete the bill in all future months!"
|
||||||
},
|
},
|
||||||
"bill-edit-form": {
|
"bill-edit-form": {
|
||||||
"bill-name-placeholder": "Bill name",
|
"bill-name-placeholder": "Bill name",
|
||||||
|
|||||||
@@ -62,9 +62,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bill-delete-form": {
|
"bill-delete-form": {
|
||||||
"text": "Molim potvrdi brisanje računa “<strong>{bill_name}</strong>” koji pripada nekretnini “<strong>{location_name}</strong>”.",
|
"text": "Molim potvrdi brisanje računa \"<strong>{bill_name}</strong>\" koji pripada nekretnini \"<strong>{location_name}</strong>\".",
|
||||||
"cancel-button": "Odustani",
|
"cancel-button": "Odustani",
|
||||||
"confirm-button": "Potvrdi"
|
"confirm-button": "Potvrdi",
|
||||||
|
"delete-in-subsequent-months": "Također obriši i u svim mjesecima koji slijede",
|
||||||
|
"warning-title": "Upozorenje",
|
||||||
|
"warning-message": "Ova operacija je nepovratna i obrisat će račun u svim mjesecima koji slijede!"
|
||||||
},
|
},
|
||||||
"bill-edit-form": {
|
"bill-edit-form": {
|
||||||
"bill-name-placeholder": "Ime računa",
|
"bill-name-placeholder": "Ime računa",
|
||||||
|
|||||||
Reference in New Issue
Block a user