Created shared billForwardingHelpers module to avoid code duplication and implemented automatic bill forwarding trigger in the multi-bill-edit feature. Changes: - Extract shouldUpdateBillFwdStatusWhenAttached and shouldUpdateBillFwdStatusWhenPayed to new billForwardingHelpers.ts module - Update billActions.ts to import from shared module instead of local definitions - Add forwarding logic to monthActions.updateMonth to set billFwdStatus to "pending" when all tenant bills are marked as paid This ensures consistent bill forwarding behavior whether updating bills individually or in bulk via the multi-bill-edit page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
262 lines
9.0 KiB
TypeScript
262 lines
9.0 KiB
TypeScript
'use server';
|
|
|
|
import { getDbClient } from '../dbClient';
|
|
import { ObjectId } from 'mongodb';
|
|
import { Bill, BillingLocation, YearMonth } from '@evidencija-rezija/shared-code';
|
|
import { AuthenticatedUser } from '../types/next-auth';
|
|
import { withUser } from '../auth';
|
|
import { unstable_noStore as noStore, unstable_noStore, revalidatePath } from 'next/cache';
|
|
import { getLocale } from 'next-intl/server';
|
|
import { gotoHomeWithMessage } from './navigationActions';
|
|
import { shouldUpdateBillFwdStatusWhenPayed } from '../billForwardingHelpers';
|
|
|
|
/**
|
|
* Server-side action which adds a new month to the database
|
|
* @param locationId location of the bill
|
|
* @param billId ID of the bill
|
|
* @param prevState previous state of the form
|
|
* @param formData form data
|
|
* @returns
|
|
*/
|
|
export const addMonth = withUser(async (user:AuthenticatedUser, { year, month }: YearMonth) => {
|
|
noStore();
|
|
|
|
const { id: userId } = user;
|
|
|
|
// update the bill in the mongodb
|
|
const dbClient = await getDbClient();
|
|
|
|
const prevYear = month === 1 ? year - 1 : year;
|
|
const prevMonth = month === 1 ? 12 : month - 1;
|
|
|
|
// find all locations for the previous month
|
|
const prevMonthLocations = await dbClient.collection<BillingLocation>("lokacije").find({
|
|
userId, // make sure that the locations belongs to the user
|
|
yearMonth: {
|
|
year: prevYear,
|
|
month: prevMonth,
|
|
}
|
|
});
|
|
|
|
const newMonthLocationsCursor = prevMonthLocations.map((prevLocation) => {
|
|
return({
|
|
// copy all the properties from the previous location
|
|
...prevLocation,
|
|
// clear properties specific to the month
|
|
seenByTenantAt: undefined,
|
|
utilBillsProofOfPayment: undefined,
|
|
// assign a new ID
|
|
_id: (new ObjectId()).toHexString(),
|
|
yearMonth: {
|
|
year: year,
|
|
month: month,
|
|
},
|
|
// copy bill array, but set all bills to unpaid and remove attachments and notes
|
|
bills: prevLocation.bills.map((bill) => {
|
|
return {
|
|
...bill,
|
|
paid: false,
|
|
attachment: null,
|
|
notes: null,
|
|
payedAmount: null,
|
|
hub3aText: undefined,
|
|
} as Bill
|
|
})
|
|
} as BillingLocation);
|
|
});
|
|
|
|
const newMonthLocations = await newMonthLocationsCursor.toArray()
|
|
await dbClient.collection<BillingLocation>("lokacije").insertMany(newMonthLocations);
|
|
});
|
|
|
|
export const fetchAvailableYears = withUser(async (user:AuthenticatedUser) => {
|
|
noStore();
|
|
|
|
const { id: userId } = user;
|
|
|
|
const dbClient = await getDbClient();
|
|
|
|
// query mnogodb for all `yearMonth` values
|
|
const years:number[] = await dbClient.collection<BillingLocation>("lokacije")
|
|
.distinct("yearMonth.year", { userId })
|
|
|
|
// sort the years in descending order
|
|
const sortedYears = years.sort((a, b) => b - a);
|
|
|
|
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);
|
|
|
|
// Check if bill forwarding should be triggered for any locations
|
|
const forwardingCheckPromises = Object.entries(updatesByLocation).map(
|
|
async ([locationId, locationUpdates]) => {
|
|
// Check if any bills were marked as paid
|
|
const hasPaidUpdate = locationUpdates.some(update => update.paid === true);
|
|
|
|
if (!hasPaidUpdate) {
|
|
return; // Skip if no bills were marked as paid
|
|
}
|
|
|
|
// Fetch the full location data to check forwarding conditions
|
|
const location = await dbClient.collection<BillingLocation>("lokacije").findOne({
|
|
_id: locationId,
|
|
userId,
|
|
yearMonth: {
|
|
year: yearMonth.year,
|
|
month: yearMonth.month,
|
|
},
|
|
});
|
|
|
|
if (!location) {
|
|
return; // Location not found
|
|
}
|
|
|
|
// Check each bill update to see if it triggers forwarding
|
|
for (const update of locationUpdates) {
|
|
if (shouldUpdateBillFwdStatusWhenPayed(location, update.billId, update.paid)) {
|
|
// Update billFwdStatus to "pending"
|
|
await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
|
{ _id: locationId },
|
|
{ $set: { billFwdStatus: "pending" } }
|
|
);
|
|
break; // Only need to set once per location
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
await Promise.all(forwardingCheckPromises);
|
|
|
|
// 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 };
|
|
});
|