diff --git a/.claude/settings.local.json b/.claude/settings.local.json index cb29853..34176df 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,8 @@ "Bash(git add:*)", "Bash(git commit:*)", "mcp__serena__replace_regex", - "Bash(npm install:*)" + "Bash(npm install:*)", + "mcp__ide__getDiagnostics" ] }, "enableAllProjectMcpServers": true, diff --git a/app/lib/actions/userProfileActions.ts b/app/lib/actions/userProfileActions.ts index a33c32e..b02d22c 100644 --- a/app/lib/actions/userProfileActions.ts +++ b/app/lib/actions/userProfileActions.ts @@ -18,6 +18,7 @@ export type State = { lastName?: string[]; address?: string[]; iban?: string[]; + show2dCodeInMonthlyStatement?: string[]; }; message?: string | null; success?: boolean; @@ -41,6 +42,48 @@ const FormSchema = (t: IntlTemplateFn) => z.object({ }, { message: t("iban-invalid") } ), + show2dCodeInMonthlyStatement: z.boolean().optional().nullable(), +}) +.refine((data) => { + if (data.show2dCodeInMonthlyStatement) { + return !!data.firstName && data.firstName.trim().length > 0; + } + return true; +}, { + message: t("first-name-required"), + path: ["firstName"], +}) +.refine((data) => { + if (data.show2dCodeInMonthlyStatement) { + return !!data.lastName && data.lastName.trim().length > 0; + } + return true; +}, { + message: t("last-name-required"), + path: ["lastName"], +}) +.refine((data) => { + if (data.show2dCodeInMonthlyStatement) { + return !!data.address && data.address.trim().length > 0; + } + return true; +}, { + message: t("address-required"), + path: ["address"], +}) +.refine((data) => { + if (data.show2dCodeInMonthlyStatement) { + if (!data.iban || data.iban.trim().length === 0) { + return false; + } + // Validate IBAN format when required + const cleaned = data.iban.replace(/\s/g, '').toUpperCase(); + return IBAN.isValid(cleaned); + } + return true; +}, { + message: t("iban-required"), + path: ["iban"], }); /** @@ -71,6 +114,7 @@ export const updateUserProfile = withUser(async (user: AuthenticatedUser, prevSt lastName: formData.get('lastName') || undefined, address: formData.get('address') || undefined, iban: formData.get('iban') || undefined, + show2dCodeInMonthlyStatement: formData.get('generateTenantCode') === 'on', }); // If form validation fails, return errors early. Otherwise, continue... @@ -82,7 +126,7 @@ export const updateUserProfile = withUser(async (user: AuthenticatedUser, prevSt }; } - const { firstName, lastName, address, iban } = validatedFields.data; + const { firstName, lastName, address, iban, show2dCodeInMonthlyStatement } = validatedFields.data; // Normalize IBAN: remove spaces and convert to uppercase const normalizedIban = iban ? iban.replace(/\s/g, '').toUpperCase() : null; @@ -97,6 +141,7 @@ export const updateUserProfile = withUser(async (user: AuthenticatedUser, prevSt lastName: lastName || null, address: address || null, iban: normalizedIban, + show2dCodeInMonthlyStatement: show2dCodeInMonthlyStatement ?? false, }; await dbClient.collection("users") diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index fc8f4a5..0932f67 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -26,6 +26,8 @@ export interface UserProfile { address?: string | null; /** IBAN */ iban?: string | null; + /** whether to show 2D code in monthly statement */ + show2dCodeInMonthlyStatement?: boolean | null; }; /** bill object in the form returned by MongoDB */ diff --git a/app/ui/AccountForm.tsx b/app/ui/AccountForm.tsx index c1b58a1..bc8cbf2 100644 --- a/app/ui/AccountForm.tsx +++ b/app/ui/AccountForm.tsx @@ -41,112 +41,130 @@ const FormFields: FC = ({ profile, errors, message }) => { const cleanedIban = formValues.iban.replace(/\s/g, ''); const hasMissingData = !formValues.firstName || !formValues.lastName || !formValues.address || !cleanedIban; + // Track whether to generate 2D code for tenant (use persisted value from database) + const [show2dCodeInMonthlyStatement, setShow2dCodeInMonthlyStatement] = useState( + profile?.show2dCodeInMonthlyStatement ?? false + ); + return ( <> - {t("info-box-message")} -
- - handleInputChange("firstName", e.target.value)} - disabled={pending} - /> -
- {errors?.firstName && - errors.firstName.map((error: string) => ( -

- {error} -

- ))} -
-
+
+ {t("tenant-2d-code-legend")} -
- - handleInputChange("lastName", e.target.value)} - disabled={pending} - /> -
- {errors?.lastName && - errors.lastName.map((error: string) => ( -

- {error} -

- ))} -
-
+ {t("info-box-message")} -
- - -
- {errors?.address && - errors.address.map((error: string) => ( -

- {error} -

- ))} -
-
+
+ +
-
- - handleInputChange("iban", e.target.value)} - disabled={pending} - /> -
- {errors?.iban && - errors.iban.map((error: string) => ( -

- {error} -

- ))} -
-
+ {show2dCodeInMonthlyStatement && ( + <> +
+ + handleInputChange("firstName", e.target.value)} + disabled={pending} + /> +
+ {errors?.firstName && + errors.firstName.map((error: string) => ( +

+ {error} +

+ ))} +
+
- {hasMissingData && ( -
- - - - {t("warning-missing-data")} -
- )} +
+ + handleInputChange("lastName", e.target.value)} + disabled={pending} + /> +
+ {errors?.lastName && + errors.lastName.map((error: string) => ( +

+ {error} +

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

+ {error} +

+ ))} +
+
+ +
+ + handleInputChange("iban", e.target.value)} + disabled={pending} + /> +
+ {errors?.iban && + errors.iban.map((error: string) => ( +

+ {error} +

+ ))} +
+
+ + )} +
{message && ( diff --git a/messages/en.json b/messages/en.json index ba8ce1d..a80ebb5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -166,8 +166,9 @@ }, "account-form": { "title": "Profile Information", - "info-box-message": "This information will be used to generate a 2D barcode displayed in the bill view, allowing tenants to scan it and refund the money you have spent on paying utility bills in their name.", - "warning-missing-data": "Warning: Some profile fields are missing. The 2D barcode will not be displayed to tenants on the shared bill view until all fields are filled in.", + "info-box-message": "By activating this option, a 2D barcode will be included in the monthly statement sent to the tenant, allowing them to make a direct payment to your bank account.", + "tenant-2d-code-legend": "TENANT 2D CODE", + "tenant-2d-code-toggle-label": "include 2D code in monthly statements", "first-name-label": "First Name", "first-name-placeholder": "Enter your first name", "last-name-label": "Last Name", @@ -179,6 +180,10 @@ "save-button": "Save", "cancel-button": "Cancel", "validation": { + "first-name-required": "First name is mandatory", + "last-name-required": "Last name is mandatory", + "address-required": "Address is mandatory", + "iban-required": "Valid IBAN is mandatory", "iban-invalid": "Invalid IBAN format. Please enter a valid IBAN", "validation-failed": "Validation failed. Please check the form and try again." } diff --git a/messages/hr.json b/messages/hr.json index 35bfa73..93ca664 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -165,8 +165,9 @@ }, "account-form": { "title": "Podaci o profilu", - "info-box-message": "Ovi podaci će se koristiti za generiranje 2D barkoda koji će biti prikazan u pregledu računa, omogućujući podstanarima da ga skeniraju i vrate novac koji ste potrošili na plaćanje režija u njihovo ime.", - "warning-missing-data": "Upozorenje: Neki podaci profila nedostaju. 2D barkod neće biti prikazan podstanarima u podijeljenom pregledu računa dok sva polja ne budu popunjena.", + "info-box-message": "Ako uključite ovu opciji na mjesečnom obračunu koji se šalje podstanaru biti će prikazan 2D bar kod, putem kojeg će moći izvršiti izravnu uplatu na vaš bankovni račun.", + "tenant-2d-code-legend": "2D BARKOD ZA PODSTANARA", + "tenant-2d-code-toggle-label": "prikazuj 2D barkod u mjesečnom obračunu", "first-name-label": "Ime", "first-name-placeholder": "Unesite svoje ime", "last-name-label": "Prezime", @@ -178,6 +179,10 @@ "save-button": "Spremi", "cancel-button": "Odbaci", "validation": { + "first-name-required": "Ime je obavezno", + "last-name-required": "Prezime je obavezno", + "address-required": "Adresa je obavezna", + "iban-required": "Ispravan IBAN je obavezan", "iban-invalid": "Neispravan IBAN format. Molimo unesite ispravan IBAN.", "validation-failed": "Validacija nije uspjela. Molimo provjerite formu i pokušajte ponovno." }