feat: add admin override functionality to ProfileFormAnagrafica and update related components
This commit is contained in:
parent
3655e2fa4b
commit
a0bb309f16
4 changed files with 127 additions and 25 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue