From fe98a63594c168ada347114bd761a7e34d0001a2 Mon Sep 17 00:00:00 2001 From: Knee Cola Date: Mon, 29 Dec 2025 23:05:57 +0100 Subject: [PATCH] feat: add persistence for tenant email status field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tenantEmailStatus hidden field to LocationEditForm - Update locationActions to persist email status across all scopes - Add reset button for unsubscribed email status - Improve email status display with new/modified indicators - Update translations for email status messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- web-app/app/lib/actions/locationActions.ts | 11 ++++++- web-app/app/ui/LocationEditForm.tsx | 36 +++++++++++++++++----- web-app/messages/en.json | 4 ++- web-app/messages/hr.json | 4 ++- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/web-app/app/lib/actions/locationActions.ts b/web-app/app/lib/actions/locationActions.ts index 12068c1..41f12ba 100644 --- a/web-app/app/lib/actions/locationActions.ts +++ b/web-app/app/lib/actions/locationActions.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { getDbClient } from '../dbClient'; -import { BillingLocation, FileAttachment, YearMonth } from '../db-types'; +import { BillingLocation, FileAttachment, YearMonth, EmailStatus } from '../db-types'; import { ObjectId } from 'mongodb'; import { withUser } from '@/app/lib/auth'; import { AuthenticatedUser } from '../types/next-auth'; @@ -22,6 +22,7 @@ export type State = { tenantTown?: string[]; autoBillFwd?: string[]; tenantEmail?: string[]; + tenantEmailStatus?: string[]; billFwdStrategy?: string[]; rentDueNotification?: string[]; rentDueDay?: string[]; @@ -44,6 +45,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ tenantTown: z.string().max(27).optional().nullable(), autoBillFwd: 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(), billFwdStrategy: z.enum(["when-payed", "when-attached"]).optional().nullable(), rentDueNotification: z.boolean().optional().nullable(), rentDueDay: z.coerce.number().min(1).max(31).optional().nullable(), @@ -122,6 +124,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat tenantTown: formData.get('tenantTown') || null, autoBillFwd: formData.get('autoBillFwd') === 'on', tenantEmail: formData.get('tenantEmail') || null, + tenantEmailStatus: formData.get('tenantEmailStatus') as "unverified" | "verification-pending" | "verified" | "unsubscribed" | undefined, billFwdStrategy: formData.get('billFwdStrategy') as "when-payed" | "when-attached" | undefined, rentDueNotification: formData.get('rentDueNotification') === 'on', rentDueDay: formData.get('rentDueDay') || null, @@ -147,6 +150,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat tenantTown, autoBillFwd, tenantEmail, + tenantEmailStatus, billFwdStrategy, rentDueNotification, rentDueDay, @@ -190,6 +194,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat tenantTown: tenantTown || null, autoBillFwd: autoBillFwd || false, tenantEmail: tenantEmail || null, + tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotification: rentDueNotification || false, rentDueDay: rentDueDay || null, @@ -221,6 +226,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat tenantTown: tenantTown || null, autoBillFwd: autoBillFwd || false, tenantEmail: tenantEmail || null, + tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotification: rentDueNotification || false, rentDueDay: rentDueDay || null, @@ -245,6 +251,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat tenantTown: tenantTown || null, autoBillFwd: autoBillFwd || false, tenantEmail: tenantEmail || null, + tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotification: rentDueNotification || false, rentDueDay: rentDueDay || null, @@ -268,6 +275,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat tenantTown: tenantTown || null, autoBillFwd: autoBillFwd || false, tenantEmail: tenantEmail || null, + tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotification: rentDueNotification || false, rentDueDay: rentDueDay || null, @@ -343,6 +351,7 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat tenantTown: tenantTown || null, autoBillFwd: autoBillFwd || false, tenantEmail: tenantEmail || null, + tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified, billFwdStrategy: billFwdStrategy || "when-payed", rentDueNotification: rentDueNotification || false, rentDueDay: rentDueDay || null, diff --git a/web-app/app/ui/LocationEditForm.tsx b/web-app/app/ui/LocationEditForm.tsx index 870ed75..3b392d3 100644 --- a/web-app/app/ui/LocationEditForm.tsx +++ b/web-app/app/ui/LocationEditForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { TrashIcon, ExclamationTriangleIcon, ClockIcon, EnvelopeIcon, CheckCircleIcon } from "@heroicons/react/24/outline"; +import { TrashIcon, ExclamationTriangleIcon, ClockIcon, EnvelopeIcon, CheckCircleIcon, PencilSquareIcon } from "@heroicons/react/24/outline"; import { FC, useState } from "react"; import { BillingLocation, UserSettings, YearMonth, EmailStatus } from "../lib/db-types"; import { updateOrAddLocation } from "../lib/actions/locationActions"; @@ -41,6 +41,7 @@ export const LocationEditForm: FC = ({ location, yearMont tenantStreet: location?.tenantStreet ?? "", tenantTown: location?.tenantTown ?? "", tenantEmail: location?.tenantEmail ?? "", + tenantEmailStatus: location?.tenantEmailStatus ?? EmailStatus.Unverified, tenantPaymentMethod: location?.tenantPaymentMethod ?? "none", proofOfPaymentType: location?.proofOfPaymentType ?? "none", autoBillFwd: location?.autoBillFwd ?? false, @@ -50,10 +51,21 @@ export const LocationEditForm: FC = ({ location, yearMont rentDueDay: location?.rentDueDay ?? 1, }); + // tenant e-mail fetched from database + const [dbTenantEmail, setDbTenantEmail] = useState(location?.tenantEmail ?? ""); + const handleInputChange = (field: keyof typeof formValues, value: string | boolean | number) => { setFormValues(prev => ({ ...prev, [field]: value })); }; + const handleResetEmailStatus = () => { + // this will simulate that the email + // is new and needs verification + setDbTenantEmail(""); + // reset the email status to unverified + setFormValues(prev => ({ ...prev, tenantEmailStatus: EmailStatus.Unverified })); + }; + let { year, month } = location ? location.yearMonth : yearMonth; return ( @@ -355,37 +367,45 @@ export const LocationEditForm: FC = ({ location, yearMont defaultValue={formValues.tenantEmail} onChange={(e) => handleInputChange("tenantEmail", e.target.value)} /> - {location?.tenantEmail && location?.tenantEmail === formValues.tenantEmail && location?.tenantEmailStatus ? ( + + {dbTenantEmail === formValues.tenantEmail ? (
- {location.tenantEmailStatus === EmailStatus.Unverified && ( + {location?.tenantEmailStatus === EmailStatus.Unverified && ( <> {t("email-status.unverified")} )} - {location.tenantEmailStatus === EmailStatus.VerificationPending && ( + {location?.tenantEmailStatus === EmailStatus.VerificationPending && ( <> {t("email-status.verification-pending")} )} - {location.tenantEmailStatus === EmailStatus.Verified && ( + {location?.tenantEmailStatus === EmailStatus.Verified && ( <> {t("email-status.verified")} )} - {location.tenantEmailStatus === EmailStatus.Unsubscribed && ( + {location?.tenantEmailStatus === EmailStatus.Unsubscribed && ( <> {t("email-status.unsubscribed")} + )}
):(
- - {t("email-status.unverified")} + + {t("email-status.new")}
)}
diff --git a/web-app/messages/en.json b/web-app/messages/en.json index b89e3bc..0d8ccde 100644 --- a/web-app/messages/en.json +++ b/web-app/messages/en.json @@ -196,10 +196,12 @@ "tenant-email-legend": "TENANT EMAIL", "tenant-email-placeholder": "enter tenant's email", "email-status": { + "reset-button-label": "Reset", + "new": "a new e-mail address will need to be verified by the tenant", "unverified": "this e-mail address will need to be verified by the tenant", "verification-pending": "waiting for tenant to verify this email address", "verified": "this e-mail address has been verified", - "unsubscribed": "tenant unsubscribed this address e-mail address from receiving emails" + "unsubscribed": "tenant unsubscribed this address from receiving emails" }, "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", diff --git a/web-app/messages/hr.json b/web-app/messages/hr.json index d0c1b36..0f4e2b5 100644 --- a/web-app/messages/hr.json +++ b/web-app/messages/hr.json @@ -195,7 +195,9 @@ "tenant-email-legend": "EMAIL PODSTANARA", "tenant-email-placeholder": "unesite email podstanara", "email-status": { - "unverified": "podstanar će morati potvrditi ovu e-mail adresu", + "reset-button-label": "Reset", + "new": "nova e-mail adresa - podstanar će je morati potvrditi", + "unverified": "čeka se da podstanar potvrdi e-mail", "verification-pending": "čeka se da podstanar potvrdi e-mail", "verified": "podstanar je potvrdio ovu e-mail adresu", "unsubscribed": "podstanar je odjavio ovu e-mail adresu"