feat: add multi-bill-edit page for batch bill status updates
- Add getLocationsByMonth server action with aggregation pipeline to calculate hasAttachment - Add updateMonth server action for bulk bill status updates with path revalidation - Create multi-bill-edit page at /home/multi-bill-edit/[year]/[month] - Implement MultiBillEdit component with toggle functionality for all bills - Add BillToggleBadge component integration for consistent bill display - Add "set all as paid/unpaid" toggle button for batch operations - Implement server-side redirect with success message after save - Add Suspense boundary with loading skeleton - Update translations for multi-bill-edit feature (Croatian and English) - Ensure data freshness with unstable_noStore and revalidatePath 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,9 @@ import { ObjectId } from 'mongodb';
|
||||
import { Bill, BillingLocation, YearMonth } from '../db-types';
|
||||
import { AuthenticatedUser } from '../types/next-auth';
|
||||
import { withUser } from '../auth';
|
||||
import { unstable_noStore as noStore } from 'next/cache';
|
||||
import { unstable_noStore as noStore, unstable_noStore, revalidatePath } from 'next/cache';
|
||||
import { getLocale } from 'next-intl/server';
|
||||
import { gotoHomeWithMessage } from './navigationActions';
|
||||
|
||||
/**
|
||||
* Server-side action which adds a new month to the database
|
||||
@@ -82,3 +84,137 @@ export const fetchAvailableYears = withUser(async (user:AuthenticatedUser) => {
|
||||
|
||||
return(sortedYears);
|
||||
})
|
||||
|
||||
/**
|
||||
* Fetches all locations for a specific month for the authenticated user
|
||||
* Only projects essential fields needed for the multi-bill-edit page
|
||||
* @param yearMonth - The year and month to fetch
|
||||
* @returns Array of locations with minimal bill data
|
||||
*/
|
||||
export const getLocationsByMonth = withUser(async (user: AuthenticatedUser, yearMonth: YearMonth) => {
|
||||
|
||||
unstable_noStore();
|
||||
|
||||
const { id: userId } = user;
|
||||
const dbClient = await getDbClient();
|
||||
|
||||
// Use aggregation pipeline to calculate hasAttachment field
|
||||
const locations = await dbClient.collection<BillingLocation>("lokacije")
|
||||
.aggregate([
|
||||
{
|
||||
$match: {
|
||||
userId,
|
||||
yearMonth: {
|
||||
year: yearMonth.year,
|
||||
month: yearMonth.month,
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
_id: { $toString: "$_id" },
|
||||
bills: {
|
||||
$map: {
|
||||
input: "$bills",
|
||||
as: "bill",
|
||||
in: {
|
||||
_id: { $toString: "$$bill._id" },
|
||||
name: "$$bill.name",
|
||||
paid: "$$bill.paid",
|
||||
hasAttachment: { $ne: ["$$bill.attachment", null] },
|
||||
proofOfPayment: "$$bill.proofOfPayment",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
"_id": 1,
|
||||
"name": 1,
|
||||
"yearMonth.year": 1,
|
||||
"yearMonth.month": 1,
|
||||
"bills._id": 1,
|
||||
"bills.name": 1,
|
||||
"bills.paid": 1,
|
||||
"bills.hasAttachment": 1,
|
||||
"bills.proofOfPayment.uploadedAt": 1,
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
name: 1,
|
||||
},
|
||||
},
|
||||
])
|
||||
.toArray();
|
||||
|
||||
return locations as Array<BillingLocation>;
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the paid status of bills for locations in a specific month
|
||||
* @param yearMonth - The year and month to update
|
||||
* @param updates - Array of updates with locationId, billId, and paid status
|
||||
* @returns Success status
|
||||
*/
|
||||
export const updateMonth = withUser(async (
|
||||
user: AuthenticatedUser,
|
||||
yearMonth: YearMonth,
|
||||
updates: Array<{ locationId: string; billId: string; paid: boolean }>
|
||||
) => {
|
||||
unstable_noStore();
|
||||
|
||||
const { id: userId } = user;
|
||||
const dbClient = await getDbClient();
|
||||
|
||||
// Group updates by location to minimize database operations
|
||||
const updatesByLocation = updates.reduce((acc, update) => {
|
||||
if (!acc[update.locationId]) {
|
||||
acc[update.locationId] = [];
|
||||
}
|
||||
acc[update.locationId].push(update);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof updates>);
|
||||
|
||||
// Perform bulk updates
|
||||
const updatePromises = Object.entries(updatesByLocation).map(
|
||||
async ([locationId, locationUpdates]) => {
|
||||
// For each bill update in this location
|
||||
const billUpdatePromises = locationUpdates.map(({ billId, paid }) =>
|
||||
dbClient.collection<BillingLocation>("lokacije").updateOne(
|
||||
{
|
||||
_id: locationId,
|
||||
userId, // Ensure the location belongs to the authenticated user
|
||||
yearMonth: {
|
||||
year: yearMonth.year,
|
||||
month: yearMonth.month,
|
||||
},
|
||||
'bills._id': billId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'bills.$.paid': paid,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return Promise.all(billUpdatePromises);
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Revalidate the home page and multi-edit page to show fresh data
|
||||
revalidatePath('/home');
|
||||
revalidatePath(`/home/multi-bill-edit/${yearMonth.year}/${yearMonth.month}`);
|
||||
|
||||
// Redirect to home page with year and month parameters, including success message
|
||||
if (yearMonth) {
|
||||
const locale = await getLocale();
|
||||
await gotoHomeWithMessage(locale, 'bill-multi-edit-saved', yearMonth);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user