feat: add language selector for tenant notification emails

Added ability to select the language for automatic notification emails sent to
tenants. Users can choose between Croatian (hr) and English (en). If not set,
defaults to the current UI language.

Changes:
- Add tenantEmailLanguage field to BillingLocation type (shared-code)
- Add language selector fieldset in LocationEditForm below email settings
- Add Zod validation for tenantEmailLanguage in locationActions
- Include field in all database insert and update operations
- Default to current locale if not explicitly set
- Add translation labels for language selector (EN/HR)

This allows tenants to receive bills and notifications in their preferred language
regardless of the landlord's UI language preference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-12-31 10:37:14 +01:00
parent 2bc5cad82d
commit 69f891210e
5 changed files with 36 additions and 0 deletions

View File

@@ -81,6 +81,8 @@ export interface BillingLocation {
tenantEmail?: string | null;
/** (optional) tenant email status */
tenantEmailStatus?: EmailStatus | null;
/** (optional) language for tenant notification emails */
tenantEmailLanguage?: "hr" | "en" | null;
/** (optional) whether to automatically notify tenant */
billFwdEnabled?: boolean | null;
/** (optional) bill forwarding strategy */

View File

@@ -47,6 +47,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({
billFwdEnabled: z.boolean().optional().nullable(),
tenantEmail: z.string().email(t("tenant-email-invalid")).optional().or(z.literal("")).nullable(),
tenantEmailStatus: z.enum([EmailStatus.Unverified, EmailStatus.VerificationPending, EmailStatus.Verified, EmailStatus.Unsubscribed]).optional().nullable(),
tenantEmailLanguage: z.enum(["hr", "en"]).optional().nullable(),
billFwdStrategy: z.enum(["when-payed", "when-attached"]).optional().nullable(),
rentDueNotificationEnabled: z.boolean().optional().nullable(),
rentDueDay: z.coerce.number().min(1).max(31).optional().nullable(),
@@ -136,6 +137,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
billFwdEnabled: formData.get('billFwdEnabled') === 'on',
tenantEmail: formData.get('tenantEmail') || null,
tenantEmailStatus: formData.get('tenantEmailStatus') as "unverified" | "verification-pending" | "verified" | "unsubscribed" | undefined,
tenantEmailLanguage: formData.get('tenantEmailLanguage') as "hr" | "en" | undefined,
billFwdStrategy: formData.get('billFwdStrategy') as "when-payed" | "when-attached" | undefined,
rentDueNotificationEnabled: formData.get('rentDueNotificationEnabled') === 'on',
rentDueDay: formData.get('rentDueDay') || null,
@@ -162,6 +164,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
billFwdEnabled,
tenantEmail,
tenantEmailStatus,
tenantEmailLanguage,
billFwdStrategy,
rentDueNotificationEnabled,
rentDueDay,
@@ -220,6 +223,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
billFwdEnabled: billFwdEnabled || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: finalEmailStatus,
tenantEmailLanguage: tenantEmailLanguage || null,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
rentDueDay: rentDueDay || null,
@@ -252,6 +256,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
billFwdEnabled: billFwdEnabled || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: finalEmailStatus,
tenantEmailLanguage: tenantEmailLanguage || null,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
rentDueDay: rentDueDay || null,
@@ -277,6 +282,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
billFwdEnabled: billFwdEnabled || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: finalEmailStatus,
tenantEmailLanguage: tenantEmailLanguage || null,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
rentDueDay: rentDueDay || null,
@@ -301,6 +307,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
billFwdEnabled: billFwdEnabled || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified,
tenantEmailLanguage: tenantEmailLanguage || null,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
rentDueDay: rentDueDay || null,
@@ -377,6 +384,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
billFwdEnabled: billFwdEnabled || false,
tenantEmail: tenantEmail || null,
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified,
tenantEmailLanguage: tenantEmailLanguage || null,
billFwdStrategy: billFwdStrategy || "when-payed",
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
rentDueDay: rentDueDay || null,

View File

@@ -42,6 +42,7 @@ export const LocationEditForm: FC<LocationEditFormProps> = ({ location, yearMont
tenantTown: location?.tenantTown ?? "",
tenantEmail: location?.tenantEmail ?? "",
tenantEmailStatus: location?.tenantEmailStatus ?? EmailStatus.Unverified,
tenantEmailLanguage: location?.tenantEmailLanguage ?? (locale as "hr" | "en"),
tenantPaymentMethod: location?.tenantPaymentMethod ?? "none",
proofOfPaymentType: location?.proofOfPaymentType ?? "none",
billFwdEnabled: location?.billFwdEnabled ?? false,
@@ -426,6 +427,23 @@ export const LocationEditForm: FC<LocationEditFormProps> = ({ location, yearMont
</fieldset>
)}
<fieldset className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4 pb-2 mt-4">
<legend className="fieldset-legend font-semibold uppercase">{t("notification-language-legend")}</legend>
<label className="label">
<span className="label-text">{t("notification-language-label")}</span>
</label>
<select
id="tenantEmailLanguage"
name="tenantEmailLanguage"
className="select input-bordered w-full"
value={formValues.tenantEmailLanguage}
onChange={(e) => handleInputChange("tenantEmailLanguage", e.target.value)}
>
<option value="hr">{t("notification-language-option-hr")}</option>
<option value="en">{t("notification-language-option-en")}</option>
</select>
</fieldset>
<fieldset className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4 pb-2 mt-4">
<legend className="fieldset-legend font-semibold uppercase text-base">{t("scope-legend")}</legend>
{!location ? (

View File

@@ -205,6 +205,10 @@
"verified": "this e-mail address has been verified",
"unsubscribed": "tenant unsubscribed this address from receiving emails"
},
"notification-language-legend": "NOTIFICATION EMAIL LANGUAGE",
"notification-language-label": "Language for automatic notification emails",
"notification-language-option-hr": "Croatian (Hrvatski)",
"notification-language-option-en": "English",
"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",

View File

@@ -204,6 +204,10 @@
"verified": "podstanar je potvrdio ovu e-mail adresu",
"unsubscribed": "podstanar je odjavio ovu e-mail adresu"
},
"notification-language-legend": "JEZIK ZA OBAVIJESTI",
"notification-language-label": "Jezik za automatske obavijesti putem e-maila",
"notification-language-option-hr": "Hrvatski",
"notification-language-option-en": "Engleski (English)",
"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",