Files
evidencija-rezija/web-app/app/ui/LocationEditForm.tsx
Knee Cola 5feab991ec refactor: reorder language selector before email input
Moved the language selector to appear before the email address field for better
UX flow. Users now select the notification language first, then enter the email
address that will receive notifications in that language.

Also applied right-alignment to the "new email" status indicator for consistency.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-31 10:53:04 +01:00

522 lines
32 KiB
TypeScript

"use client";
import { TrashIcon, ExclamationTriangleIcon, ClockIcon, EnvelopeIcon, CheckCircleIcon, PencilSquareIcon, XCircleIcon } from "@heroicons/react/24/outline";
import { FC, useState } from "react";
import { BillingLocation, UserSettings, YearMonth, EmailStatus } from '@evidencija-rezija/shared-code';
import { updateOrAddLocation } from "../lib/actions/locationActions";
import { useFormState } from "react-dom";
import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
import { InfoBox } from "./InfoBox";
export type LocationEditFormProps = {
/** location which should be edited */
location: BillingLocation,
/** year adn month at a new billing location should be assigned */
yearMonth?: undefined,
/** user settings for payment configuration */
userSettings: UserSettings | null
} | {
/** location which should be edited */
location?: undefined,
/** year adn month at a new billing location should be assigned */
yearMonth: YearMonth,
/** user settings for payment configuration */
userSettings: UserSettings | null
}
export const LocationEditForm: FC<LocationEditFormProps> = ({ location, yearMonth, userSettings }) => {
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 tenant field values for real-time validation
const [formValues, setFormValues] = useState({
locationName: location?.name ?? "",
tenantName: location?.tenantName ?? "",
tenantStreet: location?.tenantStreet ?? "",
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,
billFwdStrategy: location?.billFwdStrategy ?? "when-payed",
rentDueNotificationEnabled: location?.rentDueNotificationEnabled ?? false,
rentAmount: location?.rentAmount ?? "",
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 (
<div className="card card-compact card-bordered min-w-[20em] max-w-[90em] bg-base-100 shadow-s my-1">
<div className="card-body">
<form action={dispatch}>
{
location &&
<Link href={`/${locale}/home/location/${location._id}/delete`} className="absolute bottom-5 right-4 tooltip" data-tip={t("delete-tooltip")}>
<TrashIcon className="h-[1em] w-[1em] text-error text-2xl" />
</Link>
}
<fieldset className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4 pb-2 pt-3 mt-4">
<legend className="fieldset-legend font-semibold uppercase text-base">{t("location-name-legend")}</legend>
<fieldset className="fieldset p-2 pt-0">
<input id="locationName"
name="locationName"
type="text"
placeholder={t("location-name-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
value={formValues.locationName}
onChange={(e) => handleInputChange("locationName", e.target.value)}
/>
<div id="status-error" aria-live="polite" aria-atomic="true">
{state.errors?.locationName &&
state.errors.locationName.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</fieldset>
</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("tenant-payment-instructions-legend")}</legend>
<InfoBox>{t("tenant-payment-instructions-code-info")}</InfoBox>
<fieldset className="fieldset mt-2 p-2">
<legend className="fieldset-legend">{t("tenant-payment-instructions-method--legend")}</legend>
<select
value={(!userSettings?.enableIbanPayment && !userSettings?.enableRevolutPayment) ? "none" : formValues.tenantPaymentMethod}
className="select input-bordered w-full"
name="tenantPaymentMethod"
onChange={(e) => handleInputChange("tenantPaymentMethod", e.target.value)}
>
<option value="none">{t("tenant-payment-instructions-method--none")}</option>
<option value="iban" disabled={!userSettings?.enableIbanPayment}>
{
userSettings?.enableIbanPayment ?
t("tenant-payment-instructions-method--iban") :
t("tenant-payment-instructions-method--iban-disabled")
}
</option>
<option value="revolut" disabled={!userSettings?.enableRevolutPayment}>
{
userSettings?.enableRevolutPayment ?
t("tenant-payment-instructions-method--revolut") :
t("tenant-payment-instructions-method--revolut-disabled")
}
</option>
</select>
</fieldset>
{formValues.tenantPaymentMethod === "iban" && userSettings?.enableIbanPayment ? (
<div className="animate-expand-fade-in origin-top">
<div className="divider mt-4 mb-2 font-bold uppercase">{t("iban-payment--form-title")}</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">{t("iban-payment--tenant-name-label")}</span>
</label>
<input
id="tenantName"
name="tenantName"
type="text"
maxLength={30}
placeholder={t("iban-payment--tenant-name-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={formValues.tenantName}
onChange={(e) => handleInputChange("tenantName", e.target.value)}
/>
<div id="tenantName-error" aria-live="polite" aria-atomic="true">
{state.errors?.tenantName &&
state.errors.tenantName.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">{t("iban-payment--tenant-street-label")}</span>
</label>
<input
id="tenantStreet"
name="tenantStreet"
type="text"
maxLength={27}
placeholder={t("iban-payment--tenant-street-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={formValues.tenantStreet}
onChange={(e) => handleInputChange("tenantStreet", e.target.value)}
/>
<div id="tenantStreet-error" aria-live="polite" aria-atomic="true">
{state.errors?.tenantStreet &&
state.errors.tenantStreet.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
<div className="form-control w-full mb-4">
<label className="label">
<span className="label-text">{t("iban-payment--tenant-town-label")}</span>
</label>
<input
id="tenantTown"
name="tenantTown"
type="text"
maxLength={27}
placeholder={t("iban-payment--tenant-town-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={formValues.tenantTown}
onChange={(e) => handleInputChange("tenantTown", e.target.value)}
/>
<div id="tenantTown-error" aria-live="polite" aria-atomic="true">
{state.errors?.tenantTown &&
state.errors.tenantTown.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
</div>
) : // ELSE include hidden inputs to preserve existing values
<>
<input
id="tenantName"
name="tenantName"
type="hidden"
maxLength={30}
defaultValue={formValues.tenantName}
/>
<input
id="tenantStreet"
name="tenantStreet"
type="hidden"
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={formValues.tenantStreet}
/>
<input
id="tenantTown"
name="tenantTown"
type="hidden"
defaultValue={formValues.tenantTown}
/>
</>
}
</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("proof-of-payment-attachment-type--legend")}</legend>
<InfoBox>{t("proof-of-payment-attachment-type--info")}</InfoBox>
<fieldset className="fieldset mt-2 p-2">
<legend className="fieldset-legend">{t("proof-of-payment-attachment-type--option--label")}</legend>
<select
value={formValues.proofOfPaymentType}
className="select input-bordered w-full"
name="proofOfPaymentType"
onChange={(e) => handleInputChange("proofOfPaymentType", e.target.value)}
>
<option value="none">{t("proof-of-payment-attachment-type--option--none")}</option>
<option value="combined">{t("proof-of-payment-attachment-type--option--combined")}</option>
<option value="per-bill">{t("proof-of-payment-attachment-type--option--per-bill")}</option>
</select>
{
formValues.tenantPaymentMethod === "none" && formValues.proofOfPaymentType === "combined" ?
<p className="mt-4 ml-4 text-sm w-full sm:w-[30rem] text-yellow-600">
{
t.rich("proof-of-payment-attachment-type--option--combined--hint",
{
strong: (children: React.ReactNode) => <strong>{children}</strong>
}
)
}
</p> :
<p className="mt-4 ml-4 text-sm w-full sm:w-[30rem] italic text-gray-500">
{
formValues.proofOfPaymentType === "combined" ?
t("proof-of-payment-attachment-type--option--combined--tooltip") :
t("proof-of-payment-attachment-type--option--per-bill--tooltip")
}
</p>
}
</fieldset>
</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("auto-utility-bill-forwarding-legend")}</legend>
<InfoBox>{t("auto-utility-bill-forwarding-info")}</InfoBox>
<fieldset className="fieldset">
<label className="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name="billFwdEnabled"
className="toggle toggle-primary"
checked={formValues.billFwdEnabled}
onChange={(e) => handleInputChange("billFwdEnabled", e.target.checked)}
/>
<legend className="fieldset-legend">{t("auto-utility-bill-forwarding-toggle-label")}</legend>
</label>
</fieldset>
{formValues.billFwdEnabled && (
<fieldset className="fieldset mt-2 p-2">
<legend className="fieldset-legend">{t("utility-bill-forwarding-strategy-label")}</legend>
<select defaultValue={formValues.billFwdStrategy} className="select input-bordered w-full" name="billFwdStrategy">
<option value="when-payed">{t("utility-bill-forwarding-when-payed")}</option>
<option value="when-attached">{t("utility-bill-forwarding-when-attached")}</option>
</select>
</fieldset>
)}
</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("auto-rent-notification-legend")}</legend>
<InfoBox>{t("auto-rent-notification-info")}</InfoBox>
<fieldset className="fieldset">
<label className="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name="rentDueNotificationEnabled"
className="toggle toggle-primary"
checked={formValues.rentDueNotificationEnabled}
onChange={(e) => handleInputChange("rentDueNotificationEnabled", e.target.checked)}
/>
<legend className="fieldset-legend">{t("auto-rent-notification-toggle-label")}</legend>
</label>
</fieldset>
{formValues.rentDueNotificationEnabled && (
<div className="animate-expand-fade-in origin-top">
<fieldset className="fieldset mt-2 p-2">
<legend className="fieldset-legend">{t("rent-due-day-label")}</legend>
<select defaultValue={formValues.rentDueDay}
className="select input-bordered w-full"
name="rentDueDay"
onChange={(e) => handleInputChange("rentDueDay", parseInt(e.target.value, 10))
}>
{Array.from({ length: 28 }, (_, i) => i + 1).map(day => (
<option key={day} value={day}>{day}</option>
))}
</select>
</fieldset>
<fieldset className="fieldset mt-2 p-2">
<legend className="fieldset-legend">{t("rent-amount-label")}</legend>
<input
id="rentAmount"
name="rentAmount"
type="number"
min="1"
step="1"
placeholder={t("rent-amount-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600 text-right"
defaultValue={formValues.rentAmount}
onChange={(e) => handleInputChange("rentAmount", parseFloat(e.target.value))}
/>
<div id="rentAmount-error" aria-live="polite" aria-atomic="true">
{state.errors?.rentAmount &&
state.errors.rentAmount.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</fieldset>
</div>
)}
</fieldset>
{(formValues.billFwdEnabled || formValues.rentDueNotificationEnabled) && (
<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("tenant-email-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>
<label className="label">
<span className="label-text">{t("tenant-email-label")}</span>
</label>
<input
id="tenantEmail"
name="tenantEmail"
type="email"
placeholder={t("tenant-email-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={formValues.tenantEmail}
onChange={(e) => handleInputChange("tenantEmail", e.target.value)}
/>
<input
id="tenantEmailStatus"
name="tenantEmailStatus"
type="hidden"
maxLength={30}
defaultValue={formValues.tenantEmailStatus}
/>
{dbTenantEmail === formValues.tenantEmail ? (
<div className="flex items-center justify-end gap-2 mt-2 ml-2">
{location?.tenantEmailStatus === EmailStatus.Unverified && (
<>
<ExclamationTriangleIcon className="h-5 w-5 text-warning" />
<span className="text-sm text-warning">{t("email-status.unverified")}</span>
</>
)}
{location?.tenantEmailStatus === EmailStatus.VerificationPending && (
<>
<ClockIcon className="h-5 w-5 text-info" />
<span className="text-sm text-info">{t("email-status.verification-pending")}</span>
</>
)}
{location?.tenantEmailStatus === EmailStatus.VerificationFailed && (
<>
<XCircleIcon className="h-5 w-5 text-error" />
<span className="text-sm text-error">{t("email-status.verification-failed")}</span>
<button className="btn btn-neutral min-h-0 h-[1.5rem]" onClick={ handleResetEmailStatus }>{t("email-status.reset-button-label")}</button>
</>
)}
{location?.tenantEmailStatus === EmailStatus.Verified && (
<>
<CheckCircleIcon className="h-5 w-5 text-success" />
<span className="text-sm text-success">{t("email-status.verified")}</span>
</>
)}
{location?.tenantEmailStatus === EmailStatus.Unsubscribed && (
<>
<EnvelopeIcon className="h-5 w-5 text-error" />
<span className="text-sm text-error">{t("email-status.unsubscribed")}</span>
<button className="btn btn-neutral min-h-0 h-[1.5rem]" onClick={ handleResetEmailStatus }>{t("email-status.reset-button-label")}</button>
</>
)}
</div>
):(
<div className="flex items-center justify-end gap-2 mt-2 ml-2">
<PencilSquareIcon className="h-5 w-5 text-primary" />
<span className="text-sm text-primary">{t("email-status.new")}</span>
</div>
)}
<div id="tenantEmail-error" aria-live="polite" aria-atomic="true">
{state.errors?.tenantEmail &&
state.errors.tenantEmail.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</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 ? (
<fieldset className="fieldset">
<label className="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name="addToSubsequentMonths"
className="toggle toggle-primary"
/>
<legend className="fieldset-legend">{t("add-to-subsequent-months")}</legend>
</label>
</fieldset>
) : (
<>
<InfoBox>{t("update-scope-info")}</InfoBox>
<fieldset className="fieldset mt-2 p-2">
<legend className="fieldset-legend">{t("update-scope-legend")}</legend>
<select className="select input-bordered w-full" name="updateScope">
<option value="no-scope-selected">{t("update-option-placeholder")}</option>
<option value="current">{t("update-current-month")}</option>
<option value="subsequent">{t("update-subsequent-months")}</option>
<option value="all">{t("update-all-months")}</option>
</select>
<div id="updateScope-error" aria-live="polite" aria-atomic="true">
{state.errors?.updateScope &&
state.errors.updateScope.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</fieldset>
</>
)}
</fieldset>
<div id="status-error" aria-live="polite" aria-atomic="true">
{
state.message &&
<p className="mt-2 text-sm text-red-500">
{state.message}
</p>
}
</div>
<div className="pt-4">
<button className="btn btn-primary w-[5.5em]">{t("save-button")}</button>
<Link className="btn btn-neutral w-[5.5em] ml-3" href={`/home?year=${year}&month=${month}`}>{t("cancel-button")}</Link>
</div>
</form>
</div>
</div>
)
}
export const LocationEditFormSkeleton: FC = () => {
const t = useTranslations("location-edit-form");
return (
<div className="card card-compact card-bordered min-w-[20em] max-w-[90em] bg-base-100 shadow-s my-1">
<div className="card-body">
<fieldset className="fieldset mt-2 p-2">
<legend className="fieldset-legend font-semibold uppercase">{t("location-name-legend")}</legend>
<div id="locationName" className="input w-full skeleton"></div>
</fieldset>
<div className="pt-4">
<div className="btn w-[5.5em] skeleton"></div>
<div className="btn w-[5.5em] ml-3 skeleton"></div>
</div>
</div>
</div>
)
}