From a04e3c2fd3c0ec1f7649ae1842887a2775702501 Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Tue, 10 Mar 2026 16:56:26 +0100 Subject: [PATCH] feat: implement user labels management with CRUD operations and UI integration --- apps/db/migrations/36_user_etichette.up.sql | 34 +++ apps/infoalloggi/src/components/etichette.tsx | 2 +- .../src/components/tables/users-table.tsx | 205 +++++++++++++++++- .../src/schemas/public/PublicSchema.ts | 3 + .../src/schemas/public/UserEtichette.ts | 19 ++ .../src/server/api/routers/etichette.ts | 48 +++- .../src/server/api/routers/users.ts | 2 +- .../src/server/controllers/user.controller.ts | 20 +- 8 files changed, 322 insertions(+), 11 deletions(-) create mode 100644 apps/db/migrations/36_user_etichette.up.sql create mode 100644 apps/infoalloggi/src/schemas/public/UserEtichette.ts diff --git a/apps/db/migrations/36_user_etichette.up.sql b/apps/db/migrations/36_user_etichette.up.sql new file mode 100644 index 0000000..53bf1db --- /dev/null +++ b/apps/db/migrations/36_user_etichette.up.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS public.user_etichette ( + "userId" UUID NOT NULL, + "etichettaId" integer NOT NULL, +); + +DO $$ BEGIN +ALTER TABLE public.user_etichette +ADD CONSTRAINT user_etichette_pkey PRIMARY KEY ("userId", "etichettaId"); + EXCEPTION + WHEN invalid_table_definition THEN + RAISE NOTICE 'Primary key already exists. Ignoring...'; +END $$; +-- Foreign Key to Chats +DO $$ BEGIN IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'user_etichetta_fkey' + ) THEN + ALTER TABLE public.user_etichette + ADD CONSTRAINT user_etichetta_fkey + FOREIGN KEY ("userId") REFERENCES public.users (id) + ON UPDATE CASCADE ON DELETE CASCADE; + END IF; +END $$; +-- Foreign Key to Etichette +DO $$ BEGIN IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conname = 'etichetta_user_etichetta_fkey' + ) THEN + ALTER TABLE public.user_etichette + ADD CONSTRAINT etichetta_user_etichetta_fkey + FOREIGN KEY ("etichettaId") REFERENCES public.etichette (id_etichetta) + ON UPDATE CASCADE ON DELETE CASCADE; + END IF; +END $$; \ No newline at end of file diff --git a/apps/infoalloggi/src/components/etichette.tsx b/apps/infoalloggi/src/components/etichette.tsx index 1a31997..a9165a1 100644 --- a/apps/infoalloggi/src/components/etichette.tsx +++ b/apps/infoalloggi/src/components/etichette.tsx @@ -113,7 +113,7 @@ export const EtichetteModal = ({ /> + + + )} data={tabledata} @@ -323,6 +380,140 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => { searchColumn={searchFiltro} withContextMenu={true} /> + ); }; + +const UsersEtichetteModal = ({ + selected, + setSelected, +}: { + selected: { id: UsersId; etichette: Etichette[] } | null; + setSelected: React.Dispatch< + React.SetStateAction<{ id: UsersId; etichette: Etichette[] } | null> + >; +}) => { + const utils = api.useUtils(); + + const { data: etichette } = api.etichette.getAllEtichette.useQuery(); + + const { mutate: addEtichetta } = api.etichette.addUserEtichetta.useMutation({ + onSuccess: async () => { + toast.success("Etichetta aggiunta con successo"); + await utils.etichette.getAllEtichette.invalidate(); + await utils.users.getUsersWChatInfo.invalidate(); + }, + onError: () => { + toast.error("Errore nell'aggiunta dell'etichetta"); + }, + }); + const { mutate: removeEtichetta } = + api.etichette.removeUserEtichetta.useMutation({ + onSuccess: async () => { + toast.success("Etichetta rimossa con successo"); + await utils.etichette.getAllEtichette.invalidate(); + await utils.users.getUsersWChatInfo.invalidate(); + }, + onError: () => { + toast.error("Errore nella rimozione dell'etichetta"); + }, + }); + + return ( + { + if (!v) setSelected(null); + }} + open={!!selected} + > + + + Etichette + Etichette + +
+
+

Etichette Attive

+ {selected?.etichette.map((etichetta) => ( +
+ + +
+ ))} +
+ +
+

Aggiungi Etichetta

+ {etichette + ?.filter( + (e) => + !selected?.etichette.some( + (etichetta) => etichetta.id_etichetta === e.id_etichetta, + ), + ) + .map((e) => ( +
+ + +
+ ))} +
+
+
+
+ ); +}; diff --git a/apps/infoalloggi/src/schemas/public/PublicSchema.ts b/apps/infoalloggi/src/schemas/public/PublicSchema.ts index 3696244..5b9aedf 100644 --- a/apps/infoalloggi/src/schemas/public/PublicSchema.ts +++ b/apps/infoalloggi/src/schemas/public/PublicSchema.ts @@ -7,6 +7,7 @@ import type { default as AnnunciTable } from './Annunci'; import type { default as UsersTable } from './Users'; import type { default as ChatsEtichetteTable } from './ChatsEtichette'; import type { default as TestiEStringheTable } from './TestiEStringhe'; +import type { default as UserEtichetteTable } from './UserEtichette'; import type { default as UsersStorageTable } from './UsersStorage'; import type { default as ServizioAnnunciTable } from './ServizioAnnunci'; import type { default as UserInvitesTable } from './UserInvites'; @@ -43,6 +44,8 @@ export default interface PublicSchema { testi_e_stringhe: TestiEStringheTable; + user_etichette: UserEtichetteTable; + users_storage: UsersStorageTable; servizio_annunci: ServizioAnnunciTable; diff --git a/apps/infoalloggi/src/schemas/public/UserEtichette.ts b/apps/infoalloggi/src/schemas/public/UserEtichette.ts new file mode 100644 index 0000000..278e25a --- /dev/null +++ b/apps/infoalloggi/src/schemas/public/UserEtichette.ts @@ -0,0 +1,19 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { UsersId } from './Users'; +import type { EtichetteIdEtichetta } from './Etichette'; +import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; + +/** Represents the table public.user_etichette */ +export default interface UserEtichetteTable { + userId: ColumnType; + + etichettaId: ColumnType; +} + +export type UserEtichette = Selectable; + +export type NewUserEtichette = Insertable; + +export type UserEtichetteUpdate = Updateable; diff --git a/apps/infoalloggi/src/server/api/routers/etichette.ts b/apps/infoalloggi/src/server/api/routers/etichette.ts index 28f16d5..ff84c9b 100644 --- a/apps/infoalloggi/src/server/api/routers/etichette.ts +++ b/apps/infoalloggi/src/server/api/routers/etichette.ts @@ -1,3 +1,4 @@ +import { TRPCError } from "@trpc/server"; import { z } from "zod/v4"; import { adminProcedure, createTRPCRouter } from "~/server/api/trpc"; import { @@ -7,7 +8,7 @@ import { updateEtichetta, } from "~/server/controllers/etichette.controller"; import { db } from "~/server/db"; -import { zEtichettaId } from "~/server/utils/zod_types"; +import { zEtichettaId, zUserId } from "~/server/utils/zod_types"; export const etichetteRouter = createTRPCRouter({ addEtichetta: adminProcedure @@ -51,4 +52,49 @@ export const etichetteRouter = createTRPCRouter({ title: input.title, }); }), + addUserEtichetta: adminProcedure + .input( + z.object({ + userId: zUserId, + etichettaId: zEtichettaId, + }), + ) + .mutation(async ({ input }) => { + try { + await db + .insertInto("user_etichette") + .values({ + etichettaId: input.etichettaId, + userId: input.userId, + }) + .execute(); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Errore aggiunta etichetta all'utente: ${error instanceof Error ? error.message : "Errore sconosciuto"}`, + }); + } + }), + removeUserEtichetta: adminProcedure + .input( + z.object({ + userId: zUserId, + etichettaId: zEtichettaId, + }), + ) + .mutation(async ({ input }) => { + try { + await db + .deleteFrom("user_etichette") + .where("etichettaId", "=", input.etichettaId) + .where("userId", "=", input.userId) + .execute(); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Errore rimozione etichetta all'utente: ${error instanceof Error ? error.message : "Errore sconosciuto"}`, + }); + } + return "ok"; + }), }); diff --git a/apps/infoalloggi/src/server/api/routers/users.ts b/apps/infoalloggi/src/server/api/routers/users.ts index fecfd2f..cd3a1ae 100644 --- a/apps/infoalloggi/src/server/api/routers/users.ts +++ b/apps/infoalloggi/src/server/api/routers/users.ts @@ -100,6 +100,6 @@ export const usersRouter = createTRPCRouter({ }), getUsersWChatInfo: adminProcedure.query(async () => { - return await getUsersWithChatInfoHandler({ db }); + return await getUsersWithChatInfoHandler(); }), }); diff --git a/apps/infoalloggi/src/server/controllers/user.controller.ts b/apps/infoalloggi/src/server/controllers/user.controller.ts index cfc4457..8c35311 100644 --- a/apps/infoalloggi/src/server/controllers/user.controller.ts +++ b/apps/infoalloggi/src/server/controllers/user.controller.ts @@ -1,4 +1,5 @@ import { TRPCError } from "@trpc/server"; +import { jsonArrayFrom } from "kysely/helpers/postgres"; import type { UsersId, UsersUpdate } from "~/schemas/public/Users"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; import { db, type Querier } from "~/server/db"; @@ -167,7 +168,7 @@ export const getActiveUserWithoutChatHandler = async ({ }); }; -export const getUsersWithChatInfoHandler = async ({ db }: { db: Querier }) => { +export const getUsersWithChatInfoHandler = async () => { return await db .selectFrom("users") .select([ @@ -181,6 +182,23 @@ export const getUsersWithChatInfoHandler = async ({ db }: { db: Querier }) => { ]) .leftJoin("chats", "chats.user_ref", "users.id") .select(["chats.chatid"]) + .select((eb) => + jsonArrayFrom( + eb + .selectFrom("etichette") + .innerJoin( + "user_etichette", + "user_etichette.etichettaId", + "etichette.id_etichetta", + ) + .whereRef("user_etichette.userId", "=", "users.id") + .select([ + "etichette.id_etichetta", + "etichette.title", + "etichette.color_hex", + ]), + ).as("etichette"), + ) .orderBy("users.id", "desc") .select(["users.id as id"]) .execute();