Files
evidencija-rezija/app/ui/LocationEditForm.tsx
Knee Cola f4e82b7314 Implement bill forwarding strategy with radio button persistence
Added billFwdStrategy field to store user's choice for when to forward
utility bills to tenants, with database persistence and UI updates.

Changes:
- Added billFwdStrategy field to BillingLocation interface ("when-payed" | "when-attached")
- Updated FormSchema to validate billFwdStrategy enum values
- Modified updateOrAddLocation to persist billFwdStrategy in all database operations
- Defaults to "when-payed" (first option) when no value exists in database
- Updated LocationEditForm radio buttons to use persisted database values
- Radio button selection is preserved across edits and restored from database
- Renamed autoTenantNotification to autoBillFwd throughout codebase
- Updated localization strings for bill forwarding features

Form behavior:
- New locations: "when-payed" radio selected by default
- Existing locations: Radio selection matches stored database value
- Value persisted in current, subsequent, and all month update operations

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 10:10:18 +01:00

282 lines
16 KiB
TypeScript

"use client";
import { TrashIcon } from "@heroicons/react/24/outline";
import { FC, useState } from "react";
import { BillingLocation, YearMonth } from "../lib/db-types";
import { updateOrAddLocation } from "../lib/actions/locationActions";
import { useFormState } from "react-dom";
import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
export type LocationEditFormProps = {
/** location which should be edited */
location: BillingLocation,
/** year adn month at a new billing location should be assigned */
yearMonth?: undefined
} | {
/** location which should be edited */
location?: undefined,
/** year adn month at a new billing location should be assigned */
yearMonth: YearMonth
}
export const LocationEditForm: FC<LocationEditFormProps> = ({ location, yearMonth }) => {
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 whether to generate 2D code for tenant (use persisted value from database)
const [generateTenantCode, setGenerateTenantCode] = useState(
location?.generateTenantCode ?? false
);
// Track whether to automatically notify tenant (use persisted value from database)
const [autoBillFwd, setautoBillFwd] = useState(
location?.autoBillFwd ?? false
);
// Track tenant field values for real-time validation
const [tenantFields, setTenantFields] = useState({
tenantFirstName: location?.tenantFirstName ?? "",
tenantLastName: location?.tenantLastName ?? "",
tenantEmail: location?.tenantEmail ?? "",
});
const handleTenantFieldChange = (field: keyof typeof tenantFields, value: string) => {
setTenantFields(prev => ({ ...prev, [field]: value }));
};
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}/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>
}
<input id="locationName" name="locationName" type="text" placeholder={t("location-name-placeholder")} className="input input-bordered w-full" defaultValue={location?.name ?? ""} />
<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>
<textarea id="locationNotes" name="locationNotes" className="textarea textarea-bordered my-1 w-full block h-[8em]" placeholder={t("notes-placeholder")} defaultValue={location?.notes ?? ""}></textarea>
<div id="status-error" aria-live="polite" aria-atomic="true">
{state.errors?.locationNotes &&
state.errors.locationNotes.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
<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">{t("tenant-2d-code-legend")}</legend>
<div className="form-control mt-[-1em]">
<label className="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name="generateTenantCode"
className="toggle toggle-primary"
checked={generateTenantCode}
onChange={(e) => setGenerateTenantCode(e.target.checked)}
/>
<span className="label-text">{t("tenant-2d-code-toggle-label")}</span>
</label>
</div>
{generateTenantCode && (
<>
<div className="form-control w-full">
<label className="label">
<span className="label-text">{t("tenant-first-name-label")}</span>
</label>
<input
id="tenantFirstName"
name="tenantFirstName"
type="text"
placeholder={t("tenant-first-name-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={location?.tenantFirstName ?? ""}
onChange={(e) => handleTenantFieldChange("tenantFirstName", e.target.value)}
/>
<div id="tenantFirstName-error" aria-live="polite" aria-atomic="true">
{state.errors?.tenantFirstName &&
state.errors.tenantFirstName.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("tenant-last-name-label")}</span>
</label>
<input
id="tenantLastName"
name="tenantLastName"
type="text"
placeholder={t("tenant-last-name-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={location?.tenantLastName ?? ""}
onChange={(e) => handleTenantFieldChange("tenantLastName", e.target.value)}
/>
<div id="tenantLastName-error" aria-live="polite" aria-atomic="true">
{state.errors?.tenantLastName &&
state.errors.tenantLastName.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</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">{t("auto-utility-bill-forwarding-legend")}</legend>
<div className="form-control">
<label className="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name="autoBillFwd"
className="toggle toggle-primary"
checked={autoBillFwd}
onChange={(e) => setautoBillFwd(e.target.checked)}
/>
<span className="label-text">{t("auto-utility-bill-forwarding-toggle-label")}</span>
</label>
</div>
{autoBillFwd && (
<>
<div className="form-control">
<div className="label">
<span className="label-text font-medium">{t("utility-bill-forwarding-strategy-label")}</span>
</div>
<div className="flex flex-col gap-1 ml-4">
<label className="label cursor-pointer justify-start gap-3 py-1">
<input
type="radio"
name="billFwdStrategy"
value="when-payed"
className="radio radio-primary"
defaultChecked={(location?.billFwdStrategy ?? "when-payed") === "when-payed"}
/>
<span className="label-text">{t("utility-bill-forwarding-when-payed")}</span>
</label>
<label className="label cursor-pointer justify-start gap-3 py-1">
<input
type="radio"
name="billFwdStrategy"
value="when-attached"
className="radio radio-primary"
defaultChecked={location?.billFwdStrategy === "when-attached"}
/>
<span className="label-text">{t("utility-bill-forwarding-when-attached")}</span>
</label>
</div>
</div>
<div className="form-control w-full">
<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={location?.tenantEmail ?? ""}
onChange={(e) => handleTenantFieldChange("tenantEmail", e.target.value)}
/>
<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>
</div>
</>
)}
</fieldset>
{/* Show different options for add vs edit operations */}
{!location ? (
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">{t("add-to-subsequent-months")}</span>
<input type="checkbox" name="addToSubsequentMonths" className="toggle toggle-primary" />
</label>
</div>
) : (
<div className="form-control">
<div className="label">
<span className="label-text font-medium">{t("update-scope")}</span>
</div>
<div className="flex flex-col gap-1 ml-4">
<label className="label cursor-pointer justify-start gap-3 py-1">
<input type="radio" name="updateScope" value="current" className="radio radio-primary" defaultChecked />
<span className="label-text">{t("update-current-month")}</span>
</label>
<label className="label cursor-pointer justify-start gap-3 py-1">
<input type="radio" name="updateScope" value="subsequent" className="radio radio-primary" />
<span className="label-text">{t("update-subsequent-months")}</span>
</label>
<label className="label cursor-pointer justify-start gap-3 py-1">
<input type="radio" name="updateScope" value="all" className="radio radio-primary" />
<span className="label-text">{t("update-all-months")}</span>
</label>
</div>
</div>
)}
<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={`/?year=${year}&month=${month}`}>{t("cancel-button")}</Link>
</div>
</form>
</div>
</div>
)
}
export const LocationEditFormSkeleton: FC = () => {
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">
<div id="locationName" className="input w-full skeleton"></div>
<div id="locationNotes" className="textarea my-1 w-full block h-[8em] skeleton"></div>
<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>
)
}