From 3e581d88788668bd94a9234305cfd2d97e0a2953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Mon, 24 Nov 2025 15:34:59 +0100 Subject: [PATCH] refactor: replace generateTenantCode boolean with tenantPaymentMethod enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace generateTenantCode boolean field with tenantPaymentMethod enum ("none" | "iban" | "revolut") - Update LocationEditForm to use dropdown select instead of toggle for payment method selection - Consolidate multiple useState hooks into single formValues state object - Change from defaultValue to controlled components with value/onChange pattern - Add hidden inputs to preserve tenant data when payment method is not selected - Update validation logic to check tenantPaymentMethod === "iban" - Update ViewLocationCard to use new tenantPaymentMethod field - Add Croatian translations for new dropdown options This provides better scalability for adding future payment methods and improves form state management. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/lib/actions/locationActions.ts | 25 +++--- app/lib/db-types.ts | 6 +- app/ui/LocationEditForm.tsx | 132 +++++++++++++++++------------ app/ui/ViewLocationCard.tsx | 6 +- messages/hr.json | 13 ++- 5 files changed, 108 insertions(+), 74 deletions(-) diff --git a/app/lib/actions/locationActions.ts b/app/lib/actions/locationActions.ts index 369f178..cbe0e17 100644 --- a/app/lib/actions/locationActions.ts +++ b/app/lib/actions/locationActions.ts @@ -14,7 +14,6 @@ import { getTranslations, getLocale } from "next-intl/server"; export type State = { errors?: { locationName?: string[]; - generateTenantCode?: string[]; tenantName?: string[]; tenantStreet?: string[]; tenantTown?: string[]; @@ -35,7 +34,7 @@ export type State = { const FormSchema = (t:IntlTemplateFn) => z.object({ _id: z.string(), locationName: z.coerce.string().min(1, t("location-name-required")), - generateTenantCode: z.boolean().optional().nullable(), + tenantPaymentMethod: z.enum(["none", "iban", "revolut"]).optional().nullable(), tenantName: z.string().max(30).optional().nullable(), tenantStreet: z.string().max(27).optional().nullable(), tenantTown: z.string().max(27).optional().nullable(), @@ -50,9 +49,9 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ }) // dont include the _id field in the response .omit({ _id: true }) - // Add conditional validation: if generateTenantCode is true, tenant fields are required + // Add conditional validation: if `tenantPaymentMethod` is "iban", tenant fields are required .refine((data) => { - if (data.generateTenantCode) { + if (data.tenantPaymentMethod === "iban") { return !!data.tenantName && data.tenantName.trim().length > 0; } return true; @@ -61,7 +60,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ path: ["tenantName"], }) .refine((data) => { - if (data.generateTenantCode) { + if (data.tenantPaymentMethod === "iban") { return !!data.tenantStreet && data.tenantStreet.trim().length > 0; } return true; @@ -70,7 +69,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ path: ["tenantStreet"], }) .refine((data) => { - if (data.generateTenantCode) { + if (data.tenantPaymentMethod === "iban") { return !!data.tenantTown && data.tenantTown.trim().length > 0; } return true; @@ -112,7 +111,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat const validatedFields = FormSchema(t).safeParse({ locationName: formData.get('locationName'), - generateTenantCode: formData.get('generateTenantCode') === 'on', + tenantPaymentMethod: formData.get('tenantPaymentMethod') as "none" | "iban" | "revolut" | undefined, tenantName: formData.get('tenantName') || null, tenantStreet: formData.get('tenantStreet') || null, tenantTown: formData.get('tenantTown') || null, @@ -136,7 +135,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat const { locationName, - generateTenantCode, + tenantPaymentMethod, tenantName, tenantStreet, tenantTown, @@ -178,7 +177,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat { $set: { name: locationName, - generateTenantCode: generateTenantCode || false, + tenantPaymentMethod: tenantPaymentMethod || "none", tenantName: tenantName || null, tenantStreet: tenantStreet || null, tenantTown: tenantTown || null, @@ -208,7 +207,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat { $set: { name: locationName, - generateTenantCode: generateTenantCode || false, + tenantPaymentMethod: tenantPaymentMethod || "none", tenantName: tenantName || null, tenantStreet: tenantStreet || null, tenantTown: tenantTown || null, @@ -231,7 +230,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat { $set: { name: locationName, - generateTenantCode: generateTenantCode || false, + tenantPaymentMethod: tenantPaymentMethod || "none", tenantName: tenantName || null, tenantStreet: tenantStreet || null, tenantTown: tenantTown || null, @@ -253,7 +252,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat userEmail, name: locationName, notes: null, - generateTenantCode: generateTenantCode || false, + tenantPaymentMethod: tenantPaymentMethod || "none", tenantName: tenantName || null, tenantStreet: tenantStreet || null, tenantTown: tenantTown || null, @@ -327,7 +326,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat userEmail, name: locationName, notes: null, - generateTenantCode: generateTenantCode || false, + tenantPaymentMethod: tenantPaymentMethod || "none", tenantName: tenantName || null, tenantStreet: tenantStreet || null, tenantTown: tenantTown || null, diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index 7196b99..330c832 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -51,8 +51,10 @@ export interface BillingLocation { bills: Bill[]; /** (optional) notes */ notes: string|null; - /** (optional) whether to generate 2D code for tenant */ - generateTenantCode?: boolean | null; + + /** (optional) method for showing payment instructions to tenant */ + tenantPaymentMethod?: "none" | "iban" | "revolut" | null; + /** (optional) tenant name */ tenantName?: string | null; /** (optional) tenant street */ diff --git a/app/ui/LocationEditForm.tsx b/app/ui/LocationEditForm.tsx index 3267c99..8525413 100644 --- a/app/ui/LocationEditForm.tsx +++ b/app/ui/LocationEditForm.tsx @@ -23,36 +23,30 @@ export type LocationEditFormProps = { 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 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 whether to automatically notify tenant (use persisted value from database) - const [autoBillFwd, setautoBillFwd] = useState( - location?.autoBillFwd ?? false - ); - - // Track whether to automatically send rent notification (use persisted value from database) - const [rentDueNotification, setrentDueNotification] = useState( - location?.rentDueNotification ?? false - ); - // Track tenant field values for real-time validation - const [tenantFields, setTenantFields] = useState({ + const [formValues, setFormValues] = useState({ + locationName: location?.name ?? "", tenantName: location?.tenantName ?? "", tenantStreet: location?.tenantStreet ?? "", tenantTown: location?.tenantTown ?? "", tenantEmail: location?.tenantEmail ?? "", + tenantPaymentMethod: location?.tenantPaymentMethod ?? "none", + autoBillFwd: location?.autoBillFwd ?? false, + billFwdStrategy: location?.billFwdStrategy ?? "when-payed", + rentDueNotification: location?.rentDueNotification ?? false, + rentAmount: location?.rentAmount ?? "", + rentDueDay: location?.rentDueDay ?? 1, }); - const handleTenantFieldChange = (field: keyof typeof tenantFields, value: string) => { - setTenantFields(prev => ({ ...prev, [field]: value })); + const handleInputChange = (field: keyof typeof formValues, value: string | boolean | number) => { + setFormValues(prev => ({ ...prev, [field]: value })); }; let { year, month } = location ? location.yearMonth : yearMonth; @@ -69,7 +63,14 @@ export const LocationEditForm: FC = ({ location, yearMont }
{t("location-name-legend")} - + handleInputChange("locationName", e.target.value)} + />
{state.errors?.locationName && state.errors.locationName.map((error: string) => ( @@ -79,25 +80,24 @@ export const LocationEditForm: FC = ({ location, yearMont ))}
+
{t("tenant-payment-instructions-legend")} - {t("tenant-payment-instructions-code-info")} + + {t("tenant-payment-instructions-code-info")} -
- +
+ {t("tenant-payment-instructions-method--legend")} +
- {generateTenantCode && ( + { formValues.tenantPaymentMethod === "iban" ? ( <> +
{t("iban-payment--form-title")}
- -
{t("auto-utility-bill-forwarding-legend")} {t("auto-utility-bill-forwarding-info")} @@ -186,17 +207,17 @@ export const LocationEditForm: FC = ({ location, yearMont type="checkbox" name="autoBillFwd" className="toggle toggle-primary" - checked={autoBillFwd} - onChange={(e) => setautoBillFwd(e.target.checked)} + checked={formValues.autoBillFwd} + onChange={(e) => handleInputChange("autoBillFwd", e.target.checked)} /> {t("auto-utility-bill-forwarding-toggle-label")}
- {autoBillFwd && ( + {formValues.autoBillFwd && (
{t("utility-bill-forwarding-strategy-label")} - @@ -215,18 +236,22 @@ export const LocationEditForm: FC = ({ location, yearMont type="checkbox" name="rentDueNotification" className="toggle toggle-primary" - checked={rentDueNotification} - onChange={(e) => setrentDueNotification(e.target.checked)} + checked={formValues.rentDueNotification} + onChange={(e) => handleInputChange("rentDueNotification", e.target.checked)} /> {t("auto-rent-notification-toggle-label")}
- {rentDueNotification && ( + {formValues.rentDueNotification && ( <>
{t("rent-due-day-label")} - handleInputChange("rentDueDay", parseInt(e.target.value,10)) + }> {Array.from({ length: 28 }, (_, i) => i + 1).map(day => ( ))} @@ -242,7 +267,8 @@ export const LocationEditForm: FC = ({ location, yearMont step="0.01" placeholder={t("rent-amount-placeholder")} className="input input-bordered w-full placeholder:text-gray-600 text-right" - defaultValue={location?.rentAmount ?? ""} + defaultValue={formValues.rentAmount} + onChange={(e) => handleInputChange("rentAmount", parseFloat(e.target.value))} />
{state.errors?.rentAmount && @@ -257,7 +283,7 @@ export const LocationEditForm: FC = ({ location, yearMont )}
- {(autoBillFwd || rentDueNotification) && ( + {(formValues.autoBillFwd || formValues.rentDueNotification) && (
{t("tenant-email-legend")} = ({ location, yearMont type="email" placeholder={t("tenant-email-placeholder")} className="input input-bordered w-full placeholder:text-gray-600" - defaultValue={location?.tenantEmail ?? ""} - onChange={(e) => handleTenantFieldChange("tenantEmail", e.target.value)} + defaultValue={formValues.tenantEmail} + onChange={(e) => handleInputChange("tenantEmail", e.target.value)} />
{state.errors?.tenantEmail && diff --git a/app/ui/ViewLocationCard.tsx b/app/ui/ViewLocationCard.tsx index b2eb33c..6eca737 100644 --- a/app/ui/ViewLocationCard.tsx +++ b/app/ui/ViewLocationCard.tsx @@ -27,7 +27,7 @@ export const ViewLocationCard:FC = ({location, userSettin tenantName, tenantStreet, tenantTown, - generateTenantCode, + tenantPaymentMethod, // NOTE: only the fileName is projected from the DB to reduce data transfer utilBillsProofOfPaymentAttachment, utilBillsProofOfPaymentUploadedAt, @@ -79,7 +79,7 @@ export const ViewLocationCard:FC = ({location, userSettin const { hub3aText, paymentParams } = useMemo(() => { - if(!userSettings?.enableIbanPayment || !generateTenantCode) { + if(!userSettings?.enableIbanPayment || tenantPaymentMethod !== "iban") { return { hub3aText: "", paymentParams: {} as PaymentParams @@ -126,7 +126,7 @@ export const ViewLocationCard:FC = ({location, userSettin : null } { - userSettings?.enableIbanPayment && generateTenantCode ? + userSettings?.enableIbanPayment && tenantPaymentMethod === "iban" ? <>

{t("payment-info-header")}

    diff --git a/messages/hr.json b/messages/hr.json index 635ca3c..edc862b 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -142,10 +142,17 @@ "notes-placeholder": "biljeĆĄke", "tenant-payment-instructions-legend": "UPUTE ZA UPLATU", - "tenant-payment-instructions-code-info": "Kada podstanar otvori poveznicu na obračun za zadani mjesec aplikacija mu moĆŸe prikazati upute za uplatu troĆĄkova reĆŸija na vaĆĄ IBAN, kao i 2D koji moĆŸe skenirati.", + "tenant-payment-instructions-code-info": "Kada podstanar otvori poveznicu na obračun za zadani mjesec aplikacija mu moĆŸe prikazati upute za uplatu troĆĄkova reĆŸija na vaĆĄ IBAN ili Revolut.", + + "tenant-payment-instructions-method--legend": "Podstanaru prikaĆŸi upute za uplatu:", + "tenant-payment-instructions-method--none": "ne prikazuj upute za uplatu", + "tenant-payment-instructions-method--iban": "uplata na IBAN", + "tenant-payment-instructions-method--revolut": "uplata na Revolut", + "tenant-payment-instructions-toggle-label": "podstanaru prikaĆŸi upute za uplatu", "tenant--payment-instructions-note": "VAĆœNO: za ovu funkcionalnost potrebno je otvoriti postavke aplikacije, te unijeti vaĆĄe ime i IBAN.", + "iban-payment--form-title": "Informacije za uplatu na IBAN", "iban-payment--tenant-name-label": "Ime i prezime podstanara", "iban-payment--tenant-name-placeholder": "unesite ime i prezime podstanara", "iban-payment--tenant-street-label": "Ulica podstanara i kućni broj", @@ -159,9 +166,9 @@ "utility-bill-forwarding-strategy-label": "ReĆŸije proslijedi kada...", "utility-bill-forwarding-when-payed": "sve stavke označim kao plaćene", "utility-bill-forwarding-when-attached": "za sve stavke priloĆŸim račun (PDF)", - "auto-rent-notification-legend": "AUTOMATSKA OBAVIJEST O NAJAMNINI", + "auto-rent-notification-legend": "Automatska obavjest O najamnini", "auto-rent-notification-info": "Ova opcija omogućuje automatsko slanje mjesečnog računa za najamninu podstanaru putem emaila na zadani dan u mjesecu.", - "auto-rent-notification-toggle-label": "poĆĄalji obavijest o najamnini", + "auto-rent-notification-toggle-label": "poĆĄalji obavjest o najamnini", "rent-due-day-label": "Dan u mjesecu kada dospijeva najamnina", "rent-amount-label": "Iznos najamnine", "rent-amount-placeholder": "unesite iznos najamnine",