feat: implement Potenziali and PotenzialiGroups management with CRUD operations and UI enhancements

This commit is contained in:
Marco Pedone 2025-12-03 13:56:53 +01:00
parent 3143870cfd
commit cfcddea9c8
11 changed files with 673 additions and 142 deletions

View file

@ -0,0 +1,37 @@
-- Potenziali table
CREATE TABLE IF NOT EXISTS public.potenziali (
id UUID DEFAULT gen_random_uuid () NOT NULL,
name TEXT NOT NULL,
descrizione TEXT,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL,
userid UUID,
potenziali_group_id UUID NOT NULL
);
-- Primary Key
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'potenziali_pkey'
AND conrelid = 'public.potenziali'::regclass
) THEN
ALTER TABLE public.potenziali
ADD CONSTRAINT potenziali_pkey PRIMARY KEY (id);
END IF;
END $$;
-- Foreign Key to Users
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'potenziali_assigned_user'
AND conrelid = 'public.potenziali'::regclass
) THEN
ALTER TABLE public.potenziali
ADD CONSTRAINT potenziali_assigned_user
FOREIGN KEY (userid) REFERENCES public.users (id)
ON UPDATE CASCADE ON DELETE SET NULL;
END IF;
END $$;

View file

@ -0,0 +1,45 @@
-- Potenziali Groups Table
CREATE TABLE IF NOT EXISTS public.potenziali_groups (
id UUID DEFAULT gen_random_uuid () NOT NULL,
name TEXT NOT NULL,
color TEXT DEFAULT 'blue' NOT NULL
);
-- Primary Key
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'potenziali_groups_pkey'
AND conrelid = 'public.potenziali_groups'::regclass
) THEN
ALTER TABLE public.potenziali_groups
ADD CONSTRAINT potenziali_groups_pkey PRIMARY KEY (id);
END IF;
END $$;
-- Foreign Key to Potenziali Groups
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'potenziali_group_fk'
AND conrelid = 'public.potenziali'::regclass
) THEN
ALTER TABLE public.potenziali
ADD CONSTRAINT potenziali_group_fk
FOREIGN KEY (potenziali_group_id) REFERENCES public.potenziali_groups (id)
ON UPDATE CASCADE ON DELETE CASCADE;
END IF;
END $$;
-- Create Index on Potenziali Group ID
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE indexname = 'idx_potenziali_group_id'
) THEN
CREATE INDEX idx_potenziali_group_id
ON public.potenziali (potenziali_group_id);
END IF;
END $$;

View file

@ -1,6 +1,8 @@
"use client"; "use client";
import { format } from "date-fns"; import { format } from "date-fns";
import { Plus } from "lucide-react";
import { useState } from "react";
import { import {
KanbanBoard, KanbanBoard,
KanbanCard, KanbanCard,
@ -8,12 +10,9 @@ import {
KanbanHeader, KanbanHeader,
KanbanProvider, KanbanProvider,
} from "~/components/ui/kanban"; } from "~/components/ui/kanban";
import { import { usePotenziali } from "~/providers/PotenzialiProvider";
type ElementoPotenziali, import type { PotenzialiGroupsId } from "~/schemas/public/PotenzialiGroups";
PotenzialiProvider, import type { PotenzialeData } from "~/server/services/potenziali.service";
usePotenziali,
} from "~/providers/PotenzialiProvider";
import type { UsersId } from "~/schemas/public/Users";
import { import {
Credenza, Credenza,
CredenzaBody, CredenzaBody,
@ -21,81 +20,27 @@ import {
CredenzaDescription, CredenzaDescription,
CredenzaHeader, CredenzaHeader,
CredenzaTitle, CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza"; } from "../custom_ui/credenza";
import { import Input from "../custom_ui/input";
ContextMenu, import { Textarea } from "../custom_ui/textarea";
ContextMenuContent, import { Button } from "../ui/button";
ContextMenuItem, import { Label } from "../ui/label";
ContextMenuTrigger,
} from "../ui/context-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../ui/dialog";
import { UserAvatar } from "../user_avatar"; import { UserAvatar } from "../user_avatar";
const columns = [ export const Potenziali = () => {
{ id: "todo", name: "To Do", color: "red" }, const { data, columns, setData, updateCall, setEditingId, editingId } =
{ id: "inprogress", name: "In Progress", color: "blue" }, usePotenziali();
{ id: "done", name: "Done", color: "green" },
];
const SampleData: ElementoPotenziali[] = [
{
id: "1",
name: "Mario Rossi",
column: "todo",
descrizione:
"Single, cerca appartamento in centro da subito, budget 800 euro",
createdAt: new Date(),
owner: {
userId: "019b9a25-eff1-420a-9017-b0add205d7b6" as UsersId,
username: "Marco Pedone",
},
},
{
id: "2",
name: "Luigi Bianchi",
column: "inprogress",
descrizione: "Coppia, cerca bilocale in periferia, budget 600 euro",
createdAt: new Date(),
owner: null,
},
{
id: "3",
name: "Giovanni Verdi",
column: "done",
descrizione:
"Famiglia con 2 figli, cerca trilocale vicino a scuole, budget 1000 euro",
createdAt: new Date(),
owner: {
userId: "019b9a25-eff1-420a-9017-b0add205d7b6" as UsersId,
username: "Marco Pedone",
},
},
];
const Potenziali = () => {
return (
<PotenzialiProvider data={SampleData}>
<Kaban />
</PotenzialiProvider>
);
};
const Kaban = () => {
const { data, updateData, setEditingId, editingId } = usePotenziali();
return ( return (
<> <>
<KanbanProvider <KanbanProvider
className="w-max" className="w-max"
columns={columns} columns={columns}
data={data} data={data}
onDataChange={updateData} onDataChange={setData}
onDragEnd={() => {
updateCall();
}}
> >
{(column) => ( {(column) => (
<KanbanBoard <KanbanBoard
@ -110,10 +55,12 @@ const Kaban = () => {
style={{ backgroundColor: column.color }} style={{ backgroundColor: column.color }}
/> />
<span>{column.name}</span> <span>{column.name}</span>
<NewCard columnId={column.id} />
</div> </div>
</KanbanHeader> </KanbanHeader>
<KanbanCards className="w-68" id={column.id}> <KanbanCards className="w-68" id={column.id}>
{(data: ElementoPotenziali) => ( {(data: PotenzialeData) => (
<KanbanCard <KanbanCard
cardOnClick={() => { cardOnClick={() => {
setEditingId(data.id); setEditingId(data.id);
@ -132,16 +79,16 @@ const Kaban = () => {
{data.descrizione} {data.descrizione}
</p> </p>
</div> </div>
{data.owner && ( {data.userid && (
<UserAvatar <UserAvatar
className="size-6 shrink-0 text-sm" className="size-6 shrink-0 text-sm"
userId={data.owner.userId} userId={data.userid}
username={data.owner.username} username={data.username || "Utente"}
/> />
)} )}
</div> </div>
<p className="mt-2 text-muted-foreground text-xs"> <p className="mt-2 text-muted-foreground text-xs">
{format(data.createdAt, "dd/MM/yyyy HH:mm")} {format(data.created_at, "dd/MM/yyyy HH:mm")}
</p> </p>
</KanbanCard> </KanbanCard>
)} )}
@ -163,6 +110,101 @@ const Kaban = () => {
); );
}; };
export const NewColumn = () => {
const [newColumnName, setNewColumnName] = useState("");
const { addColumn } = usePotenziali();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button>Nuova colonna</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle className="text-center font-semibold text-2xl">
Nuova colonna
</CredenzaTitle>
<CredenzaDescription className="sr-only">desc</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="flex max-h-[80vh] flex-col gap-4 overflow-auto p-2">
<Label htmlFor="titolo">Titolo</Label>
<Input
defaultValue={newColumnName}
id="titolo"
onChange={(e) => setNewColumnName(e.target.value)}
type="string"
/>
<Button
disabled={!newColumnName.length}
onClick={() => {
addColumn(newColumnName);
setNewColumnName("");
}}
>
Aggiungi
</Button>
</CredenzaBody>
</CredenzaContent>
</Credenza>
);
};
export const NewCard = ({ columnId }: { columnId: PotenzialiGroupsId }) => {
const [newItemName, setNewItemName] = useState("");
const [newItemDescrizione, setNewItemDescrizione] = useState("");
const { addItem } = usePotenziali();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button className="ml-auto" size="sm">
<Plus className="size-4" />
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle className="text-center font-semibold text-2xl">
Nuova colonna
</CredenzaTitle>
<CredenzaDescription className="sr-only">desc</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="flex max-h-[80vh] flex-col gap-4 overflow-auto p-2">
<Label htmlFor="titolo">Titolo</Label>
<Input
defaultValue={newItemName}
id="titolo"
onChange={(e) => setNewItemName(e.target.value)}
type="string"
/>
<Label htmlFor="descrizione">Descrizione</Label>
<Textarea
className="min-h-64"
defaultValue={newItemDescrizione}
id="descrizione"
onChange={(e) => setNewItemDescrizione(e.target.value)}
placeholder=""
/>
<Button
disabled={!newItemName.length}
onClick={() => {
addItem({
name: newItemName,
descrizione: newItemDescrizione,
potenziali_group_id: columnId,
});
setNewItemName("");
setNewItemDescrizione("");
}}
>
Aggiungi
</Button>
</CredenzaBody>
</CredenzaContent>
</Credenza>
);
};
const EditingContent = () => { const EditingContent = () => {
const { getEditingItem } = usePotenziali(); const { getEditingItem } = usePotenziali();
const item = getEditingItem(); const item = getEditingItem();
@ -177,11 +219,9 @@ const EditingContent = () => {
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody className="flex max-h-[80vh] flex-col gap-4 overflow-auto pb-5"> <CredenzaBody className="flex max-h-[80vh] flex-col gap-4 overflow-auto pb-5">
<p>Descrizione: {item.descrizione}</p> <p>Descrizione: {item.descrizione}</p>
<p>Creato il: {format(item.createdAt, "dd/MM/yyyy HH:mm")}</p> <p>Creato il: {format(item.created_at, "dd/MM/yyyy HH:mm")}</p>
<p>Utente Assegnato: {item.owner ? item.owner.username : "Nessuno"}</p> <p>Utente Assegnato: {item.userid ? item.username : "Nessuno"}</p>
</CredenzaBody> </CredenzaBody>
</CredenzaContent> </CredenzaContent>
); );
}; };
export default Potenziali;

View file

@ -1,53 +1,43 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { NewColumn } from "~/components/area-riservata/potenziali";
import { AreaRiservataLayout } from "~/components/Layout"; import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import { PotenzialiProvider } from "~/providers/PotenzialiProvider";
const Potenziali = dynamic( const PotenzialiBoard = dynamic(
() => import("~/components/area-riservata/potenziali"), () =>
import("~/components/area-riservata/potenziali").then(
(mod) => mod.Potenziali,
),
{ {
loading: () => <LoadingPage />,
ssr: false, ssr: false,
loading: () => <LoadingPage />,
}, },
); );
const AdminPotenziali: NextPageWithLayout = () => { const AdminPotenziali: NextPageWithLayout = () => {
return ( return (
<div className="mx-1 flex-1 overflow-auto"> <PotenzialiProvider>
<div className="mx-auto pt-4"> <div className="mx-1 flex-1 overflow-auto">
<div className="mx-3 items-start justify-between md:flex"> <div className="mx-auto pt-4">
<div className="flex max-w-lg items-center gap-3"> <div className="mx-3 flex-wrap items-start justify-between pb-5 sm:flex">
<h3 className="font-bold text-accent-foreground text-xl sm:text-2xl"> <div className="flex max-w-lg items-center gap-3">
Potenziali <h3 className="font-bold text-accent-foreground text-xl sm:text-2xl">
</h3> Potenziali
</h3>
</div>
<NewColumn />
</div> </div>
</div>
<div className="p-4"> <PotenzialiBoard />
<Potenziali />
</div> </div>
</div> </div>
</div> </PotenzialiProvider>
); );
}; };
export default AdminPotenziali; export default AdminPotenziali;
// export const getServerSideProps = (async (context) => {
// const access_token = context.req.cookies.access_token;
// const helpers = await TrpcAuthedFetchingIstance({ access_token });
// if (helpers) {
// await helpers.trpc.etichette.getAllEtichette.prefetch();
// return {
// props: {
// trpcState: helpers.trpc.dehydrate(),
// },
// };
// }
// return {
// props: {},
// };
// }) satisfies GetServerSideProps;
AdminPotenziali.getLayout = function getLayout(page) { AdminPotenziali.getLayout = function getLayout(page) {
return <AreaRiservataLayout>{page}</AreaRiservataLayout>; return <AreaRiservataLayout>{page}</AreaRiservataLayout>;
}; };

View file

@ -1,56 +1,109 @@
import { createContext, useContext, useState } from "react"; import { createContext, useContext, useState } from "react";
import type { UsersId } from "~/schemas/public/Users"; import toast from "react-hot-toast";
import type { NewPotenziali, PotenzialiId } from "~/schemas/public/Potenziali";
import type { PotenzialiGroups } from "~/schemas/public/PotenzialiGroups";
import type { PotenzialeData } from "~/server/services/potenziali.service";
import { api } from "~/utils/api";
export type ElementoPotenziali = {
id: string;
name: string;
column: string;
descrizione: string;
createdAt: Date;
owner: { userId: UsersId; username: string } | null;
};
type PotenzialiContextType = { type PotenzialiContextType = {
data: ElementoPotenziali[]; data: PotenzialeData[];
editingId: string | null; columns: PotenzialiGroups[];
updateData: (data: ElementoPotenziali[]) => void; editingId: PotenzialiId | null;
setEditingId: (id: string | null) => void; setData: (data: PotenzialeData[]) => void;
getEditingItem: () => ElementoPotenziali | null; updateCall: () => void;
addItem: (item: ElementoPotenziali) => void; setEditingId: (id: PotenzialiId | null) => void;
removeItem: (id: string) => void; getEditingItem: () => PotenzialeData | null;
addItem: (item: NewPotenziali) => void;
removeItem: (id: PotenzialiId) => void;
addColumn: (name: string) => void;
}; };
const PotenzialiContext = createContext<PotenzialiContextType | null>(null); const PotenzialiContext = createContext<PotenzialiContextType | null>(null);
type PotenzialiProviderProps = { type PotenzialiProviderProps = {
children: React.ReactNode; children: React.ReactNode;
data: ElementoPotenziali[];
}; };
export const PotenzialiProvider = ({ export const PotenzialiProvider = ({ children }: PotenzialiProviderProps) => {
children, const { data } = api.potenziali.getPotenziali.useQuery();
data: initial,
}: PotenzialiProviderProps) => { const { data: columns } = api.potenziali.getPotenzialiGroups.useQuery();
const [data, setData] = useState<ElementoPotenziali[]>(initial);
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<PotenzialiId | null>(null);
const getEditingItem = () => { const getEditingItem = () => {
return data.find((item) => item.id === editingId) || null; return data?.find((item) => item.id === editingId) || null;
}; };
const addItem = (item: ElementoPotenziali) => { const utils = api.useUtils();
setData((prev) => [...prev, item]); const { mutate: addItemMutation } = api.potenziali.addPotenziale.useMutation({
onSuccess: async () => {
toast.success("Elemento aggiunto con successo");
await utils.potenziali.getPotenziali.invalidate();
await utils.potenziali.getPotenzialiGroups.invalidate();
},
});
const addItem = (item: NewPotenziali) => {
addItemMutation({
data: item,
});
}; };
const removeItem = (id: string) => { const { mutate: removeItemMutation } =
setData((prev) => prev.filter((item) => item.id !== id)); api.potenziali.deletePotenziale.useMutation({
onSuccess: async () => {
toast.success("Elemento rimosso con successo");
await utils.potenziali.getPotenziali.invalidate();
await utils.potenziali.getPotenzialiGroups.invalidate();
},
});
const removeItem = (id: PotenzialiId) => {
removeItemMutation({
potenzialiId: id,
});
}; };
const setData = (newData: PotenzialeData[]) => {
utils.potenziali.getPotenziali.setData(undefined, newData);
};
const { mutate: updatePotenzialiMutation } =
api.potenziali.updatePotenziali.useMutation({
onSuccess: async () => {
await utils.potenziali.getPotenziali.invalidate();
},
});
const updateCall = () => {
updatePotenzialiMutation({
data: utils.potenziali.getPotenziali.getData() || [],
});
};
const { mutate: addColumnMutation } =
api.potenziali.addPotenzialeGroup.useMutation({
onSuccess: async () => {
toast.success("Colonna aggiunta con successo");
await utils.potenziali.getPotenziali.invalidate();
await utils.potenziali.getPotenzialiGroups.invalidate();
},
});
const addColumn = (name: string) => {
addColumnMutation({
name,
});
};
return ( return (
<PotenzialiContext.Provider <PotenzialiContext.Provider
value={{ value={{
data, data: data || [],
columns: columns || [],
editingId, editingId,
updateData: setData, setData,
updateCall,
setEditingId, setEditingId,
getEditingItem, getEditingItem,
addItem, addItem,
removeItem, removeItem,
addColumn,
}} }}
> >
{children} {children}

View file

@ -0,0 +1,30 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { UsersId } from './Users';
import type { PotenzialiGroupsId } from './PotenzialiGroups';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.potenziali */
export type PotenzialiId = string & { __brand: 'public.potenziali' };
/** Represents the table public.potenziali */
export default interface PotenzialiTable {
id: ColumnType<PotenzialiId, PotenzialiId | undefined, PotenzialiId>;
name: ColumnType<string, string, string>;
descrizione: ColumnType<string | null, string | null, string | null>;
created_at: ColumnType<Date, Date | string | undefined, Date | string>;
userid: ColumnType<UsersId | null, UsersId | null, UsersId | null>;
potenziali_group_id: ColumnType<PotenzialiGroupsId, PotenzialiGroupsId, PotenzialiGroupsId>;
}
export type Potenziali = Selectable<PotenzialiTable>;
export type NewPotenziali = Insertable<PotenzialiTable>;
export type PotenzialiUpdate = Updateable<PotenzialiTable>;

View file

@ -0,0 +1,22 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.potenziali_groups */
export type PotenzialiGroupsId = string & { __brand: 'public.potenziali_groups' };
/** Represents the table public.potenziali_groups */
export default interface PotenzialiGroupsTable {
id: ColumnType<PotenzialiGroupsId, PotenzialiGroupsId | undefined, PotenzialiGroupsId>;
name: ColumnType<string, string, string>;
color: ColumnType<string, string | undefined, string>;
}
export type PotenzialiGroups = Selectable<PotenzialiGroupsTable>;
export type NewPotenzialiGroups = Insertable<PotenzialiGroupsTable>;
export type PotenzialiGroupsUpdate = Updateable<PotenzialiGroupsTable>;

View file

@ -11,6 +11,7 @@ import type { default as UsersStorageTable } from './UsersStorage';
import type { default as ServizioAnnunciTable } from './ServizioAnnunci'; import type { default as ServizioAnnunciTable } from './ServizioAnnunci';
import type { default as EmailsTable } from './Emails'; import type { default as EmailsTable } from './Emails';
import type { default as PaymentsTable } from './Payments'; import type { default as PaymentsTable } from './Payments';
import type { default as PotenzialiGroupsTable } from './PotenzialiGroups';
import type { default as ComuniTable } from './Comuni'; import type { default as ComuniTable } from './Comuni';
import type { default as EventQueueTable } from './EventQueue'; import type { default as EventQueueTable } from './EventQueue';
import type { default as OrdiniTable } from './Ordini'; import type { default as OrdiniTable } from './Ordini';
@ -21,6 +22,7 @@ import type { default as EtichetteTable } from './Etichette';
import type { default as MessagesTable } from './Messages'; import type { default as MessagesTable } from './Messages';
import type { default as ProvincieTable } from './Provincie'; import type { default as ProvincieTable } from './Provincie';
import type { default as UsersAnagraficaTable } from './UsersAnagrafica'; import type { default as UsersAnagraficaTable } from './UsersAnagrafica';
import type { default as PotenzialiTable } from './Potenziali';
import type { default as VideosRefsTable } from './VideosRefs'; import type { default as VideosRefsTable } from './VideosRefs';
import type { default as BanlistTable } from './Banlist'; import type { default as BanlistTable } from './Banlist';
import type { default as NazioniTable } from './Nazioni'; import type { default as NazioniTable } from './Nazioni';
@ -49,6 +51,8 @@ export default interface PublicSchema {
payments: PaymentsTable; payments: PaymentsTable;
potenziali_groups: PotenzialiGroupsTable;
comuni: ComuniTable; comuni: ComuniTable;
event_queue: EventQueueTable; event_queue: EventQueueTable;
@ -69,6 +73,8 @@ export default interface PublicSchema {
users_anagrafica: UsersAnagraficaTable; users_anagrafica: UsersAnagraficaTable;
potenziali: PotenzialiTable;
videos_refs: VideosRefsTable; videos_refs: VideosRefsTable;
banlist: BanlistTable; banlist: BanlistTable;

View file

@ -22,6 +22,7 @@ import { bannersRouter } from "./routers/banners";
import { catastoRouter } from "./routers/catasto"; import { catastoRouter } from "./routers/catasto";
import { etichetteRouter } from "./routers/etichette"; import { etichetteRouter } from "./routers/etichette";
import { flagsRouter } from "./routers/flags"; import { flagsRouter } from "./routers/flags";
import { potenzialiRouter } from "./routers/potenziali";
import { revalidationRouter } from "./routers/revalidation"; import { revalidationRouter } from "./routers/revalidation";
import { stringsRouter } from "./routers/strings"; import { stringsRouter } from "./routers/strings";
import { syncRouter } from "./routers/sync"; import { syncRouter } from "./routers/sync";
@ -55,5 +56,6 @@ export const appRouter = createTRPCRouter({
etichette: etichetteRouter, etichette: etichetteRouter,
strings: stringsRouter, strings: stringsRouter,
revalidation: revalidationRouter, revalidation: revalidationRouter,
potenziali: potenzialiRouter,
}); });
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;

View file

@ -0,0 +1,121 @@
import z from "zod";
import type {
NewPotenziali,
PotenzialiId,
PotenzialiUpdate,
} from "~/schemas/public/Potenziali";
import type { PotenzialiGroupsId } from "~/schemas/public/PotenzialiGroups";
import { adminProcedure, createTRPCRouter } from "~/server/api/trpc";
import {
addPotenziale,
addPotenzialeGroup,
deletePotenziale,
deletePotenzialeGroup,
getPotenziali,
getPotenzialiGroups,
type PotenzialeData,
updatePotenziale,
updatePotenzialeGroup,
updatePotenziali,
} from "~/server/services/potenziali.service";
export const zPotenzialiId = z.custom<PotenzialiId>(
(val) => typeof val === "string" && val.length > 0,
{ message: "Invalid PotenzialiId" },
);
export const zNewPotenziali = z.custom<NewPotenziali>(
(val) => {
return typeof val === "object" && val !== null;
},
{ message: "Invalid NewPotenziali" },
);
export const zPotenzialiUpdate = z.custom<PotenzialiUpdate>(
(val) => {
return typeof val === "object" && val !== null;
},
{ message: "Invalid PotenzialiUpdate" },
);
export const zPotenzialiGroupId = z.custom<PotenzialiGroupsId>(
(val) => typeof val === "string" && val.length > 0,
{ message: "Invalid PotenzialiGroupsId" },
);
export const potenzialiRouter = createTRPCRouter({
getPotenziali: adminProcedure.query(async () => {
return await getPotenziali();
}),
getPotenzialiGroups: adminProcedure.query(async () => {
return await getPotenzialiGroups();
}),
addPotenziale: adminProcedure
.input(
z.object({
data: zNewPotenziali,
}),
)
.mutation(async ({ input }) => {
return await addPotenziale(input.data);
}),
updatePotenziali: adminProcedure
.input(
z.object({
data: z.custom<PotenzialeData[]>(),
}),
)
.mutation(async ({ input }) => {
return await updatePotenziali(input.data);
}),
updatePotenziale: adminProcedure
.input(
z.object({
potenzialiId: zPotenzialiId,
data: zPotenzialiUpdate,
}),
)
.mutation(async ({ input }) => {
return await updatePotenziale(input.potenzialiId, input.data);
}),
deletePotenziale: adminProcedure
.input(
z.object({
potenzialiId: zPotenzialiId,
}),
)
.mutation(async ({ input }) => {
return await deletePotenziale(input.potenzialiId);
}),
addPotenzialeGroup: adminProcedure
.input(
z.object({
name: z.string().min(1).max(255),
}),
)
.mutation(async ({ input }) => {
return await addPotenzialeGroup({ name: input.name });
}),
updatePotenzialeGroup: adminProcedure
.input(
z.object({
potenzialiGroupId: zPotenzialiGroupId,
name: z.string().min(1).max(255),
}),
)
.mutation(async ({ input }) => {
return await updatePotenzialeGroup(input.potenzialiGroupId, {
name: input.name,
});
}),
deletePotenzialeGroup: adminProcedure
.input(
z.object({
potenzialiGroupId: zPotenzialiGroupId,
}),
)
.mutation(async ({ input }) => {
return await deletePotenzialeGroup(input.potenzialiGroupId);
}),
});

View file

@ -0,0 +1,185 @@
import { TRPCError } from "@trpc/server";
import type { Nullable } from "kysely";
import type {
NewPotenziali,
Potenziali,
PotenzialiId,
PotenzialiUpdate,
} from "~/schemas/public/Potenziali";
import type {
NewPotenzialiGroups,
PotenzialiGroups,
PotenzialiGroupsId,
PotenzialiGroupsUpdate,
} from "~/schemas/public/PotenzialiGroups";
import type { Users } from "~/schemas/public/Users";
import { db } from "../db";
export type PotenzialeData = Omit<Potenziali, "potenziali_group_id"> & {
column: string;
} & Nullable<Pick<Users, "username">>;
export const getPotenziali = async (): Promise<PotenzialeData[]> => {
try {
const potenziali = await db
.selectFrom("potenziali")
.select([
"potenziali.id",
"potenziali.name",
"potenziali.created_at",
"potenziali.descrizione",
"potenziali.userid",
"potenziali.potenziali_group_id as column",
])
.innerJoin(
"potenziali_groups",
"potenziali.potenziali_group_id",
"potenziali_groups.id",
)
.leftJoin("users", "potenziali.userid", "users.id")
.select("users.username")
.orderBy("potenziali_groups.name", "asc")
.orderBy("potenziali.created_at", "asc")
.execute();
return potenziali.map((data) => ({
...data,
created_at: new Date(data.created_at),
}));
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to fetch potenziali, error: ${(e as Error).message}`,
});
}
};
export const getPotenzialiGroups = async (): Promise<PotenzialiGroups[]> => {
try {
return await db.selectFrom("potenziali_groups").selectAll().execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to fetch potenziali groups, error: ${(e as Error).message}`,
});
}
};
export const addPotenzialeGroup = async (data: NewPotenzialiGroups) => {
try {
await db.insertInto("potenziali_groups").values(data).execute();
return { status: "success" };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to add potenziale group, error: ${(e as Error).message}`,
});
}
};
export const deletePotenzialeGroup = async (id: PotenzialiGroupsId) => {
try {
await db.deleteFrom("potenziali_groups").where("id", "=", id).execute();
return { status: "success" };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to delete potenziale group, error: ${(e as Error).message}`,
});
}
};
export const updatePotenzialeGroup = async (
id: PotenzialiGroupsId,
data: PotenzialiGroupsUpdate,
) => {
try {
await db
.updateTable("potenziali_groups")
.set(data)
.where("id", "=", id)
.execute();
return { status: "success" };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to update potenziale group, error: ${(e as Error).message}`,
});
}
};
export const addPotenziale = async (data: NewPotenziali) => {
try {
await db.insertInto("potenziali").values(data).execute();
return { status: "success" };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to add potenziale, error: ${(e as Error).message}`,
});
}
};
export const deletePotenziale = async (id: PotenzialiId) => {
try {
await db.deleteFrom("potenziali").where("id", "=", id).execute();
return { status: "success" };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to delete potenziale, error: ${(e as Error).message}`,
});
}
};
export const updatePotenziale = async (
id: PotenzialiId,
data: PotenzialiUpdate,
) => {
try {
await db.updateTable("potenziali").set(data).where("id", "=", id).execute();
return { status: "success" };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to update potenziale, error: ${(e as Error).message}`,
});
}
};
export const updatePotenziali = async (data: PotenzialeData[]) => {
try {
const db_mappings = await db
.selectFrom("potenziali")
.select(["id", "potenziali_group_id"])
.execute();
const updates = data
.map((item) => {
const db_item = db_mappings.find((db_item) => db_item.id === item.id);
if (db_item && db_item.potenziali_group_id !== item.column) {
const { column, username: _username, ...rest } = item;
return {
...rest,
potenziali_group_id: column as PotenzialiGroupsId,
};
}
return null;
})
.filter((item) => item !== null);
for (const item of updates) {
await db
.updateTable("potenziali")
.set(item)
.where("id", "=", item.id)
.execute();
}
return { status: "success" };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Failed to update potenziali, error: ${(e as Error).message}`,
});
}
};