feat: implement user intestazione generation and update related handlers

This commit is contained in:
Marco Pedone 2026-04-16 13:35:34 +02:00
parent d4ffc27aed
commit 995eadd9dc
4 changed files with 134 additions and 81 deletions

View file

@ -1,4 +1,3 @@
import { format } from "date-fns";
import { import {
ChevronDown, ChevronDown,
CloudSync, CloudSync,
@ -274,14 +273,22 @@ const OverseerButton = () => {
); );
}; };
const VOCALI = ["A", "E", "I", "O", "U"] as const;
const IntestazioneMaker = () => { const IntestazioneMaker = () => {
const data = useUserViewContext(); const data = useUserViewContext();
const { data: comuni = [] } = api.catasto.searchComuniByCatasto.useQuery({ const { mutate } = api.users.genIntestazione.useMutation({
catasto: [data?.luogo_nascita || "", data?.comune_residenza || ""].filter( onError: (error) => {
(f) => f !== "", toast.error(error.message);
), },
onSuccess: (intestazione) => {
navigator.clipboard
.writeText(intestazione)
.then(() => {
toast.success("Intestazione copiata negli appunti");
})
.catch(() => {
toast.error("Errore nella copia");
});
},
}); });
const isDisabled = const isDisabled =
@ -293,86 +300,17 @@ const IntestazioneMaker = () => {
!data.comune_residenza || !data.comune_residenza ||
!data.via_residenza; !data.via_residenza;
const copyToClipboard = () => {
if (
!data.sesso ||
!data.cognome ||
!data.nome ||
!data.data_nascita ||
!data.luogo_nascita ||
!data.comune_residenza ||
!data.via_residenza
) {
toast.error("Dati mancanti");
return;
}
const comune_nascita = comuni.find((c) => c.catasto === data.luogo_nascita);
const comune_residenza = comuni.find(
(c) => c.catasto === data.comune_residenza,
);
// Format names
const cognome = data.cognome.toUpperCase();
const nome = data.nome.toUpperCase();
const viaResidenza = titleCase(data.via_residenza);
const comuneNascita = comune_nascita
? titleCase(comune_nascita.nome)
: data.luogo_nascita;
const comuneResidenza = comune_residenza
? titleCase(comune_residenza.nome)
: data.comune_residenza;
// Determine article
const isMale = data.sesso === "M";
const articolo = isMale ? "Al Sig." : "Alla Sig.ra";
const natoNata = isMale ? "nato" : "nata";
const preposizione = VOCALI.includes(comuneNascita[0]?.toUpperCase() || "")
? "ad"
: "a";
// Build phrase
const frase = [
`${articolo} ${cognome} ${nome}`,
`${natoNata} il ${format(data.data_nascita, "dd/MM/yyyy")}`,
`${preposizione} ${comuneNascita}${comune_nascita ? ` (${comune_nascita.sigla})` : ""},`,
`residente in ${viaResidenza}, ${data.civico_residenza}`,
`a ${comuneResidenza} (${data.provincia_residenza})`,
`CAP ${data.cap_residenza},`,
`con codice fiscale: ${data.codice_fiscale}`,
].join(" ");
navigator.clipboard
.writeText(frase)
.then(() => {
toast.success("Intestazione copiata negli appunti");
})
.catch(() => {
toast.error("Errore nella copia");
});
};
return ( return (
<Button disabled={isDisabled} onClick={copyToClipboard} size="sm"> <Button
disabled={isDisabled}
onClick={() => mutate({ userId: data.id })}
size="sm"
>
<UserPen /> Intestazione <UserPen /> Intestazione
</Button> </Button>
); );
}; };
function titleCase(str: string | null | undefined): string {
if (!str) return "";
return str
.trim()
.toLowerCase()
.split(/\s+/) // Handle multiple spaces
.map((word) => {
if (word.length === 0) return word;
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(" ");
}
const NoteUtenteDialog = () => { const NoteUtenteDialog = () => {
const { id, note } = useUserViewContext(); const { id, note } = useUserViewContext();
const [newNote, setNewNote] = useState(note || ""); const [newNote, setNewNote] = useState(note || "");

View file

@ -11,6 +11,7 @@ import {
deleteUserHandler, deleteUserHandler,
editAccountHandler, editAccountHandler,
editAnagraficaHandler, editAnagraficaHandler,
genUserIntestazione,
getActiveUserWithoutChatHandler, getActiveUserWithoutChatHandler,
getUserHandler, getUserHandler,
getUsersWithChatInfoHandler, getUsersWithChatInfoHandler,
@ -102,4 +103,13 @@ export const usersRouter = createTRPCRouter({
getUsersWChatInfo: adminProcedure.query(async () => { getUsersWChatInfo: adminProcedure.query(async () => {
return await getUsersWithChatInfoHandler(); return await getUsersWithChatInfoHandler();
}), }),
genIntestazione: adminProcedure
.input(
z.object({
userId: zUserId,
}),
)
.mutation(async ({ input }) => {
return await genUserIntestazione(input.userId);
}),
}); });

View file

@ -1,9 +1,17 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { format } from "date-fns";
import { jsonArrayFrom } from "kysely/helpers/postgres"; import { jsonArrayFrom } from "kysely/helpers/postgres";
import {
ITALY_CATASTO_CODE,
parseDBComune,
parseDBNazione,
} from "~/lib/catasto";
import type { Etichette } from "~/schemas/public/Etichette"; import type { Etichette } from "~/schemas/public/Etichette";
import type { Users, UsersId, UsersUpdate } from "~/schemas/public/Users"; import type { Users, UsersId, UsersUpdate } from "~/schemas/public/Users";
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
import { getComuni, getNazioni } from "~/server/controllers/catasto.controller";
import { db, type Querier } from "~/server/db"; import { db, type Querier } from "~/server/db";
import { startsWithVowel, titleCase } from "~/utils/utils";
export const editAccountHandler = async ({ export const editAccountHandler = async ({
id, id,
@ -214,3 +222,84 @@ export const getUsersWithChatInfoHandler = async (): Promise<
}); });
} }
}; };
export const genUserIntestazione = async (userId: UsersId) => {
try {
const data = await db
.selectFrom("users")
.innerJoin("users_anagrafica", "users_anagrafica.userid", "users.id")
.where("id", "=", userId)
.select([
"users.cognome",
"users.nome",
"users_anagrafica.data_nascita",
"users_anagrafica.luogo_nascita",
"users_anagrafica.nazione_nascita",
"users_anagrafica.sesso",
"users_anagrafica.via_residenza",
"users_anagrafica.civico_residenza",
"users_anagrafica.comune_residenza",
"users_anagrafica.provincia_residenza",
"users_anagrafica.cap_residenza",
"users_anagrafica.codice_fiscale",
])
.executeTakeFirstOrThrow(
() => new Error(`User not found - userId: ${userId}`),
);
if (
!data.sesso ||
!data.cognome ||
!data.nome ||
!data.data_nascita ||
!data.luogo_nascita ||
!data.nazione_nascita ||
!data.comune_residenza ||
!data.via_residenza ||
!data.codice_fiscale
) {
throw new Error(
`Dati anagrafici incompleti per generazione intestazione - userId: ${userId}`,
);
}
const comuni = await getComuni();
const nazioni = await getNazioni();
const parsedComuneResidenza = parseDBComune(data.comune_residenza, comuni);
const parsedComuneNascita = parseDBComune(data.luogo_nascita, comuni);
const parsedNazione = parseDBNazione(data.nazione_nascita, nazioni);
const comuneNascita = parsedComuneNascita
? titleCase(parsedComuneNascita.nome)
: data.luogo_nascita;
const comuneResidenza = parsedComuneResidenza
? titleCase(parsedComuneResidenza.nome)
: data.comune_residenza;
const nazioneNascita = parsedNazione ? titleCase(parsedNazione.nome) : "";
// Determine article
const isMale = data.sesso === "M";
const articolo = isMale ? "Al Sig." : "Alla Sig.ra";
const natoNata = isMale ? "nato" : "nata";
const preposizione1 = startsWithVowel(comuneNascita) ? "ad" : "a";
const preposizione2 = startsWithVowel(comuneResidenza) ? "ad" : "a";
// Build phrase
const frase = [
`${articolo} ${data.cognome.toUpperCase()} ${data.nome.toUpperCase()}`,
`${natoNata} il ${format(data.data_nascita, "dd/MM/yyyy")}`,
`${preposizione1} ${comuneNascita}${parsedNazione?.catasto === ITALY_CATASTO_CODE ? (parsedComuneNascita ? ` (${parsedComuneNascita.sigla})` : "") : ` (${nazioneNascita})`},`,
`residente in ${titleCase(data.via_residenza)}, ${data.civico_residenza}`,
`${preposizione2} ${comuneResidenza} (${data.provincia_residenza})`,
`CAP ${data.cap_residenza},`,
`con codice fiscale: ${data.codice_fiscale}`,
].join(" ");
return frase;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Errore genUserIntestazione - userId: ${userId}: ${(e as Error).message}`,
});
}
};

View file

@ -15,3 +15,19 @@ export const listWithAnd = (list: string[]) => {
} }
return `${list.slice(0, -1).join(", ")}, and ${list.at(-1)}`; return `${list.slice(0, -1).join(", ")}, and ${list.at(-1)}`;
}; };
export function titleCase(str: string | null | undefined): string {
if (!str) return "";
return str
.trim()
.toLowerCase()
.split(/\s+/) // Handle multiple spaces
.map((word) => {
if (word.length === 0) return word;
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(" ");
}
export const startsWithVowel = (s: string) => /^[aeiouàèéìòù]/i.test(s);