'use server'; import { z } from 'zod'; import { getDbClient } from '../dbClient'; import { BillingLocation, YearMonth } from '../db-types'; import { ObjectId } from 'mongodb'; import { withUser } from '@/app/lib/auth'; import { AuthenticatedUser } from '../types/next-auth'; import { gotoHome } from './navigationActions'; import { unstable_noStore as noStore } from 'next/cache'; import { IntlTemplateFn } from '@/app/i18n'; import { getTranslations } from "next-intl/server"; export type State = { errors?: { locationName?: string[]; locationNotes?: string[], }; message?:string | null; }; /** * Schema for validating location form fields * @description this is defined as factory function so that it can be used with the next-intl library */ const FormSchema = (t:IntlTemplateFn) => z.object({ _id: z.string(), locationName: z.coerce.string().min(1, t("location-name-required")), locationNotes: z.string(), addToSubsequentMonths: z.boolean().optional(), updateScope: z.enum(["current", "subsequent", "all"]).optional(), }) // dont include the _id field in the response .omit({ _id: true }); /** * Server-side action which adds or updates a bill * @param locationId location of the bill * @param prevState previous state of the form * @param formData form data * @returns */ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locationId: string | undefined, yearMonth: YearMonth | undefined, prevState:State, formData: FormData) => { noStore(); const t = await getTranslations("location-edit-form.validation"); const validatedFields = FormSchema(t).safeParse({ locationName: formData.get('locationName'), locationNotes: formData.get('locationNotes'), addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on', updateScope: formData.get('updateScope') as "current" | "subsequent" | "all" | undefined, }); // If form validation fails, return errors early. Otherwise, continue... if(!validatedFields.success) { return({ errors: validatedFields.error.flatten().fieldErrors, message: "Missing Fields", }); } const { locationName, locationNotes, addToSubsequentMonths, updateScope, } = validatedFields.data; // update the bill in the mongodb const dbClient = await getDbClient(); const { id: userId, email: userEmail } = user; if(locationId) { // Get the current location first to find its name const currentLocation = await dbClient.collection("lokacije") .findOne({ _id: locationId, userId }, { projection: { bills: 0 } }); if (!currentLocation) { return { message: "Location not found", errors: undefined, }; } // Handle different update scopes if (updateScope === "current" || !updateScope) { // Update only the current location (default behavior) await dbClient.collection("lokacije").updateOne( { _id: locationId, userId }, { $set: { name: locationName, notes: locationNotes, } } ); } else if (updateScope === "subsequent") { // Update current and all subsequent months await dbClient.collection("lokacije").updateMany( { userId, name: currentLocation.name, $or: [ { "yearMonth.year": { $gt: currentLocation.yearMonth.year } }, { "yearMonth.year": currentLocation.yearMonth.year, "yearMonth.month": { $gte: currentLocation.yearMonth.month } } ] }, { $set: { name: locationName, notes: locationNotes, } } ); } else if (updateScope === "all") { // Update all locations with the same name across all months await dbClient.collection("lokacije").updateMany( { userId, name: currentLocation.name }, { $set: { name: locationName, notes: locationNotes, } } ); } } else if(yearMonth) { // Always add location to the specified month await dbClient.collection("lokacije").insertOne({ _id: (new ObjectId()).toHexString(), userId, userEmail, name: locationName, notes: locationNotes, yearMonth: yearMonth, bills: [], }); // If addToSubsequentMonths is enabled, add to all subsequent months if (addToSubsequentMonths) { // Find all subsequent months that exist in the database const subsequentMonths = await dbClient.collection("lokacije") .aggregate([ { $match: { userId, $or: [ { "yearMonth.year": { $gt: yearMonth.year } }, { "yearMonth.year": yearMonth.year, "yearMonth.month": { $gt: yearMonth.month } } ] } }, { $group: { _id: { year: "$yearMonth.year", month: "$yearMonth.month" } } }, { $project: { _id: 0, year: "$_id.year", month: "$_id.month" } }, { $sort: { year: 1, month: 1 } } ]) .toArray(); // For each subsequent month, check if location with same name already exists const locationsToInsert = []; for (const monthData of subsequentMonths) { const existingLocation = await dbClient.collection("lokacije") .findOne({ userId, name: locationName, "yearMonth.year": monthData.year, "yearMonth.month": monthData.month }, { projection: { bills: 0 } }); // Only add if location with same name doesn't already exist in that month if (!existingLocation) { locationsToInsert.push({ _id: (new ObjectId()).toHexString(), userId, userEmail, name: locationName, notes: locationNotes, yearMonth: { year: monthData.year, month: monthData.month }, bills: [], }); } } // Insert all new locations at once if any if (locationsToInsert.length > 0) { await dbClient.collection("lokacije").insertMany(locationsToInsert); } } } if(yearMonth) await gotoHome(yearMonth); return { message: null, errors: undefined, }; }); export const fetchAllLocations = withUser(async (user:AuthenticatedUser, year:number) => { noStore(); const dbClient = await getDbClient(); const { id: userId } = user; // fetch all locations for the given year const locations = await dbClient.collection("lokacije") .aggregate([ { $match: { userId, "yearMonth.year": year, }, }, { $addFields: { bills: { $map: { input: "$bills", as: "bill", in: { _id: "$$bill._id", name: "$$bill.name", paid: "$$bill.paid", payedAmount: "$$bill.payedAmount", hasAttachment: { $ne: ["$$bill.attachment", null] }, }, }, }, }, }, { $project: { "_id": 1, // "userId": 0, // "userEmail": 0, "name": 1, // "notes": 0, // "yearMonth": 1, "yearMonth.year": 1, "yearMonth.month": 1, // "bills": 1, "bills._id": 1, "bills.name": 1, "bills.paid": 1, "bills.payedAmount": 1, "bills.hasAttachment": 1, // "bills.attachment": 0, // "bills.notes": 0, // "bills.barcodeImage": 1, }, }, { $sort: { "yearMonth.year": -1, "yearMonth.month": -1, name: 1, }, }, ]) .toArray(); return(locations) }) /* ova metoda je zamijenjena sa jednostavnijom `fetchLocationById`, koja brže radi jer ne provjerava korisnika export const fetchLocationByUserAndId = withUser(async (user:AuthenticatedUser, locationID:string) => { noStore(); const dbClient = await getDbClient(); const { id: userId } = user; // find a location with the given locationID const billLocation = await dbClient.collection("lokacije") .findOne( { _id: locationID, userId }, { projection: { // don't include the attachment binary data in the response "bills.attachment.fileContentsBase64": 0, }, } ); if(!billLocation) { console.log(`Location ${locationID} not found`); return(null); } return(billLocation); }); */ export const fetchLocationById = async (locationID:string) => { noStore(); const dbClient = await getDbClient(); // find a location with the given locationID const billLocation = await dbClient.collection("lokacije") .findOne( { _id: locationID }, { projection: { // don't include the attachment binary data in the response "bills.attachment.fileContentsBase64": 0, }, } ); if(!billLocation) { console.log(`Location ${locationID} not found`); return(null); } return(billLocation); }; export const deleteLocationById = withUser(async (user:AuthenticatedUser, locationID:string, yearMonth:YearMonth, _prevState:any, formData: FormData) => { noStore(); const dbClient = await getDbClient(); const { id: userId } = user; const deleteInSubsequentMonths = formData.get('deleteInSubsequentMonths') === 'on'; if (deleteInSubsequentMonths) { // Get the location name first to find all locations with the same name const location = await dbClient.collection("lokacije") .findOne({ _id: locationID, userId }, { projection: { bills: 0 } }); if (location) { // Delete all locations with the same name in current and subsequent months await dbClient.collection("lokacije").deleteMany({ userId, name: location.name, $or: [ { "yearMonth.year": { $gt: yearMonth.year } }, { "yearMonth.year": yearMonth.year, "yearMonth.month": { $gte: yearMonth.month } } ] }); } } else { // Delete only the specific location (current behavior) await dbClient.collection("lokacije").deleteOne({ _id: locationID, userId }); } await gotoHome(yearMonth); return { message: null }; })