From 9ae023cc94cb55409a97ca67feb18d06bb6c04c5 Mon Sep 17 00:00:00 2001 From: Knee Cola Date: Tue, 18 Nov 2025 08:23:08 +0100 Subject: [PATCH] Add tenant information fields to LocationEditForm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added optional tenant fields (first name, last name, email) to billing locations with a toggle to enable/disable 2D barcode generation for tenants. Changes: - Added generateTenantCode, tenantFirstName, tenantLastName, and tenantEmail fields to BillingLocation interface - Updated LocationEditForm with toggle control and conditional tenant fields - Implemented conditional validation: tenant names required when generateTenantCode is true - Updated updateOrAddLocation action to persist tenant data across all update operations - Added localization strings for tenant fields and validation messages (Croatian/English) The generateTenantCode flag is persisted in the database and controls visibility of tenant name fields. When enabled, both first and last names become mandatory. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/lib/actions/locationActions.ts | 67 +++++++++++-- app/lib/db-types.ts | 8 ++ app/ui/LocationEditForm.tsx | 152 ++++++++++++++++++++++++----- messages/en.json | 13 ++- messages/hr.json | 13 ++- 5 files changed, 219 insertions(+), 34 deletions(-) diff --git a/app/lib/actions/locationActions.ts b/app/lib/actions/locationActions.ts index b8c956e..2c10ffa 100644 --- a/app/lib/actions/locationActions.ts +++ b/app/lib/actions/locationActions.ts @@ -14,7 +14,11 @@ import { getTranslations, getLocale } from "next-intl/server"; export type State = { errors?: { locationName?: string[]; - locationNotes?: string[], + locationNotes?: string[]; + generateTenantCode?: string[]; + tenantFirstName?: string[]; + tenantLastName?: string[]; + tenantEmail?: string[]; }; message?:string | null; }; @@ -27,11 +31,34 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ _id: z.string(), locationName: z.coerce.string().min(1, t("location-name-required")), locationNotes: z.string(), + generateTenantCode: z.boolean().optional().nullable(), + tenantFirstName: z.string().optional().nullable(), + tenantLastName: z.string().optional().nullable(), + tenantEmail: z.string().optional().nullable(), addToSubsequentMonths: z.boolean().optional().nullable(), updateScope: z.enum(["current", "subsequent", "all"]).optional().nullable(), }) // dont include the _id field in the response - .omit({ _id: true }); + .omit({ _id: true }) + // Add conditional validation: if generateTenantCode is true, tenant names are required + .refine((data) => { + if (data.generateTenantCode) { + return !!data.tenantFirstName && data.tenantFirstName.trim().length > 0; + } + return true; + }, { + message: t("tenant-first-name-required"), + path: ["tenantFirstName"], + }) + .refine((data) => { + if (data.generateTenantCode) { + return !!data.tenantLastName && data.tenantLastName.trim().length > 0; + } + return true; + }, { + message: t("tenant-last-name-required"), + path: ["tenantLastName"], + }); /** * Server-side action which adds or updates a bill @@ -49,6 +76,10 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat const validatedFields = FormSchema(t).safeParse({ locationName: formData.get('locationName'), locationNotes: formData.get('locationNotes'), + generateTenantCode: formData.get('generateTenantCode') === 'on', + tenantFirstName: formData.get('tenantFirstName') || null, + tenantLastName: formData.get('tenantLastName') || null, + tenantEmail: formData.get('tenantEmail') || null, addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on', updateScope: formData.get('updateScope') as "current" | "subsequent" | "all" | undefined, }); @@ -57,13 +88,17 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat if(!validatedFields.success) { return({ errors: validatedFields.error.flatten().fieldErrors, - message: "Missing Fields", + message: t("validation-failed"), }); } const { locationName, locationNotes, + generateTenantCode, + tenantFirstName, + tenantLastName, + tenantEmail, addToSubsequentMonths, updateScope, } = validatedFields.data; @@ -97,6 +132,10 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat $set: { name: locationName, notes: locationNotes, + generateTenantCode: generateTenantCode || false, + tenantFirstName: tenantFirstName || null, + tenantLastName: tenantLastName || null, + tenantEmail: tenantEmail || null, } } ); @@ -108,9 +147,9 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat name: currentLocation.name, $or: [ { "yearMonth.year": { $gt: currentLocation.yearMonth.year } }, - { - "yearMonth.year": currentLocation.yearMonth.year, - "yearMonth.month": { $gte: currentLocation.yearMonth.month } + { + "yearMonth.year": currentLocation.yearMonth.year, + "yearMonth.month": { $gte: currentLocation.yearMonth.month } } ] }, @@ -118,6 +157,10 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat $set: { name: locationName, notes: locationNotes, + generateTenantCode: generateTenantCode || false, + tenantFirstName: tenantFirstName || null, + tenantLastName: tenantLastName || null, + tenantEmail: tenantEmail || null, } } ); @@ -132,6 +175,10 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat $set: { name: locationName, notes: locationNotes, + generateTenantCode: generateTenantCode || false, + tenantFirstName: tenantFirstName || null, + tenantLastName: tenantLastName || null, + tenantEmail: tenantEmail || null, } } ); @@ -144,6 +191,10 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat userEmail, name: locationName, notes: locationNotes, + generateTenantCode: generateTenantCode || false, + tenantFirstName: tenantFirstName || null, + tenantLastName: tenantLastName || null, + tenantEmail: tenantEmail || null, yearMonth: yearMonth, bills: [], }); @@ -208,6 +259,10 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat userEmail, name: locationName, notes: locationNotes, + generateTenantCode: generateTenantCode || false, + tenantFirstName: tenantFirstName || null, + tenantLastName: tenantLastName || null, + tenantEmail: tenantEmail || null, yearMonth: { year: monthData.year, month: monthData.month }, bills: [], }); diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index c0c7528..e920b37 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -43,6 +43,14 @@ export interface BillingLocation { bills: Bill[]; /** (optional) notes */ notes: string|null; + /** (optional) whether to generate 2D code for tenant */ + generateTenantCode?: boolean | null; + /** (optional) tenant first name */ + tenantFirstName?: string | null; + /** (optional) tenant last name */ + tenantLastName?: string | null; + /** (optional) tenant email */ + tenantEmail?: string | null; }; export enum BilledTo { diff --git a/app/ui/LocationEditForm.tsx b/app/ui/LocationEditForm.tsx index cf95c0c..6e0b3a7 100644 --- a/app/ui/LocationEditForm.tsx +++ b/app/ui/LocationEditForm.tsx @@ -1,7 +1,7 @@ "use client"; import { TrashIcon } from "@heroicons/react/24/outline"; -import { FC } from "react"; +import { FC, useState } from "react"; import { BillingLocation, YearMonth } from "../lib/db-types"; import { updateOrAddLocation } from "../lib/actions/locationActions"; import { useFormState } from "react-dom"; @@ -20,51 +20,152 @@ export type LocationEditFormProps = { yearMonth: YearMonth } -export const LocationEditForm:FC = ({ location, yearMonth }) => -{ +export const LocationEditForm: FC = ({ location, yearMonth }) => { const initialState = { message: null, errors: {} }; const handleAction = updateOrAddLocation.bind(null, location?._id, location?.yearMonth ?? yearMonth); - const [ state, dispatch ] = useFormState(handleAction, initialState); + const [state, dispatch] = useFormState(handleAction, initialState); const t = useTranslations("location-edit-form"); const locale = useLocale(); + // Track whether to generate 2D code for tenant (use persisted value from database) + const [generateTenantCode, setGenerateTenantCode] = useState( + location?.generateTenantCode ?? false + ); + + // Track tenant field values for real-time validation + const [tenantFields, setTenantFields] = useState({ + tenantFirstName: location?.tenantFirstName ?? "", + tenantLastName: location?.tenantLastName ?? "", + tenantEmail: location?.tenantEmail ?? "", + }); + + const handleTenantFieldChange = (field: keyof typeof tenantFields, value: string) => { + setTenantFields(prev => ({ ...prev, [field]: value })); + }; + let { year, month } = location ? location.yearMonth : yearMonth; - return( + return (
{ - location && - - - + location && + + + }
{state.errors?.locationName && - state.errors.locationName.map((error: string) => ( -

- {error} -

- ))} + state.errors.locationName.map((error: string) => ( +

+ {error} +

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

- {error} -

- ))} + state.errors.locationNotes.map((error: string) => ( +

+ {error} +

+ ))} +
+ +
+ +
+ + {generateTenantCode && ( + <> +
+ + handleTenantFieldChange("tenantFirstName", e.target.value)} + /> +
+ {state.errors?.tenantFirstName && + state.errors.tenantFirstName.map((error: string) => ( +

+ {error} +

+ ))} +
+
+ +
+ + handleTenantFieldChange("tenantLastName", e.target.value)} + /> +
+ {state.errors?.tenantLastName && + state.errors.tenantLastName.map((error: string) => ( +

+ {error} +

+ ))} +
+
+ + )} + +
+ + handleTenantFieldChange("tenantEmail", e.target.value)} + /> +
+ {state.errors?.tenantEmail && + state.errors.tenantEmail.map((error: string) => ( +

+ {error} +

+ ))} +
{/* Show different options for add vs edit operations */} {!location ? (
@@ -93,9 +194,9 @@ export const LocationEditForm:FC = ({ location, yearMonth
{ state.message && -

- {state.message} -

+

+ {state.message} +

}
@@ -108,9 +209,8 @@ export const LocationEditForm:FC = ({ location, yearMonth ) } -export const LocationEditFormSkeleton:FC = () => -{ - return( +export const LocationEditFormSkeleton: FC = () => { + return (
diff --git a/messages/en.json b/messages/en.json index 4cb5892..8ef72a1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -124,6 +124,14 @@ "location-edit-form": { "location-name-placeholder": "Realestate name", "notes-placeholder": "Notes", + "generate-tenant-code": "Generate 2D code for tenant", + "tenant-first-name-label": "Tenant First Name", + "tenant-first-name-placeholder": "Enter tenant's first name", + "tenant-last-name-label": "Tenant Last Name", + "tenant-last-name-placeholder": "Enter tenant's last name", + "tenant-email-label": "Tenant Email", + "tenant-email-placeholder": "Enter tenant's email", + "warning-missing-tenant-names": "Warning: Tenant first and last name are missing. The 2D barcode will not be displayed to the tenant when they open the shared link until both fields are filled in.", "save-button": "Save", "cancel-button": "Cancel", "delete-tooltip": "Delete realestate", @@ -133,7 +141,10 @@ "update-subsequent-months": "current and all future months", "update-all-months": "all months", "validation": { - "location-name-required": "Relaestate name is required" + "location-name-required": "Relaestate name is required", + "tenant-first-name-required": "tenant first name is missing", + "tenant-last-name-required": "tenant last name is missing", + "validation-failed": "Validation failed. Please check the form and try again." } }, "account-form": { diff --git a/messages/hr.json b/messages/hr.json index abca82d..3bb18be 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -123,6 +123,14 @@ "location-edit-form": { "location-name-placeholder": "Ime nekretnine", "notes-placeholder": "Bilješke", + "generate-tenant-code": "Generiraj 2D barkod za podstanara", + "tenant-first-name-label": "Ime podstanara", + "tenant-first-name-placeholder": "Unesite ime podstanara", + "tenant-last-name-label": "Prezime podstanara", + "tenant-last-name-placeholder": "Unesite prezime podstanara", + "tenant-email-label": "Email podstanara", + "tenant-email-placeholder": "Unesite email podstanara", + "warning-missing-tenant-names": "Upozorenje: Ime i prezime podstanara nedostaju. 2D barkod neće biti prikazan podstanaru kada otvori podijeljenu poveznicu dok oba polja ne budu popunjena.", "save-button": "Spremi", "cancel-button": "Odbaci", "delete-tooltip": "Brisanje nekretnine", @@ -132,7 +140,10 @@ "update-subsequent-months": "trenutni i svi budući mjeseci", "update-all-months": "svi mjeseci", "validation": { - "location-name-required": "Ime nekretnine je obavezno" + "location-name-required": "Ime nekretnine je obavezno", + "tenant-first-name-required": "nedostaje ime podstanara", + "tenant-last-name-required": "nedostaje prezime podstanara", + "validation-failed": "Validacija nije uspjela. Molimo provjerite formu i pokušajte ponovno." } }, "account-form": {