diff --git a/app/[locale]/account/page.tsx b/app/[locale]/account/page.tsx index c8fc5e1..caec00a 100644 --- a/app/[locale]/account/page.tsx +++ b/app/[locale]/account/page.tsx @@ -1,15 +1,15 @@ import { FC, Suspense } from 'react'; import { Main } from '@/app/ui/Main'; -import { AccountForm, AccountFormSkeleton } from '@/app/ui/AccountForm'; -import { getUserProfile } from '@/app/lib/actions/userProfileActions'; +import { UserSettingsForm as UserSettingsForm, UserSettingsFormSkeleton } from '@/app/ui/AppSettingsForm'; +import { getUserSettings } from '@/app/lib/actions/userSettingsActions'; const AccountPage: FC = async () => { - const profile = await getUserProfile(); + const userSettings = await getUserSettings(); return (
- +
); @@ -21,7 +21,7 @@ const Page: FC = () => {
- +
}> diff --git a/app/lib/actions/userProfileActions.ts b/app/lib/actions/userSettingsActions.ts similarity index 84% rename from app/lib/actions/userProfileActions.ts rename to app/lib/actions/userSettingsActions.ts index 26f3ee8..d123999 100644 --- a/app/lib/actions/userProfileActions.ts +++ b/app/lib/actions/userSettingsActions.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { getDbClient } from '../dbClient'; -import { UserProfile } from '../db-types'; +import { UserSettings } from '../db-types'; import { withUser } from '@/app/lib/auth'; import { AuthenticatedUser } from '../types/next-auth'; import { unstable_noStore as noStore } from 'next/cache'; @@ -25,7 +25,7 @@ export type State = { }; /** - * Schema for validating user profile form fields + * Schema for validating user settings form fields */ const FormSchema = (t: IntlTemplateFn) => z.object({ firstName: z.string().optional(), @@ -87,27 +87,27 @@ const FormSchema = (t: IntlTemplateFn) => z.object({ }); /** - * Get user profile + * Get user settings */ -export const getUserProfile = withUser(async (user: AuthenticatedUser) => { +export const getUserSettings = withUser(async (user: AuthenticatedUser) => { noStore(); const dbClient = await getDbClient(); const { id: userId } = user; - const profile = await dbClient.collection("users") + const userSettings = await dbClient.collection("userSettings") .findOne({ userId }); - return profile; + return userSettings; }); /** - * Update user profile + * Update user settings */ -export const updateUserProfile = withUser(async (user: AuthenticatedUser, prevState: State, formData: FormData) => { +export const updateUserSettings = withUser(async (user: AuthenticatedUser, prevState: State, formData: FormData) => { noStore(); - const t = await getTranslations("settings-form.validation"); + const t = await getTranslations("user-settings-form.validation"); const validatedFields = FormSchema(t).safeParse({ firstName: formData.get('firstName') || undefined, @@ -131,11 +131,11 @@ export const updateUserProfile = withUser(async (user: AuthenticatedUser, prevSt // Normalize IBAN: remove spaces and convert to uppercase const normalizedIban = iban ? iban.replace(/\s/g, '').toUpperCase() : null; - // Update the user profile in MongoDB + // Update the user settings in MongoDB const dbClient = await getDbClient(); const { id: userId } = user; - const userProfile: UserProfile = { + const userSettings: UserSettings = { userId, firstName: firstName || null, lastName: lastName || null, @@ -144,18 +144,18 @@ export const updateUserProfile = withUser(async (user: AuthenticatedUser, prevSt show2dCodeInMonthlyStatement: show2dCodeInMonthlyStatement ?? false, }; - await dbClient.collection("users") + await dbClient.collection("userSettings") .updateOne( { userId }, - { $set: userProfile }, + { $set: userSettings }, { upsert: true } ); - revalidatePath('/account'); + revalidatePath('/settings'); // Get current locale and redirect to home with success message const locale = await getLocale(); - await gotoHomeWithMessage(locale, 'profileSaved'); + await gotoHomeWithMessage(locale, 'user-settings-saved-message'); // This return is needed for TypeScript, but won't be reached due to redirect return { diff --git a/app/lib/db-types.ts b/app/lib/db-types.ts index 0932f67..3e73c11 100644 --- a/app/lib/db-types.ts +++ b/app/lib/db-types.ts @@ -14,8 +14,8 @@ export interface YearMonth { month: number; }; -/** User profile data */ -export interface UserProfile { +/** User settings data */ +export interface UserSettings { /** user's ID */ userId: string; /** first name */ diff --git a/app/ui/AccountForm.tsx b/app/ui/AppSettingsForm.tsx similarity index 88% rename from app/ui/AccountForm.tsx rename to app/ui/AppSettingsForm.tsx index 4d48428..0873d5f 100644 --- a/app/ui/AccountForm.tsx +++ b/app/ui/AppSettingsForm.tsx @@ -1,8 +1,8 @@ "use client"; import { FC, useState } from "react"; -import { UserProfile } from "../lib/db-types"; -import { updateUserProfile } from "../lib/actions/userProfileActions"; +import { UserSettings } from "../lib/db-types"; +import { updateUserSettings } from "../lib/actions/userSettingsActions"; import { useFormState, useFormStatus } from "react-dom"; import { useLocale, useTranslations } from "next-intl"; import Link from "next/link"; @@ -10,27 +10,27 @@ import SettingsIcon from "@mui/icons-material/Settings"; import { formatIban } from "../lib/formatStrings"; import { InfoBox } from "./InfoBox"; -export type AccountFormProps = { - profile: UserProfile | null; +export type UserSettingsFormProps = { + userSettings: UserSettings | null; } type FormFieldsProps = { - profile: UserProfile | null; + userSettings: UserSettings | null; errors: any; message: string | null; } -const FormFields: FC = ({ profile, errors, message }) => { +const FormFields: FC = ({ userSettings, errors, message }) => { const { pending } = useFormStatus(); - const t = useTranslations("settings-form"); + const t = useTranslations("user-settings-form"); const locale = useLocale(); // Track current form values for real-time validation const [formValues, setFormValues] = useState({ - firstName: profile?.firstName ?? "", - lastName: profile?.lastName ?? "", - address: profile?.address ?? "", - iban: formatIban(profile?.iban) ?? "", + firstName: userSettings?.firstName ?? "", + lastName: userSettings?.lastName ?? "", + address: userSettings?.address ?? "", + iban: formatIban(userSettings?.iban) ?? "", }); const handleInputChange = (field: keyof typeof formValues, value: string) => { @@ -43,7 +43,7 @@ const FormFields: FC = ({ profile, errors, message }) => { // Track whether to generate 2D code for tenant (use persisted value from database) const [show2dCodeInMonthlyStatement, setShow2dCodeInMonthlyStatement] = useState( - profile?.show2dCodeInMonthlyStatement ?? false + userSettings?.show2dCodeInMonthlyStatement ?? false ); return ( @@ -78,7 +78,7 @@ const FormFields: FC = ({ profile, errors, message }) => { type="text" placeholder={t("first-name-placeholder")} className="input input-bordered w-full placeholder:text-gray-600" - defaultValue={profile?.firstName ?? ""} + defaultValue={userSettings?.firstName ?? ""} onChange={(e) => handleInputChange("firstName", e.target.value)} disabled={pending} /> @@ -102,7 +102,7 @@ const FormFields: FC = ({ profile, errors, message }) => { type="text" placeholder={t("last-name-placeholder")} className="input input-bordered w-full placeholder:text-gray-600" - defaultValue={profile?.lastName ?? ""} + defaultValue={userSettings?.lastName ?? ""} onChange={(e) => handleInputChange("lastName", e.target.value)} disabled={pending} /> @@ -125,7 +125,7 @@ const FormFields: FC = ({ profile, errors, message }) => { name="address" className="textarea textarea-bordered w-full placeholder:text-gray-600" placeholder={t("address-placeholder")} - defaultValue={profile?.address ?? ""} + defaultValue={userSettings?.address ?? ""} onChange={(e) => handleInputChange("address", e.target.value)} disabled={pending} > @@ -149,7 +149,7 @@ const FormFields: FC = ({ profile, errors, message }) => { type="text" placeholder={t("iban-placeholder")} className="input input-bordered w-full placeholder:text-gray-600" - defaultValue={formatIban(profile?.iban)} + defaultValue={formatIban(userSettings?.iban)} onChange={(e) => handleInputChange("iban", e.target.value)} disabled={pending} /> @@ -191,10 +191,10 @@ const FormFields: FC = ({ profile, errors, message }) => { ); }; -export const AccountForm: FC = ({ profile }) => { +export const UserSettingsForm: FC = ({ userSettings }) => { const initialState = { message: null, errors: {} }; - const [state, dispatch] = useFormState(updateUserProfile, initialState); - const t = useTranslations("settings-form"); + const [state, dispatch] = useFormState(updateUserSettings, initialState); + const t = useTranslations("user-settings-form"); return (
@@ -202,7 +202,7 @@ export const AccountForm: FC = ({ profile }) => {

{t("title")}

@@ -212,7 +212,7 @@ export const AccountForm: FC = ({ profile }) => { ); }; -export const AccountFormSkeleton: FC = () => { +export const UserSettingsFormSkeleton: FC = () => { return (
diff --git a/app/ui/MonthLocationList.tsx b/app/ui/MonthLocationList.tsx index 25533b8..7938415 100644 --- a/app/ui/MonthLocationList.tsx +++ b/app/ui/MonthLocationList.tsx @@ -52,9 +52,9 @@ export const MonthLocationList:React.FC = ({ const params = new URLSearchParams(search.toString()); let messageShown = false; - if (search.get('profileSaved') === 'true') { - toast.success(t("profile-saved-message"), { theme: "dark" }); - params.delete('profileSaved'); + if (search.get('userSettingsSaved') === 'true') { + toast.success(t("user-settings-saved-message"), { theme: "dark" }); + params.delete('userSettingsSaved'); messageShown = true; } diff --git a/app/ui/UserSettingsForm.tsx b/app/ui/UserSettingsForm.tsx new file mode 100644 index 0000000..0873d5f --- /dev/null +++ b/app/ui/UserSettingsForm.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { FC, useState } from "react"; +import { UserSettings } from "../lib/db-types"; +import { updateUserSettings } from "../lib/actions/userSettingsActions"; +import { useFormState, useFormStatus } from "react-dom"; +import { useLocale, useTranslations } from "next-intl"; +import Link from "next/link"; +import SettingsIcon from "@mui/icons-material/Settings"; +import { formatIban } from "../lib/formatStrings"; +import { InfoBox } from "./InfoBox"; + +export type UserSettingsFormProps = { + userSettings: UserSettings | null; +} + +type FormFieldsProps = { + userSettings: UserSettings | null; + errors: any; + message: string | null; +} + +const FormFields: FC = ({ userSettings, errors, message }) => { + const { pending } = useFormStatus(); + const t = useTranslations("user-settings-form"); + const locale = useLocale(); + + // Track current form values for real-time validation + const [formValues, setFormValues] = useState({ + firstName: userSettings?.firstName ?? "", + lastName: userSettings?.lastName ?? "", + address: userSettings?.address ?? "", + iban: formatIban(userSettings?.iban) ?? "", + }); + + 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 cleanedIban = formValues.iban.replace(/\s/g, ''); + const hasMissingData = !formValues.firstName || !formValues.lastName || !formValues.address || !cleanedIban; + + // Track whether to generate 2D code for tenant (use persisted value from database) + const [show2dCodeInMonthlyStatement, setShow2dCodeInMonthlyStatement] = useState( + userSettings?.show2dCodeInMonthlyStatement ?? false + ); + + return ( + <> +
+ {t("tenant-2d-code-legend")} + + {t("info-box-message")} + +
+ +
+ + {show2dCodeInMonthlyStatement && ( + <> +
+ + handleInputChange("firstName", e.target.value)} + disabled={pending} + /> +
+ {errors?.firstName && + errors.firstName.map((error: string) => ( +

+ {error} +

+ ))} +
+
+ +
+ + handleInputChange("lastName", e.target.value)} + disabled={pending} + /> +
+ {errors?.lastName && + errors.lastName.map((error: string) => ( +

+ {error} +

+ ))} +
+
+ +
+ + +
+ {errors?.address && + errors.address.map((error: string) => ( +

+ {error} +

+ ))} +
+
+ +
+ + handleInputChange("iban", e.target.value)} + disabled={pending} + /> +
+ {errors?.iban && + errors.iban.map((error: string) => ( +

+ {error} +

+ ))} +
+
+ {t("additional-notes")} + + )} +
+ +
+ {message && ( +

+ {message} +

+ )} +
+ +
+ + + {t("cancel-button")} + +
+ + ); +}; + +export const UserSettingsForm: FC = ({ userSettings }) => { + const initialState = { message: null, errors: {} }; + const [state, dispatch] = useFormState(updateUserSettings, initialState); + const t = useTranslations("user-settings-form"); + + return ( +
+
+

{t("title")}

+ + + +
+
+ ); +}; + +export const UserSettingsFormSkeleton: FC = () => { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/messages/en.json b/messages/en.json index d9a1846..280615c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -74,7 +74,7 @@ "empty-state-title": "No Barcode Data Found", "empty-state-message": "No bills with 2D barcodes found for {yearMonth}" }, - "profile-saved-message": "Profile updated successfully", + "user-settings-saved-message": "User settings updated successfully", "bill-saved-message": "Bill saved successfully", "bill-deleted-message": "Bill deleted successfully", "location-saved-message": "Location saved successfully", @@ -164,8 +164,8 @@ "validation-failed": "Validation failed. Please check the form and try again." } }, - "settings-form": { - "title": "Settings", + "user-settings-form": { + "title": "User settings", "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", diff --git a/messages/hr.json b/messages/hr.json index 569304a..342ae79 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -74,7 +74,7 @@ "empty-state-title": "Nema Podataka o Barkodovima", "empty-state-message": "Nema računa s 2D barkodovima za {yearMonth}" }, - "profile-saved-message": "Profil uspješno ažuriran", + "user-settings-saved-message": "Korisničke postavke uspješno ažurirane", "bill-saved-message": "Račun uspješno spremljen", "bill-deleted-message": "Račun uspješno obrisan", "location-saved-message": "Nekretnina uspješno spremljena", @@ -163,8 +163,8 @@ "validation-failed": "Validacija nije uspjela. Molimo provjerite formu i pokušajte ponovno." } }, - "settings-form": { - "title": "Postavke", + "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",