diff --git a/src/app/protected/settings/locations/page.tsx b/src/app/protected/settings/locations/page.tsx index 9f0340c..50508f4 100644 --- a/src/app/protected/settings/locations/page.tsx +++ b/src/app/protected/settings/locations/page.tsx @@ -3,6 +3,7 @@ import { LocationTable } from "@/components/settings/locations/LocationTable" import type { Counter } from "@/generated/prisma/client" import { doesLocationCodeExist } from "@/lib/prisma/location/doesLocationCodeExist" import { getAllLocations } from "@/lib/prisma/location/getAllLocations" +import { insertLocation } from "@/lib/prisma/location/insertLocation" import { updateLocation } from "@/lib/prisma/location/updateLocation" import { getAllServices } from "@/lib/prisma/service/getAllServices" import { getAllStaffUsers } from "@/lib/prisma/staff_user/getAllStaffUsers" @@ -30,6 +31,7 @@ export default async function Page() { counters={counters} staffUsers={staffUsers} updateLocation={updateLocation} + insertLocation={insertLocation} doesLocationCodeExist={doesLocationCodeExist} revalidateTable={revalidateTable} /> diff --git a/src/components/geocoder/AddressAutocomplete.tsx b/src/components/geocoder/AddressAutocomplete.tsx index 18c8789..71487ac 100644 --- a/src/components/geocoder/AddressAutocomplete.tsx +++ b/src/components/geocoder/AddressAutocomplete.tsx @@ -87,7 +87,7 @@ export const AddressAutocomplete = ({
diff --git a/src/components/settings/locations/CreateLocationModal/CreateLocationModal.tsx b/src/components/settings/locations/CreateLocationModal/CreateLocationModal.tsx new file mode 100644 index 0000000..71a833d --- /dev/null +++ b/src/components/settings/locations/CreateLocationModal/CreateLocationModal.tsx @@ -0,0 +1,184 @@ +"use client" + +import { useEffect, useState } from "react" +import { z } from "zod" +import { + CloseButton, + DialogActions, + DialogBody, + DialogHeader, + DialogTitle, + Modal, +} from "@/components/common/dialog" +import type { Counter, StaffUser } from "@/generated/prisma/client" +import type { LocationWithRelations } from "@/lib/prisma/location/types" +import type { ServiceWithRelations } from "@/lib/prisma/service/types" +import { LocationForm } from "../LocationForm" + +type CreateLocationModalProps = { + open: boolean + onClose: () => void + services: ServiceWithRelations[] + counters: Counter[] + staffUsers: StaffUser[] + insertLocation: ( + location: Partial + ) => Promise + doesLocationCodeExist: (code: string) => Promise + revalidateTable: () => Promise +} + +export const CreateLocationModal = ({ + open, + onClose, + services, + counters, + staffUsers, + insertLocation, + doesLocationCodeExist, + revalidateTable, +}: CreateLocationModalProps) => { + const [isSaving, setIsSaving] = useState(false) + const [formData, setFormData] = useState | null>(null) + const [isFormValidState, setIsFormValidState] = useState(false) + const [isFormValidating, setIsFormValidating] = useState(false) + + // initialize form data when the modal opens + useEffect(() => { + if (open) { + setFormData({ + name: "", + code: "", + streetAddress: "", + mailAddress: null, + phoneNumber: null, + timezone: "America/Vancouver", + latitude: undefined, + longitude: undefined, + legacyOfficeNumber: null, + deletedAt: null, + services: [], + counters: [], + staffUsers: [], + }) + } else { + setFormData(null) + setIsFormValidState(false) + setIsFormValidating(false) + } + }, [open]) + + const NewLocationWithRelationsSchema = z.object({ + name: z.string().min(1, "Name is required"), + code: z + .string() + .min(1, "Code is required") + .refine( + async (code) => { + return !(await doesLocationCodeExist(code)) + }, + { message: "Code already exists" } + ), + streetAddress: z.string(), + mailAddress: z.string().nullable(), + timezone: z.string(), + phoneNumber: z + .string() + .nullable() + .refine( + (phone) => { + // Allow null or empty string (optional field) + if (!phone || phone.trim() === "") return true + // Must contain at least 10 digits for a valid phone number + const digits = phone.replace(/\D/g, "") + return digits.length >= 10 + }, + { message: "Phone number must contain at least 10 digits" } + ), + latitude: z.number(), + longitude: z.number(), + legacyOfficeNumber: z.number().nullable(), + services: z.array(z.any()), + counters: z.array(z.any()), + staffUsers: z.array(z.any()), + }) + + // Validate formData asynchronously and update local state instead of calling async validators during render + // biome-ignore lint/correctness/useExhaustiveDependencies: <> + useEffect(() => { + if (!formData) { + setIsFormValidState(false) + setIsFormValidating(false) + return + } + + let active = true + setIsFormValidating(true) + + NewLocationWithRelationsSchema.parseAsync(formData) + .then(() => { + if (active) setIsFormValidState(true) + }) + .catch(() => { + if (active) setIsFormValidState(false) + }) + .finally(() => { + if (active) setIsFormValidating(false) + }) + + return () => { + active = false + } + }, [formData, doesLocationCodeExist]) + + if (!formData) return null + + const isArchived = formData.deletedAt !== null + const isReadonly = isArchived + + const handleSave = async () => { + if (formData && !isReadonly) { + setIsSaving(true) + await insertLocation(formData) + await revalidateTable() + onClose() + setIsSaving(false) + } + } + + return ( + + }> + Create Location + + + +
+ + +
+ + + + + +
+ ) +} diff --git a/src/components/settings/locations/CreateLocationModal/index.ts b/src/components/settings/locations/CreateLocationModal/index.ts new file mode 100644 index 0000000..29dd8aa --- /dev/null +++ b/src/components/settings/locations/CreateLocationModal/index.ts @@ -0,0 +1 @@ +export * from "./CreateLocationModal" diff --git a/src/components/settings/locations/EditLocationModal/EditLocationModal.tsx b/src/components/settings/locations/EditLocationModal/EditLocationModal.tsx index b4cc43b..630197c 100644 --- a/src/components/settings/locations/EditLocationModal/EditLocationModal.tsx +++ b/src/components/settings/locations/EditLocationModal/EditLocationModal.tsx @@ -63,6 +63,24 @@ export const EditLocationModal = ({ }, { message: "Code already exists" } ), + streetAddress: z.string(), + mailAddress: z.string().nullable(), + phoneNumber: z + .string() + .nullable() + .refine( + (phone) => { + // Allow null or empty string (optional field) + if (!phone || phone.trim() === "") return true + // Must contain at least 10 digits for a valid phone number + const digits = phone.replace(/\D/g, "") + return digits.length >= 10 + }, + { message: "Phone number must contain at least 10 digits" } + ), + timezone: z.string(), + latitude: z.number(), + longitude: z.number(), legacyOfficeNumber: z.number().nullable(), deletedAt: z.date().nullable(), createdAt: z.date(), diff --git a/src/components/settings/locations/LocationForm.tsx b/src/components/settings/locations/LocationForm.tsx index 0cb0eb0..1c50d25 100644 --- a/src/components/settings/locations/LocationForm.tsx +++ b/src/components/settings/locations/LocationForm.tsx @@ -131,6 +131,7 @@ export const LocationForm = ({ { // Update the form with the selected address information @@ -156,7 +157,6 @@ export const LocationForm = ({ value={location.latitude?.toString() || ""} onChange={() => {}} disabled - className="flex-1" /> {}} disabled - className="flex-1" /> , prevLocation: Partial ) => Promise + insertLocation: ( + location: Partial + ) => Promise doesLocationCodeExist: (code: string) => Promise revalidateTable: () => Promise } @@ -29,6 +33,7 @@ export const LocationTable = ({ counters, staffUsers, updateLocation, + insertLocation, doesLocationCodeExist, revalidateTable, }: LocationTableProps) => { @@ -37,6 +42,11 @@ export const LocationTable = ({ openDialog: openEditLocationModal, closeDialog: closeEditLocationModal, } = useDialog() + const { + open: createLocationModalOpen, + openDialog: openCreateLocationModal, + closeDialog: closeCreateLocationModal, + } = useDialog() const [showArchived, setShowArchived] = useState(false) const [selectedLocation, setSelectedLocation] = useState(null) @@ -55,7 +65,7 @@ export const LocationTable = ({

Show Archived

-
@@ -85,6 +95,16 @@ export const LocationTable = ({ doesLocationCodeExist={doesLocationCodeExist} revalidateTable={revalidateTable} /> + ) }