From 995eadd9dc24bccdf96d1863e5b15e405e5b48d2 Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Thu, 16 Apr 2026 13:35:34 +0200 Subject: [PATCH] feat: implement user intestazione generation and update related handlers --- .../area-riservata/userViewHeader.tsx | 100 ++++-------------- .../src/server/api/routers/users.ts | 10 ++ .../src/server/controllers/user.controller.ts | 89 ++++++++++++++++ apps/infoalloggi/src/utils/utils.ts | 16 +++ 4 files changed, 134 insertions(+), 81 deletions(-) diff --git a/apps/infoalloggi/src/components/area-riservata/userViewHeader.tsx b/apps/infoalloggi/src/components/area-riservata/userViewHeader.tsx index 1d31525..39b036d 100644 --- a/apps/infoalloggi/src/components/area-riservata/userViewHeader.tsx +++ b/apps/infoalloggi/src/components/area-riservata/userViewHeader.tsx @@ -1,4 +1,3 @@ -import { format } from "date-fns"; import { ChevronDown, CloudSync, @@ -274,14 +273,22 @@ const OverseerButton = () => { ); }; -const VOCALI = ["A", "E", "I", "O", "U"] as const; - const IntestazioneMaker = () => { const data = useUserViewContext(); - const { data: comuni = [] } = api.catasto.searchComuniByCatasto.useQuery({ - catasto: [data?.luogo_nascita || "", data?.comune_residenza || ""].filter( - (f) => f !== "", - ), + const { mutate } = api.users.genIntestazione.useMutation({ + onError: (error) => { + 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 = @@ -293,86 +300,17 @@ const IntestazioneMaker = () => { !data.comune_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 ( - ); }; -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 { id, note } = useUserViewContext(); const [newNote, setNewNote] = useState(note || ""); diff --git a/apps/infoalloggi/src/server/api/routers/users.ts b/apps/infoalloggi/src/server/api/routers/users.ts index cd3a1ae..67ff0ae 100644 --- a/apps/infoalloggi/src/server/api/routers/users.ts +++ b/apps/infoalloggi/src/server/api/routers/users.ts @@ -11,6 +11,7 @@ import { deleteUserHandler, editAccountHandler, editAnagraficaHandler, + genUserIntestazione, getActiveUserWithoutChatHandler, getUserHandler, getUsersWithChatInfoHandler, @@ -102,4 +103,13 @@ export const usersRouter = createTRPCRouter({ getUsersWChatInfo: adminProcedure.query(async () => { return await getUsersWithChatInfoHandler(); }), + genIntestazione: adminProcedure + .input( + z.object({ + userId: zUserId, + }), + ) + .mutation(async ({ input }) => { + return await genUserIntestazione(input.userId); + }), }); diff --git a/apps/infoalloggi/src/server/controllers/user.controller.ts b/apps/infoalloggi/src/server/controllers/user.controller.ts index 3470905..50bd186 100644 --- a/apps/infoalloggi/src/server/controllers/user.controller.ts +++ b/apps/infoalloggi/src/server/controllers/user.controller.ts @@ -1,9 +1,17 @@ import { TRPCError } from "@trpc/server"; +import { format } from "date-fns"; import { jsonArrayFrom } from "kysely/helpers/postgres"; +import { + ITALY_CATASTO_CODE, + parseDBComune, + parseDBNazione, +} from "~/lib/catasto"; import type { Etichette } from "~/schemas/public/Etichette"; import type { Users, UsersId, UsersUpdate } from "~/schemas/public/Users"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; +import { getComuni, getNazioni } from "~/server/controllers/catasto.controller"; import { db, type Querier } from "~/server/db"; +import { startsWithVowel, titleCase } from "~/utils/utils"; export const editAccountHandler = async ({ 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}`, + }); + } +}; diff --git a/apps/infoalloggi/src/utils/utils.ts b/apps/infoalloggi/src/utils/utils.ts index f103d57..195af0c 100644 --- a/apps/infoalloggi/src/utils/utils.ts +++ b/apps/infoalloggi/src/utils/utils.ts @@ -15,3 +15,19 @@ export const listWithAnd = (list: string[]) => { } 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);