diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index a0eb685..e453422 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -15,6 +15,7 @@ export interface BillingLocation { /** the value is encoded as yyyymm (i.e. 202301) */ yearMonth: number; bills: Bill[]; + notes: string|null; }; /** Bill basic data */ diff --git a/app/lib/locationActions.ts b/app/lib/locationActions.ts new file mode 100644 index 0000000..18749ba --- /dev/null +++ b/app/lib/locationActions.ts @@ -0,0 +1,107 @@ +'use server'; + +import { z } from 'zod'; +import { revalidatePath } from 'next/cache'; +import { redirect } from 'next/navigation'; +import clientPromise from './mongodb'; +import { BillingLocation } from './db-types'; +import { ObjectId } from 'mongodb'; + +export type State = { + errors?: { + locationName?: string[]; + locationNotes?: string[], + }; + message?:string | null; +} + +const FormSchema = z.object({ + _id: z.string(), + locationName: z.coerce.string().min(1, "Location Name is required."), + locationNotes: z.string(), + }); + +const UpdateLocation = FormSchema.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 async function updateOrAddLocation(locationId?: string, prevState:State, formData: FormData) { + + const validatedFields = UpdateLocation.safeParse({ + locationName: formData.get('locationName'), + locationNotes: formData.get('locationNotes'), + }); + + // If form validation fails, return errors early. Otherwise, continue... + if(!validatedFields.success) { + return({ + errors: validatedFields.error.flatten().fieldErrors, + message: "Missing Fields", + }); + } + + const { + locationName, + locationNotes, + } = validatedFields.data; + + // update the bill in the mongodb + const client = await clientPromise; + const db = client.db("rezije"); + + if(locationId) { + await db.collection("lokacije").updateOne( + { + _id: locationId // find a location with the given locationID + }, + { + $set: { + name: locationName, + notes: locationNotes, + } + }); + } else { + await db.collection("lokacije").insertOne({ + _id: (new ObjectId()).toHexString(), + name: locationName, + notes: locationNotes, + yearMonth: 202101, // ToDo: get the current year and month + bills: [], + }); + } + + // clear the cache for the path + revalidatePath('/'); + // go to the bill list + redirect('/'); +} + +export const fetchLocationById = async (locationID:string) => { + const client = await clientPromise; + const db = client.db("rezije"); + + // find a location with the given locationID + const billLocation = await db.collection("lokacije").findOne({ _id: locationID }); + + if(!billLocation) { + console.log(`Location ${locationID} not found`); + return(null); + } + + return(billLocation); +} + +export const deleteLocationById = async (locationID:string) => { + const client = await clientPromise; + const db = client.db("rezije"); + + // find a location with the given locationID + const post = await db.collection("lokacije").deleteOne({ _id: locationID }); + + return(post.deletedCount); +} \ No newline at end of file diff --git a/app/location/[id]/edit/not-found.tsx b/app/location/[id]/edit/not-found.tsx new file mode 100644 index 0000000..6d17c02 --- /dev/null +++ b/app/location/[id]/edit/not-found.tsx @@ -0,0 +1,4 @@ +import { NotFoundPage } from '@/app/ui/NotFoundPage'; + +export default () => +; \ No newline at end of file diff --git a/app/location/[id]/edit/page.tsx b/app/location/[id]/edit/page.tsx new file mode 100644 index 0000000..d2b7360 --- /dev/null +++ b/app/location/[id]/edit/page.tsx @@ -0,0 +1,18 @@ +import { BillingLocation, Bill } from '@/app/lib/db-types'; +import { fetchBillById } from '@/app/lib/billActions'; +import clientPromise from '@/app/lib/mongodb'; +import { BillEditForm } from '@/app/ui/BillEditForm'; +import { ObjectId } from 'mongodb'; +import { notFound } from 'next/navigation'; +import { LocationEditForm } from '@/app/ui/LocationEditForm'; +import { fetchLocationById } from '@/app/lib/locationActions'; + +export default async function Page({ params:{ id } }: { params: { id:string } }) { + + const location = await fetchLocationById(id); + + if (!location) { + return(notFound()); + } + return (); +} \ No newline at end of file diff --git a/app/ui/LocationEditForm.tsx b/app/ui/LocationEditForm.tsx index 4f68d98..cc1b919 100644 --- a/app/ui/LocationEditForm.tsx +++ b/app/ui/LocationEditForm.tsx @@ -1,18 +1,76 @@ +"use client"; + import { TrashIcon } from "@heroicons/react/24/outline"; import { FC } from "react"; +import { BillingLocation } from "../lib/db-types"; +import { updateOrAddLocation } from "../lib/locationActions"; +import { useFormState } from "react-dom"; +import { gotoHome } from "../lib/billActions"; export interface LocationEditFormProps { - + /** location which should be edited */ + location?: BillingLocation } -export const LocationEditForm:FC = () => -
-
-
- - - - - -
-
\ No newline at end of file +export const LocationEditForm:FC = ({ location }) => +{ + const initialState = { message: null, errors: {} }; + const handleAction = updateOrAddLocation.bind(null, location?._id); + const [ state, dispatch ] = useFormState(handleAction, initialState); + + // redirect to the main page + const handleCancel = () => { + console.log('handleCancel'); + gotoHome(); + }; + + return( +
+
+
+
+ { + // show delete button only if location is set (otherwise it's a add operation) + location ? + + + : null + } + + +
+ {state.errors?.locationName && + state.errors.locationName.map((error: string) => ( +

+ {error} +

+ ))} +
+ + +
+ {state.errors?.locationNotes && + state.errors.locationNotes.map((error: string) => ( +

+ {error} +

+ ))} +
+ +
+ { + state.message && +

+ {state.message} +

+ } +
+ + + +
+
+
+
+ ) +}