diff --git a/app/lib/actions.ts b/app/lib/actions.ts index 52b6278..7cbe5f3 100644 --- a/app/lib/actions.ts +++ b/app/lib/actions.ts @@ -5,6 +5,7 @@ import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; import clientPromise from './mongodb'; import { ObjectId } from 'mongodb'; +import { BillAttachment } from './db-types'; export type State = { errors?: { @@ -18,17 +19,59 @@ export type State = { const FormSchema = z.object({ _id: z.string(), billName: z.coerce.string().min(1, "Bill Name is required."), - billAttachment: z.object({ }), billNotes: z.string(), }); const UpdateBill = FormSchema.omit({ _id: true }); +/** + * 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 file contents to a base64 string + const fileContents = await billAttachment.arrayBuffer(); + const fileContentsBase64 = Buffer.from(fileContents).toString('base64'); + + // create an object to store the file in the database + return({ + fileName: decodeURIComponent(fileName), + fileSize, + fileType, + fileLastModified, + fileContentsBase64, + } as BillAttachment); +} + +/** + * Server-side action which creates a new 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 async function updateBill(locationId: string, billId:string, prevState:State, formData: FormData) { const validatedFields = UpdateBill.safeParse({ billName: formData.get('billName'), - billAttachment: formData.get('billAttachment'), billNotes: formData.get('billNotes'), }); @@ -43,7 +86,6 @@ export async function updateBill(locationId: string, billId:string, prevState:St const { billName, - billAttachment, billNotes, } = validatedFields.data; @@ -52,6 +94,22 @@ export async function updateBill(locationId: string, billId:string, prevState:St // update the bill in the mongodb const client = await clientPromise; const db = client.db("rezije"); + + const billAttachment = await serializeAttachment(formData.get('billAttachment') as File); + + // 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].name": billName, + "bills.$[elem].paid": billPaid, + "bills.$[elem].notes": billNotes, + }; + // find a location with the given locationID const post = await db.collection("lokacije").updateOne( @@ -59,12 +117,7 @@ export async function updateBill(locationId: string, billId:string, prevState:St _id: locationId // find a location with the given locationID }, { - $set: { - "bills.$[elem].name": billName, - "bills.$[elem].paid": billPaid, - "bills.$[elem].attachment": billAttachment, - "bills.$[elem].notes": billNotes, - } + $set: mongoDbSet }, { arrayFilters: [ { "elem._id": { $eq: new ObjectId(billId) } } // find a bill with the given billID @@ -75,4 +128,8 @@ export async function updateBill(locationId: string, billId:string, prevState:St revalidatePath('/'); // go to the bill list redirect('/'); +} + +export async function gotoHome() { + redirect('/'); } \ No newline at end of file diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index 9bf4350..cbf1ec2 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -1,5 +1,13 @@ import { ObjectId } from "mongodb"; +export interface BillAttachment { + fileName: string; + fileSize: number; + fileType: string; + fileLastModified: number; + fileContentsBase64: string; +}; + /** Bill basic data */ export interface LocationBase { name: string; @@ -25,7 +33,8 @@ export interface PlainLocation { export interface BillBase { name: string; paid: boolean; - document?: string|null; + attachment?: BillAttachment|null; + notes?: string|null; }; /** bill object in the form returned by MongoDB */ diff --git a/app/ui/BillEditForm.tsx b/app/ui/BillEditForm.tsx index 1c9e58e..4f91f9f 100644 --- a/app/ui/BillEditForm.tsx +++ b/app/ui/BillEditForm.tsx @@ -1,30 +1,48 @@ "use client"; -import { TrashIcon } from "@heroicons/react/24/outline"; +import { DocumentIcon, TrashIcon } from "@heroicons/react/24/outline"; import { PlainBill } from "../lib/db-types"; import { FC } from "react"; import { useFormState } from "react-dom"; -import { updateBill } from "../lib/actions"; +import { gotoHome, updateBill } from "../lib/actions"; +import { redirect } from 'next/navigation'; +// Next.js does not encode an utf-8 file name correctly when sending a form with a file attachment +// This is a workaround for that +const updateBill2 = (locationId: string, billId:string, prevState:any, formData: FormData) => { + console.log('updateBill2', formData.get('billAttachment')); + + // URL encode the file name of the attachment so it is correctly sent to the server + const billAttachment = formData.get('billAttachment') as File; + formData.set('billAttachment', billAttachment, encodeURIComponent(billAttachment.name)); + + return updateBill(locationId, billId, prevState, formData); +} export interface BillEditFormProps { invoiceID: string, bill: PlainBill } -export const BillEditForm:FC = ({ invoiceID, bill: { id, name, paid } }) => { +export const BillEditForm:FC = ({ invoiceID, bill: { id, name, paid, attachment, notes } }) => { const initialState = { message: null, errors: {} }; - const updateBillWithId = updateBill.bind(null, invoiceID, id); + const updateBillWithId = updateBill2.bind(null, invoiceID, id); const [ state, dispatch ] = useFormState(updateBillWithId, initialState); + // redirect to the main page + const handleCancel = () => { + console.log('handleCancel'); + gotoHome(); + }; + return(
- +
{state.errors?.billName && state.errors.billName.map((error: string) => ( @@ -33,13 +51,15 @@ export const BillEditForm:FC = ({ invoiceID, bill: { id, name

))}
- { // - // - // - // 2023-22-12 document GSKG račun za 2023.pdf - // + + attachment ? + + + {attachment.fileName} + + : null }
@@ -58,7 +78,7 @@ export const BillEditForm:FC = ({ invoiceID, bill: { id, name
- +
{state.errors?.billNotes && state.errors.billNotes.map((error: string) => ( @@ -76,6 +96,7 @@ export const BillEditForm:FC = ({ invoiceID, bill: { id, name }
+
);