feat: add admin override functionality to ProfileFormAnagrafica and update related components

This commit is contained in:
Marco Pedone 2026-04-30 13:56:07 +02:00
parent 3655e2fa4b
commit a0bb309f16
4 changed files with 127 additions and 25 deletions

View file

@ -1,6 +1,7 @@
import { differenceInYears, format } from "date-fns";
import { enUS, it } from "date-fns/locale";
import { CalendarIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { type Control, useWatch } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod/v4";
@ -22,6 +23,7 @@ import {
PopoverTrigger,
} from "~/components/ui/popover";
import { Separator } from "~/components/ui/separator";
import { Switch } from "~/components/ui/switch";
import {
getProvinciaFromSigla,
ITALY_CATASTO_CODE,
@ -36,32 +38,35 @@ import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm";
import { useCatasto } from "~/providers/CatastoProvider";
import { useTranslation } from "~/providers/I18nProvider";
import { UsersAnagraficaSchema } from "~/schemas/public/UsersAnagrafica";
import type { CompleteUserData } from "~/server/services/user.service";
import { api } from "~/utils/api";
const profileFormSchema = z.object({
cap_residenza: z.string().optional(),
civico_residenza: z.string().optional(),
codice_fiscale: z.string().optional(),
comune_residenza: z.string().nullable(),
data_nascita: z.date().optional(),
luogo_nascita: z.string().nullable(),
nazione_nascita: z.string().nullable(),
nazione_residenza: z.string().nullable(),
provincia_residenza: z.string().nullable(),
sesso: z.string().optional(),
via_residenza: z.string().optional(),
});
const DevT: React.ElementType = dynamic(
() => import("@hookform/devtools").then((module) => module.DevTool),
{ ssr: false },
);
type ProfileFormValues = z.infer<typeof profileFormSchema>;
const Schema = UsersAnagraficaSchema.omit({
idanagrafica: true,
userid: true,
doc_personale_fronte_ref: true,
doc_personale_retro_ref: true,
})
.extend({
admin_override: z.boolean(),
})
.nonoptional();
type ProfileFormValues = z.infer<typeof Schema>;
export const ProfileFormAnagrafica = ({
userData,
isAdmin,
}: {
userData: CompleteUserData;
isAdmin: boolean;
}) => {
const { comuni, nazioni } = useCatasto();
const schema = profileFormSchema.superRefine(
const schema = Schema.superRefine(
(
{
data_nascita,
@ -70,9 +75,11 @@ export const ProfileFormAnagrafica = ({
luogo_nascita,
nazione_nascita,
cap_residenza,
admin_override,
},
refinementContext,
) => {
if (admin_override) return;
if (cap_residenza) {
if (cap_residenza.length !== 5) {
refinementContext.issues.push({
@ -131,13 +138,14 @@ export const ProfileFormAnagrafica = ({
provincia_residenza: userData.provincia_residenza,
sesso: userData.sesso || "",
via_residenza: userData.via_residenza || "",
admin_override: false,
};
const { locale, t } = useTranslation();
const DatePickerLocale = locale === "it" ? it : enUS;
z.config(z.locales[locale]());
const form = useZodForm(schema, {
defaultValues: defaultValues,
defaultValues,
});
const utils = api.useUtils();
@ -166,7 +174,7 @@ export const ProfileFormAnagrafica = ({
return (
<>
<Form {...form}>
{/* <FormDebug control={form.control} /> */}
<DevT control={form.control} />
<form
className="h-full space-y-8 px-0.5"
onSubmit={form.handleSubmit(onSubmit)}
@ -210,12 +218,12 @@ export const ProfileFormAnagrafica = ({
<Calendar
autoFocus
captionLayout="dropdown"
defaultMonth={field.value}
defaultMonth={field.value || undefined}
endMonth={new Date()}
locale={DatePickerLocale}
mode="single"
onSelect={field.onChange}
selected={field.value}
selected={field.value || undefined}
/>
</PopoverContent>
</Popover>
@ -400,6 +408,7 @@ export const ProfileFormAnagrafica = ({
await form.trigger("codice_fiscale");
}}
type="text"
value={field.value || undefined}
/>
</FormControl>
</FormItem>
@ -414,8 +423,36 @@ export const ProfileFormAnagrafica = ({
setValue={form.setValue}
trigger={form.trigger}
/>
<Button type="submit">{t.profile.aggiorna}</Button>
<div className="flex items-center gap-8">
<Button type="submit">{t.profile.aggiorna}</Button>
{isAdmin && (
<FormField
control={form.control}
name="admin_override"
render={({ field }) => (
<FormItem className="flex items-center">
<div className="flex items-center justify-center gap-x-4">
<FormLabel htmlFor="admin_override">
Abilita modifiche senza controlli (admin)
</FormLabel>
<FormControl>
<Switch
className="data-[state=checked]:bg-neutral-700"
defaultChecked={field.value}
id="admin_override"
name="admin_override"
onCheckedChange={(v) => {
field.onChange(v);
}}
/>
</FormControl>
<FormMessage />
</div>
</FormItem>
)}
/>
)}
</div>
</form>
</Form>
</>
@ -539,6 +576,11 @@ const ResidenzaSection = ({
<Input
{...field}
id="provincia_residenza"
onChange={(e) => {
field.onChange(
e.target.value === "" ? null : e.target.value,
);
}}
type="text"
value={field.value || undefined}
/>
@ -621,7 +663,9 @@ const ResidenzaSection = ({
className="w-full"
id="comune_residenza"
onChange={async (v) => {
field.onChange(v.target.value);
field.onChange(
v.target.value !== "" ? v.target.value : null,
);
await trigger("comune_residenza");
}}
type="text"
@ -647,8 +691,14 @@ const ResidenzaSection = ({
<Input
{...field}
id="cap_residenza"
onChange={(e) => {
field.onChange(
e.target.value === "" ? null : e.target.value,
);
}}
placeholder=""
type="text"
value={field.value || undefined}
/>
</FormControl>
</FormItem>
@ -671,8 +721,14 @@ const ResidenzaSection = ({
<Input
{...field}
id="via_residenza"
onChange={(e) => {
field.onChange(
e.target.value === "" ? null : e.target.value,
);
}}
placeholder="Via ..."
type="text"
value={field.value || undefined}
/>
</FormControl>
</FormItem>
@ -695,8 +751,14 @@ const ResidenzaSection = ({
<Input
{...field}
id="civico_residenza"
onChange={(e) => {
field.onChange(
e.target.value === "" ? null : e.target.value,
);
}}
placeholder=""
type="text"
value={field.value || undefined}
/>
</FormControl>
</FormItem>

View file

@ -208,7 +208,7 @@ const EditUser: NextPageWithLayout<EditUserProps> = ({
<CatastoProvider>
<Card>
<CardContent>
<ProfileFormAnagrafica userData={userData} />
<ProfileFormAnagrafica isAdmin userData={userData} />
</CardContent>
</Card>
</CatastoProvider>

View file

@ -108,7 +108,10 @@ const Dashboard: NextPageWithLayout = () => {
</CardHeader>
<CardContent>
<CatastoProvider>
<ProfileFormAnagrafica userData={userData} />
<ProfileFormAnagrafica
isAdmin={userData.isAdmin}
userData={userData}
/>
</CatastoProvider>
</CardContent>
</Card>

View file

@ -49,4 +49,41 @@ export const MessageUpdateEventSchema = z.discriminatedUnion("eventType", [
z.object({ eventType: z.literal("read_receipt"), data: z.null() }), // all unread messages in this chat are now read
]);
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
/**
* The "Diff" engine that identifies specific mismatches
*/
type SchemaDiff<T, U> = Expand<{
[K in keyof T | keyof U]: K extends keyof T
? K extends keyof U
? T[K] extends U[K]
? U[K] extends T[K]
? never
: { mismatch: "Type structural difference"; s1: T[K]; s2: U[K] }
: { schema1: T[K]; schema2: U[K] }
: { MISSING_IN: "Schema2"; value: T[K] }
: K extends keyof U
? { MISSING_IN: "Schema1"; value: U[K] }
: never;
}>;
/**
* @public
* The Helper Function
* If the types match perfectly, it returns true.
* If they don't, it returns the Diff object as a type error.
* @example const result = assertSchemasEqual(S1, S2);
*/
export function assertSchemasEqual<
S1 extends z.ZodTypeAny,
S2 extends z.ZodTypeAny,
>(
_s1: S1,
_s2: S2,
): [SchemaDiff<z.infer<S1>, z.infer<S2>>] extends [Record<string, never>]
? "✅ Schemas Match"
: { ERROR: "Schemas Diverge"; DIFF: SchemaDiff<z.infer<S1>, z.infer<S2>> } {
// biome-ignore lint/suspicious/noExplicitAny: <ok>
return "✅ Schemas Match" as any;
}