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

View file

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

View file

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