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