'use server'; import { z } from 'zod'; import { getDbClient } from '../dbClient'; import { Bill, BillAttachment, 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 { Formats, TranslationValues, useTranslations } from "next-intl"; export type State = { errors?: { billName?: string[]; billAttachment?: string[], billNotes?: string[], payedAmount?: string[], }; message?:string | null; } type IntlTemplate = (key: TargetKey, values?: TranslationValues | undefined, formats?: Partial | undefined) => string; const FormSchema = (t:IntlTemplate) => z.object({ _id: z.string(), billName: z.coerce.string().min(1, t("bill-name-required")), billNotes: z.string(), payedAmount: z.string().nullable().transform((val, ctx) => { if(!val || val === '') { return null; } const parsed = parseFloat(val.replace(',', '.')); if (isNaN(parsed)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("not-a-number"), }); // This is a special symbol you can use to // return early from the transform function. // It has type `never` so it does not affect the // inferred return type. return z.NEVER; } if (parsed < 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("negative-number") }); // This is a special symbol you can use to // return early from the transform function. // It has type `never` so it does not affect the // inferred return type. return z.NEVER; } return Math.floor(parsed * 100); // value is stored in cents }), }); parseFloat const UpdateBill = ; /** * converts the file to a format stored in the database * @param billAttachment * @returns */ const serializeAttachment = async (billAttachment: File | null) => { if (!billAttachment) { return null; } const { name: fileName, size: fileSize, type: fileType, lastModified: fileLastModified, } = billAttachment; if(!fileName || fileName === 'undefined') { return null; } // convert the billAttachment file contents to format that can be stored in the database const fileContents = await billAttachment.arrayBuffer(); const fileContentsBase64 = Buffer.from(fileContents).toString('base64'); // create an object to store the file in the database return({ fileName, fileSize, fileType, fileLastModified, fileContentsBase64, } as BillAttachment); } /** * Server-side action which adds or updates a bill * @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 updateOrAddBill = withUser(async (user:AuthenticatedUser, locationId: string, billId:string|undefined, billYear:number|undefined, billMonth:number|undefined, prevState:State, formData: FormData) => { const { id: userId } = user; const t = useTranslations("bill-edit-form.validation"); // FormSchema const validatedFields = UpdateBill(t) .omit({ _id: true }) .safeParse({ billName: formData.get('billName'), billNotes: formData.get('billNotes'), payedAmount: formData.get('payedAmount'), }); // If form validation fails, return errors early. Otherwise, continue... if(!validatedFields.success) { console.log("updateBill.validation-error"); return({ errors: validatedFields.error.flatten().fieldErrors, message: t("form-error-message"), }); } const { billName, billNotes, payedAmount, } = validatedFields.data; const billPaid = formData.get('billPaid') === 'on'; const barcodeImage = formData.get('barcodeImage')?.valueOf() as string; // update the bill in the mongodb const dbClient = await getDbClient(); const billAttachment = await serializeAttachment(formData.get('billAttachment') as File); if(billId) { // if there is an attachment, update the attachment field // otherwise, do not update the attachment field const mongoDbSet = billAttachment ? { "bills.$[elem].name": billName, "bills.$[elem].paid": billPaid, "bills.$[elem].attachment": billAttachment, "bills.$[elem].notes": billNotes, "bills.$[elem].payedAmount": payedAmount, "bills.$[elem].barcodeImage": barcodeImage, }: { "bills.$[elem].name": billName, "bills.$[elem].paid": billPaid, "bills.$[elem].notes": billNotes, "bills.$[elem].payedAmount": payedAmount, "bills.$[elem].barcodeImage": barcodeImage, }; // find a location with the given locationID const post = await dbClient.collection("lokacije").updateOne( { _id: locationId, // find a location with the given locationID userId // make sure that the location belongs to the user }, { $set: mongoDbSet }, { arrayFilters: [ { "elem._id": { $eq: billId } } // find a bill with the given billID ] }); } else { // find a location with the given locationID const post = await dbClient.collection("lokacije").updateOne( { _id: locationId, // find a location with the given locationID userId // make sure that the location belongs to the user }, { $push: { bills: { _id: (new ObjectId()).toHexString(), name: billName, paid: billPaid, attachment: billAttachment, notes: billNotes, payedAmount, barcodeImage, } } }); } if(billYear && billMonth ) { await gotoHome({ year: billYear, month: billMonth }); } }) export const fetchBillById = withUser(async (user:AuthenticatedUser, locationID:string, billID:string, includeAttachmentBinary:boolean = false) => { const { id: userId } = user; const dbClient = await getDbClient(); // don't include the attachment binary data in the response // if the attachment binary data is not needed const projection = includeAttachmentBinary ? {} : { "bills.attachment.fileContentsBase64": 0, }; // find a location with the given locationID const billLocation = await dbClient.collection("lokacije").findOne( { _id: locationID, userId }, { projection }) if(!billLocation) { console.log(`Location ${locationID} not found`); return(null); } // find a bill with the given billID const bill = billLocation?.bills.find(({ _id }) => _id.toString() === billID); if(!bill) { console.log('Bill not found'); return(null); } return([billLocation, bill] as [BillingLocation, Bill]); }) export const deleteBillById = withUser(async (user:AuthenticatedUser, locationID:string, billID:string, year:number, month:number) => { const { id: userId } = user; const dbClient = await getDbClient(); // find a location with the given locationID const post = await dbClient.collection("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}); return(post.modifiedCount); });