From 686bec6c103bbe0dccfac36c83f8119c895df2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Dere=C5=BEi=C4=87?= Date: Mon, 24 Nov 2025 13:37:54 +0100 Subject: [PATCH] feat: add Revolut payment link support alongside IBAN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/lib/db-types.ts | 10 +++++ app/ui/NoteBox.tsx | 7 +++ app/ui/UserSettingsForm.tsx | 87 ++++++++++++++++++++++++------------- messages/en.json | 16 ++++++- messages/hr.json | 17 +++++--- package-lock.json | 21 --------- 6 files changed, 101 insertions(+), 57 deletions(-) create mode 100644 app/ui/NoteBox.tsx diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index db92e21..e5f7e0a 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -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 */ diff --git a/app/ui/NoteBox.tsx b/app/ui/NoteBox.tsx new file mode 100644 index 0000000..ce60aa8 --- /dev/null +++ b/app/ui/NoteBox.tsx @@ -0,0 +1,7 @@ +import { FC, ReactNode } from "react"; + +export const NoteBox: FC<{ children: ReactNode, className?: string }> = ({ children, className }) => +
+ ⚠️ + {children} +
\ No newline at end of file diff --git a/app/ui/UserSettingsForm.tsx b/app/ui/UserSettingsForm.tsx index 95a12fa..898191d 100644 --- a/app/ui/UserSettingsForm.tsx +++ b/app/ui/UserSettingsForm.tsx @@ -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 = ({ 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 ( <>
@@ -60,7 +56,7 @@ const FormFields: FC = ({ 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 = ({ userSettings, errors, message }) => {
- {t("tenant-2d-code-legend")} + {t("tenant-payment-instructions--legend")} {t("info-box-message")} -
- +
+
- - {show2dCodeInMonthlyStatement && ( + {formValues.showPaymentInstructions === "iban" && ( <> +
Informacije za uplatu
- {t("additional-notes")} + {t("payment-additional-notes")} )} + {formValues.showPaymentInstructions === "revolut" && ( + <> +
Informacije za uplatu
+
+ + handleInputChange("ownerRevolutProfileName", e.target.value)} + disabled={pending} + /> +
+ {errors?.ownerRevolutProfileName && + errors.ownerRevolutProfileName.map((error: string) => ( +

+ {error} +

+ ))} +
+
+ {t("payment-additional-notes")} + + )}
diff --git a/messages/en.json b/messages/en.json index 087e2fd..d770e94 100644 --- a/messages/en.json +++ b/messages/en.json @@ -194,6 +194,13 @@ "info-box-message": "By activating this option, a 2D barcode will be included in the monthly statement sent to the tenant, allowing them to make a direct payment to your bank account.", "tenant-2d-code-legend": "TENANT 2D CODE", "tenant-2d-code-toggle-label": "include 2D code in monthly statements", + + "tenant-payment-instructions--legend": "Payment Instructions", + "tenant-payment-instructions--info": "If you enable this option, the monthly statement sent to the tenant will include payment instructions and a 2D barcode, allowing them to make a direct payment to your bank account.", + "tenant-payment-instructions--show-no-instructions": "🚫 - do not show payment instructions", + "tenant-payment-instructions--show-iban-instructions": "🏦 - show payment instructions for IBAN", + "tenant-payment-instructions--show-revolut-instructions": "🆁 - show payment instructions for Revolut", + "owner-name-label": "Your First and Last Name", "owner-name-placeholder": "enter your first and last name", "owner-street-label": "Your Street and House Number", @@ -201,7 +208,12 @@ "owner-town-label": "Your Postal Code and Town", "owner-town-placeholder": "enter your postal code and town", "owner-iban-label": "IBAN", - "owner-iban-placeholder": "enter your IBAN", + "owner-iban-placeholder": "enter your IBAN for receiving payments", + + "owner-revolut-profile-label": "Revolut profile name", + "owner-revolut-profile-placeholder": "enter your Revolut profile name for receiving payments", + "owner-revolut-profile-tooltip": "You can find your Revolut profile name in the Revolut app under your user profile. It is displayed below your name and starts with the '@' symbol (e.g., '@john123').", + "general-settings-legend": "General Settings", "currency-label": "Currency", "save-button": "Save", @@ -215,6 +227,6 @@ "currency-required": "Currency is mandatory", "validation-failed": "Validation failed. Please check the form and try again." }, - "additional-notes": "Note: For the 2D code to be displayed, you must enter both the tenant's first and last names in the settings of each property for which you want to use this functionality." + "payment-additional-notes": "IMPORTANT: For the payment instructions to be displayed to the tenant, you must also enable this option in the property's settings." } } \ No newline at end of file diff --git a/messages/hr.json b/messages/hr.json index 047c1e7..285d9ac 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -190,9 +190,12 @@ }, "user-settings-form": { "title": "Korisničke postavke", - "info-box-message": "Ako uključite ovu opciji na mjesečnom obračunu koji se šalje podstanaru biti će prikazan 2D bar kod, putem kojeg će moći izvršiti izravnu uplatu na vaš bankovni račun.", - "tenant-2d-code-legend": "2D BARKOD ZA PODSTANARA", - "tenant-2d-code-toggle-label": "prikazuj 2D barkod u mjesečnom obračunu", + "info-box-message": "Ako uključite ovu opciju na mjesečnom obračunu koji se šalje podstanaru biti će prikazane upute za uplatu i 2D bar kod, putem kojeg će moći izvršiti izravnu uplatu na vaš bankovni račun.", + "tenant-payment-instructions--legend": "Upute za uplatu", + "tenant-payment-instructions--info": "Ako uključite ovu opciju na mjesečnom obračunu koji se šalje podstanaru biti će prikazane upute za uplatu i 2D bar kod, putem kojeg će moći izvršiti izravnu uplatu na vaš bankovni račun.", + "tenant-payment-instructions--show-no-instructions": "🚫 - Ne prikazivati upute za uplatu", + "tenant-payment-instructions--show-iban-instructions": "🏦 - Prikazuj upute za uplatu na IBAN", + "tenant-payment-instructions--show-revolut-instructions": "🆁 - Prikazuj upute za uplatu na Revolut", "owner-name-label": "Vaše ime i prezime", "owner-name-placeholder": "unesite svoje ime i prezime", "owner-street-label": "Ulica i kućni broj", @@ -200,7 +203,11 @@ "owner-town-label": "Poštanski broj i Grad", "owner-town-placeholder": "unesite poštanski broj i grad", "owner-iban-label": "IBAN", - "owner-iban-placeholder": "unesite svoj IBAN", + "owner-iban-placeholder": "IBAN putem kojeg ćete primate uplate", + "owner-revolut-profile-label": "Naziv vašeg Revolut profila", + "owner-revolut-profile-placeholder": "profil putem kojeg ćete primati uplate", + "owner-revolut-profile-tooltip": "Naziv vašeg Revolute profila možete pronaći u aplikaciji Revolut u korisničkom profilu. Prikazan je ispod vašeg imena i prezimena - počinje sa znakom '@' (npr: '@ivan123').", + "general-settings-legend": "Opće postavke", "currency-label": "Valuta", "save-button": "Spremi", @@ -214,6 +221,6 @@ "currency-required": "Valuta je obavezna", "validation-failed": "Validacija nije uspjela. Molimo provjerite formu i pokušajte ponovno." }, - "additional-notes": "Napomena: da bi 2D koda bio prikazan, morate unijeti i ime i prezime podstanara u postavkama svake nekretnine za koju želite koristiti ovu funkcionalnost." + "payment-additional-notes": "VAŽNO: da bi upute za uplatu bile prikazane podstanaru, morate tu ovu opciju uključiti i u postavkama pripadajuće nekretnine." } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1a94b78..8562202 100644 --- a/package-lock.json +++ b/package-lock.json @@ -147,7 +147,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -501,7 +500,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -545,7 +543,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1072,7 +1069,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.5", @@ -1473,7 +1469,6 @@ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.0.tgz", "integrity": "sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q==", "dev": true, - "peer": true, "dependencies": { "glob": "10.3.10" } @@ -1804,7 +1799,6 @@ "version": "18.2.21", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.21.tgz", "integrity": "sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1926,7 +1920,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -2226,7 +2219,6 @@ "version": "0.20.0", "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz", "integrity": "sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==", - "peer": true, "dependencies": { "ts-custom-error": "^3.2.1" }, @@ -2253,7 +2245,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2673,7 +2664,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -3352,7 +3342,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3548,7 +3537,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, - "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -5988,7 +5976,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-14.2.33.tgz", "integrity": "sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "14.2.33", "@swc/helpers": "0.5.5", @@ -6506,7 +6493,6 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", - "peer": true, "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", @@ -6708,7 +6694,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -6894,7 +6879,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -6923,7 +6907,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7065,7 +7048,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7077,7 +7059,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -8002,7 +7983,6 @@ "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -8308,7 +8288,6 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"