From 3cf880a661e012dad17cf6534357296de7ced6ee Mon Sep 17 00:00:00 2001 From: Knee Cola Date: Sat, 22 Nov 2025 15:14:19 +0100 Subject: [PATCH] Add town and currency fields to user settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two new fields to user settings form: - Town field: Text input for city/town (required when 2D code enabled) - Currency field: Select dropdown with ISO 4217 currency codes (EUR default) Updates: - Database schema: Added town and currency fields to UserSettings - Validation: Both fields required when 2D code is enabled - Form UI: Added input fields with proper validation and error handling - Translations: Added Croatian and English labels and error messages - Currency options: 36 ISO 4217 codes with EUR at top as default 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/lib/actions/userSettingsActions.ts | 28 ++++++++- app/lib/db-types.ts | 4 ++ app/ui/UserSettingsForm.tsx | 86 +++++++++++++++++++++++++- messages/en.json | 5 ++ messages/hr.json | 5 ++ 5 files changed, 126 insertions(+), 2 deletions(-) diff --git a/app/lib/actions/userSettingsActions.ts b/app/lib/actions/userSettingsActions.ts index e4f1305..4e5ca44 100644 --- a/app/lib/actions/userSettingsActions.ts +++ b/app/lib/actions/userSettingsActions.ts @@ -17,7 +17,9 @@ export type State = { firstName?: string[]; lastName?: string[]; street?: string[]; + town?: string[]; iban?: string[]; + currency?: string[]; show2dCodeInMonthlyStatement?: string[]; }; message?: string | null; @@ -31,6 +33,7 @@ const FormSchema = (t: IntlTemplateFn) => z.object({ firstName: z.string().optional(), lastName: z.string().optional(), street: z.string().optional(), + town: z.string().optional(), iban: z.string() .optional() .refine( @@ -42,6 +45,7 @@ const FormSchema = (t: IntlTemplateFn) => z.object({ }, { message: t("iban-invalid") } ), + currency: z.string().optional(), show2dCodeInMonthlyStatement: z.boolean().optional().nullable(), }) .refine((data) => { @@ -71,6 +75,15 @@ const FormSchema = (t: IntlTemplateFn) => z.object({ message: t("street-required"), path: ["street"], }) +.refine((data) => { + if (data.show2dCodeInMonthlyStatement) { + return !!data.town && data.town.trim().length > 0; + } + return true; +}, { + message: t("town-required"), + path: ["town"], +}) .refine((data) => { if (data.show2dCodeInMonthlyStatement) { if (!data.iban || data.iban.trim().length === 0) { @@ -84,6 +97,15 @@ const FormSchema = (t: IntlTemplateFn) => z.object({ }, { message: t("iban-required"), path: ["iban"], +}) +.refine((data) => { + if (data.show2dCodeInMonthlyStatement) { + return !!data.currency && data.currency.trim().length > 0; + } + return true; +}, { + message: t("currency-required"), + path: ["currency"], }); /** @@ -113,7 +135,9 @@ export const updateUserSettings = withUser(async (user: AuthenticatedUser, prevS firstName: formData.get('firstName') || undefined, lastName: formData.get('lastName') || undefined, street: formData.get('street') || undefined, + town: formData.get('town') || undefined, iban: formData.get('iban') || undefined, + currency: formData.get('currency') || undefined, show2dCodeInMonthlyStatement: formData.get('generateTenantCode') === 'on', }); @@ -126,7 +150,7 @@ export const updateUserSettings = withUser(async (user: AuthenticatedUser, prevS }; } - const { firstName, lastName, street, iban, show2dCodeInMonthlyStatement } = validatedFields.data; + const { firstName, lastName, street, town, iban, currency, show2dCodeInMonthlyStatement } = validatedFields.data; // Normalize IBAN: remove spaces and convert to uppercase const normalizedIban = iban ? iban.replace(/\s/g, '').toUpperCase() : null; @@ -140,7 +164,9 @@ export const updateUserSettings = withUser(async (user: AuthenticatedUser, prevS firstName: firstName || null, lastName: lastName || null, street: street || null, + town: town || null, iban: normalizedIban, + currency: currency || null, show2dCodeInMonthlyStatement: show2dCodeInMonthlyStatement ?? false, }; diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index 7cc3344..6754305 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -24,8 +24,12 @@ export interface UserSettings { lastName?: string | null; /** street */ street?: string | null; + /** town */ + town?: string | null; /** IBAN */ iban?: string | null; + /** currency (ISO 4217) */ + currency?: string | null; /** whether to show 2D code in monthly statement */ show2dCodeInMonthlyStatement?: boolean | null; }; diff --git a/app/ui/UserSettingsForm.tsx b/app/ui/UserSettingsForm.tsx index 9087ed1..0f6953d 100644 --- a/app/ui/UserSettingsForm.tsx +++ b/app/ui/UserSettingsForm.tsx @@ -30,7 +30,9 @@ const FormFields: FC = ({ userSettings, errors, message }) => { firstName: userSettings?.firstName ?? "", lastName: userSettings?.lastName ?? "", street: userSettings?.street ?? "", + town: userSettings?.town ?? "", iban: formatIban(userSettings?.iban) ?? "", + currency: userSettings?.currency ?? "EUR", }); const handleInputChange = (field: keyof typeof formValues, value: string) => { @@ -39,7 +41,7 @@ const FormFields: FC = ({ userSettings, errors, message }) => { // Check if any required field is missing (clean IBAN of spaces for validation) const cleanedIban = formValues.iban.replace(/\s/g, ''); - const hasMissingData = !formValues.firstName || !formValues.lastName || !formValues.street || !cleanedIban; + const hasMissingData = !formValues.firstName || !formValues.lastName || !formValues.street || !formValues.town || !cleanedIban || !formValues.currency; // Track whether to generate 2D code for tenant (use persisted value from database) const [show2dCodeInMonthlyStatement, setShow2dCodeInMonthlyStatement] = useState( @@ -140,6 +142,30 @@ const FormFields: FC = ({ userSettings, errors, message }) => { +
+ + handleInputChange("town", e.target.value)} + disabled={pending} + /> +
+ {errors?.town && + errors.town.map((error: string) => ( +

+ {error} +

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

+ {error} +

+ ))} +
+
{t("additional-notes")} )} diff --git a/messages/en.json b/messages/en.json index d080540..2626dd2 100644 --- a/messages/en.json +++ b/messages/en.json @@ -192,16 +192,21 @@ "last-name-placeholder": "Enter your last name", "street-label": "Street", "street-placeholder": "Enter your street", + "town-label": "Town", + "town-placeholder": "Enter your town", "iban-label": "IBAN", "iban-placeholder": "Enter your IBAN", + "currency-label": "Currency", "save-button": "Save", "cancel-button": "Cancel", "validation": { "first-name-required": "First name is mandatory", "last-name-required": "Last name is mandatory", "street-required": "Street is mandatory", + "town-required": "Town is mandatory", "iban-required": "Valid IBAN is mandatory", "iban-invalid": "Invalid IBAN format. Please enter a valid IBAN", + "currency-required": "Currency is mandatory", "validation-failed": "Validation failed. Please check the form and try again." }, "additional-notes": "Note: For the 2D code to be displayed, you must enter both the tenant's first and last names in the settings of each property for which you want to use this functionality." diff --git a/messages/hr.json b/messages/hr.json index 4942887..fe0f6b8 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -191,16 +191,21 @@ "last-name-placeholder": "Unesite svoje prezime", "street-label": "Ulica", "street-placeholder": "Unesite ulicu", + "town-label": "Grad", + "town-placeholder": "Unesite grad", "iban-label": "IBAN", "iban-placeholder": "Unesite svoj IBAN", + "currency-label": "Valuta", "save-button": "Spremi", "cancel-button": "Odbaci", "validation": { "first-name-required": "Ime je obavezno", "last-name-required": "Prezime je obavezno", "street-required": "Ulica je obavezna", + "town-required": "Grad je obavezan", "iban-required": "Ispravan IBAN je obavezan", "iban-invalid": "Neispravan IBAN format. Molimo unesite ispravan IBAN.", + "currency-required": "Valuta je obavezna", "validation-failed": "Validacija nije uspjela. Molimo provjerite formu i pokušajte ponovno." }, "additional-notes": "Napomena: da bi 2D koda bio prikazan, morate unijeti i ime i prezime podstanara u postavkama svake nekretnine za koju želite koristiti ovu funkcionalnost."