feat: add Revolut payment link support alongside IBAN

- Add NoteBox component for displaying warning messages with icon
- Add Revolut profile name field to user settings schema
- Update UserSettingsForm to support payment instruction selection (disabled/IBAN/Revolut)
- Add Croatian and English translations for new payment options
- Reserve fields for future per-instruction enable/disable functionality

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-24 13:37:54 +01:00
parent 64b31a08b4
commit 686bec6c10
6 changed files with 101 additions and 57 deletions

View File

@@ -28,8 +28,18 @@ export interface UserSettings {
ownerIBAN?: string | null;
/** currency (ISO 4217) */
currency?: string | null;
/** owner Revolut payment link */
ownerRevolutProfileName?: string | null;
/** whether to show 2D code in monthly statement */
show2dCodeInMonthlyStatement?: boolean | null;
/** whether to show payment instructions in monthly statement */
showPaymentInstructionsInMonthlyStatement?: "disabled" | "iban" | "revolut" | null;
// /** whether enableshow IBAN payment instructions in monthly statement */
// enableIbanPaymentInstructionsInMonthlyStatement?: boolean | null;
// /** whether to enable Revolut payment instructions in monthly statement */
// enableRevolutPaymentInstructionsInMonthlyStatement?: boolean | null;
};
/** bill object in the form returned by MongoDB */

7
app/ui/NoteBox.tsx Normal file
View File

@@ -0,0 +1,7 @@
import { FC, ReactNode } from "react";
export const NoteBox: FC<{ children: ReactNode, className?: string }> = ({ children, className }) =>
<div className={`alert max-w-md flex flex-row items-start gap-[.8rem] ml-1 ${className}`}>
<span className="w-6 h-6 text-xl"></span>
<span className="text-left">{children}</span>
</div>

View File

@@ -9,6 +9,7 @@ import Link from "next/link";
import SettingsIcon from "@mui/icons-material/Settings";
import { formatIban } from "../lib/formatStrings";
import { InfoBox } from "./InfoBox";
import { NoteBox } from "./NoteBox";
export type UserSettingsFormProps = {
userSettings: UserSettings | null;
@@ -32,21 +33,16 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
ownerTown: userSettings?.ownerTown ?? "",
ownerIBAN: formatIban(userSettings?.ownerIBAN) ?? "",
currency: userSettings?.currency ?? "EUR",
ownerRevolutProfileName: userSettings?.ownerRevolutProfileName ?? "",
showPaymentInstructions: userSettings?.showPaymentInstructionsInMonthlyStatement ?? "disabled",
});
// https://revolut.me/aderezic?currency=EUR&amount=70000
const handleInputChange = (field: keyof typeof formValues, value: string) => {
setFormValues(prev => ({ ...prev, [field]: value }));
};
// Check if any required field is missing (clean IBAN of spaces for validation)
const cleanedOwnerIBAN = formValues.ownerIBAN.replace(/\s/g, '');
const hasMissingData = !formValues.ownerName || !formValues.ownerStreet || !formValues.ownerTown || !cleanedOwnerIBAN || !formValues.currency;
// Track whether to generate 2D code for tenant (use persisted value from database)
const [show2dCodeInMonthlyStatement, setShow2dCodeInMonthlyStatement] = useState(
userSettings?.show2dCodeInMonthlyStatement ?? false
);
return (
<>
<fieldset className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4 pt-1 pb-2 mt-4">
@@ -60,7 +56,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
id="currency"
name="currency"
className="select select-bordered w-full"
defaultValue={userSettings?.currency ?? "EUR"}
defaultValue={formValues.currency}
onChange={(e) => handleInputChange("currency", e.target.value)}
disabled={pending}
>
@@ -111,25 +107,28 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
</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">{t("tenant-2d-code-legend")}</legend>
<legend className="fieldset-legend font-semibold uppercase">{t("tenant-payment-instructions--legend")}</legend>
<InfoBox className="p-1 mb-1">{t("info-box-message")}</InfoBox>
<fieldset className="fieldset">
<label className="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
name="generateTenantCode"
className="toggle toggle-primary"
checked={show2dCodeInMonthlyStatement}
onChange={(e) => setShow2dCodeInMonthlyStatement(e.target.checked)}
/>
<legend className="fieldset-legend">{t("tenant-2d-code-toggle-label")}</legend>
</label>
<fieldset className="form-control w-full">
<select
id="showPaymentInstructions"
name="showPaymentInstructions"
className="select select-bordered w-full"
defaultValue={formValues.showPaymentInstructions}
onChange={(e) => handleInputChange("showPaymentInstructions", e.target.value)}
disabled={pending}
>
<option value="disabled">{t("tenant-payment-instructions--show-no-instructions")}</option>
<option value="iban">{t("tenant-payment-instructions--show-iban-instructions")}</option>
<option value="revolut">{t("tenant-payment-instructions--show-revolut-instructions")}</option>
</select>
</fieldset>
{show2dCodeInMonthlyStatement && (
{formValues.showPaymentInstructions === "iban" && (
<>
<div className="divider mt-6 mb-2 font-bold uppercase">Informacije za uplatu</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">{t("owner-name-label")}</span>
@@ -141,7 +140,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
maxLength={25}
placeholder={t("owner-name-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={userSettings?.ownerName ?? ""}
defaultValue={formValues.ownerName}
onChange={(e) => handleInputChange("ownerName", e.target.value)}
disabled={pending}
/>
@@ -166,7 +165,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
maxLength={25}
placeholder={t("owner-street-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={userSettings?.ownerStreet ?? ""}
defaultValue={formValues.ownerStreet}
onChange={(e) => handleInputChange("ownerStreet", e.target.value)}
disabled={pending}
/>
@@ -191,7 +190,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
maxLength={27}
placeholder={t("owner-town-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={userSettings?.ownerTown ?? ""}
defaultValue={formValues.ownerTown}
onChange={(e) => handleInputChange("ownerTown", e.target.value)}
disabled={pending}
/>
@@ -215,7 +214,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
type="text"
placeholder={t("owner-iban-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={formatIban(userSettings?.ownerIBAN)}
defaultValue={formValues.ownerIBAN}
onChange={(e) => handleInputChange("ownerIBAN", e.target.value)}
disabled={pending}
/>
@@ -229,9 +228,39 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
</div>
</div>
<InfoBox className="p-1 mt-1">{t("additional-notes")}</InfoBox>
<NoteBox className="p-1 mt-1">{t("payment-additional-notes")}</NoteBox>
</>
)}
{formValues.showPaymentInstructions === "revolut" && (
<>
<div className="divider mt-6 mb-2 font-bold uppercase">Informacije za uplatu</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text">{t("owner-revolut-link-label")}</span>
</label>
<input
id="ownerRevolutProfileName"
name="ownerRevolutProfileName"
type="text"
maxLength={25}
placeholder={t("owner-revolut-link-placeholder")}
className="input input-bordered w-full placeholder:text-gray-600"
defaultValue={formValues.ownerRevolutProfileName}
onChange={(e) => handleInputChange("ownerRevolutProfileName", e.target.value)}
disabled={pending}
/>
<div id="ownerRevolutProfileName-error" aria-live="polite" aria-atomic="true">
{errors?.ownerRevolutProfileName &&
errors.ownerRevolutProfileName.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
<NoteBox className="p-1 mt-1">{t("payment-additional-notes")}</NoteBox>
</>
)}
</fieldset>
<div id="general-error" aria-live="polite" aria-atomic="true">