feat: enhance Potenziali management with delete functionality and user assignment

This commit is contained in:
Marco Pedone 2025-12-03 16:16:28 +01:00
parent c459da5d23
commit 2ef18a0b5b
3 changed files with 316 additions and 111 deletions

View file

@ -1,8 +1,8 @@
"use client"; "use client";
import { format } from "date-fns"; import { format } from "date-fns";
import { Plus } from "lucide-react"; import { Plus, Trash2 } from "lucide-react";
import { useState } from "react"; import { useEffect, useState } from "react";
import { import {
KanbanBoard, KanbanBoard,
KanbanCard, KanbanCard,
@ -12,7 +12,10 @@ import {
} from "~/components/ui/kanban"; } from "~/components/ui/kanban";
import { usePotenziali } from "~/providers/PotenzialiProvider"; import { usePotenziali } from "~/providers/PotenzialiProvider";
import type { PotenzialiGroupsId } from "~/schemas/public/PotenzialiGroups"; import type { PotenzialiGroupsId } from "~/schemas/public/PotenzialiGroups";
import type { UsersId } from "~/schemas/public/Users";
import type { PotenzialeData } from "~/server/services/potenziali.service"; import type { PotenzialeData } from "~/server/services/potenziali.service";
import { api } from "~/utils/api";
import { Confirm } from "../confirm";
import { import {
Credenza, Credenza,
CredenzaBody, CredenzaBody,
@ -28,11 +31,19 @@ import Input from "../custom_ui/input";
import { Textarea } from "../custom_ui/textarea"; import { Textarea } from "../custom_ui/textarea";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { UserAvatar } from "../user_avatar"; import { UserAvatar } from "../user_avatar";
export const Potenziali = () => { export const Potenziali = () => {
const { data, columns, setData, updateCall, setEditing, editingItem } = const { data, columns, setData, updateCall, removeColumn } = usePotenziali();
usePotenziali();
return ( return (
<> <>
<KanbanProvider <KanbanProvider
@ -59,24 +70,57 @@ export const Potenziali = () => {
<span>{column.name}</span> <span>{column.name}</span>
<NewCard columnId={column.id} /> <NewCard columnId={column.id} />
<Confirm
description="Sei sicuro di voler eliminare?"
onConfirm={() => {
removeColumn(column.id);
}}
title="Elimina"
>
<Button aria-label="Delete" variant="destructive">
<Trash2 />
</Button>
</Confirm>
</div> </div>
</KanbanHeader> </KanbanHeader>
<KanbanCards className="w-68" id={column.id}> <KanbanCards className="w-68" id={column.id}>
{(data: PotenzialeData) => ( {(data: PotenzialeData) => (
<PotenzialeCardItem
columnId={column.id}
data={data}
key={data.id}
/>
)}
</KanbanCards>
</KanbanBoard>
)}
</KanbanProvider>
</>
);
};
// 1. Extract the card logic into a separate component
const PotenzialeCardItem = ({
data,
columnId,
}: {
data: PotenzialeData;
columnId: string;
}) => {
const [isEditingOpen, setIsEditingOpen] = useState(false);
return (
<KanbanCard <KanbanCard
cardOnClick={() => { cardOnClick={() => {
setEditing(data.id); setIsEditingOpen(true);
}} }}
column={column.id} column={columnId}
id={data.id} id={data.id}
key={data.id} key={data.id}
name={data.name} name={data.name}
> >
<>
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<p className="m-0 flex-1 font-medium text-sm"> <p className="m-0 flex-1 font-medium text-sm">{data.name}</p>
{data.name}
</p>
<p className="m-0 text-muted-foreground text-xs"> <p className="m-0 text-muted-foreground text-xs">
{data.descrizione} {data.descrizione}
</p> </p>
@ -92,31 +136,27 @@ export const Potenziali = () => {
<p className="mt-2 text-muted-foreground text-xs"> <p className="mt-2 text-muted-foreground text-xs">
{format(data.created_at, "dd/MM/yyyy HH:mm")} {format(data.created_at, "dd/MM/yyyy HH:mm")}
</p> </p>
</KanbanCard> {/** biome-ignore lint/a11y/noNoninteractiveElementInteractions: <ok> */}
)} {/** biome-ignore lint/a11y/noStaticElementInteractions: <ok> */}
</KanbanCards> {/** biome-ignore lint/a11y/useKeyWithClickEvents: <ok> */}
</KanbanBoard> <div onClick={(e) => e.stopPropagation()}>
)} <EditingItem
</KanbanProvider> data={data}
<Credenza open={isEditingOpen}
onOpenChange={(v) => { setOpen={setIsEditingOpen}
if (!v) { />
setEditing(null); </div>
}
}}
open={!!editingItem}
>
<EditingContent />
</Credenza>
</> </>
</KanbanCard>
); );
}; };
export const NewColumn = () => { export const NewColumn = () => {
const [newColumnName, setNewColumnName] = useState(""); const [newColumnName, setNewColumnName] = useState("");
const { addColumn } = usePotenziali(); const { addColumn } = usePotenziali();
const [open, setOpen] = useState(false);
return ( return (
<Credenza> <Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button>Nuova colonna</Button> <Button>Nuova colonna</Button>
</CredenzaTrigger> </CredenzaTrigger>
@ -141,6 +181,7 @@ export const NewColumn = () => {
onClick={() => { onClick={() => {
addColumn(newColumnName); addColumn(newColumnName);
setNewColumnName(""); setNewColumnName("");
setOpen(false);
}} }}
> >
Aggiungi Aggiungi
@ -154,18 +195,32 @@ export const NewColumn = () => {
export const NewCard = ({ columnId }: { columnId: PotenzialiGroupsId }) => { export const NewCard = ({ columnId }: { columnId: PotenzialiGroupsId }) => {
const [newItemName, setNewItemName] = useState(""); const [newItemName, setNewItemName] = useState("");
const [newItemDescrizione, setNewItemDescrizione] = useState(""); const [newItemDescrizione, setNewItemDescrizione] = useState("");
const [newItemUserid, setNewItemUserid] = useState<UsersId | null>(null);
const { addItem } = usePotenziali(); const { addItem } = usePotenziali();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const resetState = () => {
setNewItemName("");
setNewItemDescrizione("");
setNewItemUserid(null);
};
const handleAddItem = () => { const handleAddItem = () => {
addItem({ addItem({
name: newItemName, name: newItemName,
descrizione: newItemDescrizione, descrizione: newItemDescrizione,
potenziali_group_id: columnId, potenziali_group_id: columnId,
userid: newItemUserid,
}); });
setNewItemName(""); resetState();
setNewItemDescrizione("");
setOpen(false); setOpen(false);
}; };
useEffect(() => {
if (open) {
resetState();
}
}, [open]);
const { data: admins, isLoading } = api.users.getAdmins.useQuery();
return ( return (
<Credenza onOpenChange={setOpen} open={open}> <Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
@ -197,26 +252,86 @@ export const NewCard = ({ columnId }: { columnId: PotenzialiGroupsId }) => {
placeholder="" placeholder=""
value={newItemDescrizione} value={newItemDescrizione}
/> />
<Label htmlFor="utente">Utente Assegnato</Label>
{isLoading ? (
<p>Loading...</p>
) : (
<Select
onValueChange={(v) => {
setNewItemUserid(v === "empty" ? null : (v as UsersId));
}}
value={newItemUserid || "empty"}
>
<SelectTrigger
className="w-full data-[size=default]:h-10"
id="utente"
>
<SelectValue placeholder="Assegna un Utente" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Utente Assegnato</SelectLabel>
<SelectItem className="h-10" key="empty" value="empty">
nessuno
</SelectItem>
{admins?.map((admin) => (
<SelectItem key={admin.id} value={admin.id.toString()}>
<span className="flex items-center gap-2">
<UserAvatar
className="size-8"
userId={admin.id}
username={admin.username}
/>
<span>
<span className="block font-medium">
{admin.username}
</span>
</span>
</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<Button disabled={!newItemName.length} onClick={handleAddItem}> <Button disabled={!newItemName.length} onClick={handleAddItem}>
Aggiungi Aggiungi
</Button> </Button>
<CredenzaClose>Chiudi</CredenzaClose> <CredenzaClose asChild>
<Button>Chiudi</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>
); );
}; };
const EditingContent = () => { const EditingItem = ({
const { editingItem, updateItem } = usePotenziali(); open,
setOpen,
data,
}: {
open: boolean;
setOpen: (open: boolean) => void;
data: PotenzialeData;
}) => {
const { updateItem, removeItem } = usePotenziali();
const [state, setState] = useState<PotenzialeData | null>(editingItem); const [state, setState] = useState<PotenzialeData>(data);
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
if (!state) return null; const { data: admins, isLoading } = api.users.getAdmins.useQuery();
useEffect(() => {
if (open) {
setState(data);
setHasChanges(false);
}
}, [open]);
return ( return (
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaContent className="max-h-[90vh]"> <CredenzaContent className="max-h-[90vh]">
<CredenzaHeader> <CredenzaHeader>
<CredenzaTitle className="text-center font-semibold text-2xl"> <CredenzaTitle className="text-center font-semibold text-2xl">
@ -249,19 +364,89 @@ const EditingContent = () => {
placeholder="" placeholder=""
value={state.descrizione || undefined} value={state.descrizione || undefined}
/> />
<Label htmlFor="utente">Utente Assegnato</Label>
{isLoading ? (
<p>Loading...</p>
) : (
<Select
onValueChange={(v) => {
setState(
(prev) =>
prev && {
...prev,
userid: v === "empty" ? null : (v as UsersId),
},
);
setHasChanges(true);
}}
value={state.userid?.toString() || "empty"}
>
<SelectTrigger
className="w-full data-[size=default]:h-10"
id="utente"
>
<SelectValue placeholder="Assegna un Utente" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Utente Assegnato</SelectLabel>
<SelectItem className="h-10" key="empty" value="empty">
nessuno
</SelectItem>
{admins?.map((admin) => (
<SelectItem key={admin.id} value={admin.id.toString()}>
<span className="flex items-center gap-2">
<UserAvatar
className="size-8"
userId={admin.id}
username={admin.username}
/>
<span>
<span className="block font-medium">
{admin.username}
</span>
</span>
</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<Button <Button
disabled={!hasChanges || !state} disabled={!hasChanges || !state}
onClick={() => { onClick={() => {
updateItem(state); updateItem(state);
setHasChanges(false);
}} }}
type="button" type="button"
> >
Salva Salva
</Button> </Button>
<CredenzaClose>Chiudi</CredenzaClose> <Confirm
description="Sei sicuro di voler eliminare?"
onConfirm={() => {
if (state) {
setOpen(false);
removeItem(state.id);
}
}}
title="Elimina"
>
<Button aria-label="Delete" variant="destructive">
<Trash2 />
<span>Elimina</span>
</Button>
</Confirm>
<CredenzaClose asChild>
<Button>Chiudi</Button>
</CredenzaClose>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza>
); );
}; };

View file

@ -1,19 +1,21 @@
import { createContext, useContext, useState } from "react"; import { createContext, useContext } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import type { NewPotenziali, PotenzialiId } from "~/schemas/public/Potenziali"; import type { NewPotenziali, PotenzialiId } from "~/schemas/public/Potenziali";
import type { PotenzialiGroups } from "~/schemas/public/PotenzialiGroups"; import type {
PotenzialiGroups,
PotenzialiGroupsId,
} from "~/schemas/public/PotenzialiGroups";
import type { PotenzialeData } from "~/server/services/potenziali.service"; import type { PotenzialeData } from "~/server/services/potenziali.service";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
type PotenzialiContextType = { type PotenzialiContextType = {
data: PotenzialeData[]; data: PotenzialeData[];
columns: PotenzialiGroups[]; columns: PotenzialiGroups[];
editingItem: PotenzialeData | null;
setData: (data: PotenzialeData[]) => void; setData: (data: PotenzialeData[]) => void;
updateCall: () => void; updateCall: () => void;
setEditing: (id: PotenzialiId | null) => void;
addItem: (item: NewPotenziali) => void; addItem: (item: NewPotenziali) => void;
removeItem: (id: PotenzialiId) => void; removeItem: (id: PotenzialiId) => void;
removeColumn: (id: PotenzialiGroupsId) => void;
addColumn: (name: string) => void; addColumn: (name: string) => void;
updateItem: (item: PotenzialeData) => void; updateItem: (item: PotenzialeData) => void;
}; };
@ -28,13 +30,6 @@ export const PotenzialiProvider = ({ children }: PotenzialiProviderProps) => {
const { data: columns } = api.potenziali.getPotenzialiGroups.useQuery(); const { data: columns } = api.potenziali.getPotenzialiGroups.useQuery();
const [editingItem, setEditingItem] = useState<PotenzialeData | null>(null);
const setEditing = (id: PotenzialiId | null) => {
const item = data?.find((item) => item.id === id) || null;
setEditingItem(item);
};
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: addItemMutation } = api.potenziali.addPotenziale.useMutation({ const { mutate: addItemMutation } = api.potenziali.addPotenziale.useMutation({
onSuccess: async () => { onSuccess: async () => {
@ -100,9 +95,27 @@ export const PotenzialiProvider = ({ children }: PotenzialiProviderProps) => {
}, },
}); });
const updateItem = (item: PotenzialeData) => { const updateItem = (item: PotenzialeData) => {
const { column, username: _username, ...rest } = item;
updateItemMutation({ updateItemMutation({
potenzialiId: item.id, potenzialiId: item.id,
data: item, data: {
...rest,
potenziali_group_id: column as PotenzialiGroupsId,
},
});
};
const { mutate: removeColumnMutation } =
api.potenziali.deletePotenzialeGroup.useMutation({
onSuccess: async () => {
toast.success("Colonna rimossa con successo");
await utils.potenziali.getPotenziali.invalidate();
await utils.potenziali.getPotenzialiGroups.invalidate();
},
});
const removeColumn = (id: PotenzialiGroupsId) => {
removeColumnMutation({
potenzialiGroupId: id,
}); });
}; };
@ -111,14 +124,13 @@ export const PotenzialiProvider = ({ children }: PotenzialiProviderProps) => {
value={{ value={{
data: data || [], data: data || [],
columns: columns || [], columns: columns || [],
editingItem,
setData, setData,
updateCall, updateCall,
setEditing,
addItem, addItem,
removeItem, removeItem,
addColumn, addColumn,
updateItem, updateItem,
removeColumn,
}} }}
> >
{children} {children}

View file

@ -98,4 +98,12 @@ export const usersRouter = createTRPCRouter({
getUsersWChatInfo: adminProcedure.query(async () => { getUsersWChatInfo: adminProcedure.query(async () => {
return await getUsersWithChatInfoHandler({ db }); return await getUsersWithChatInfoHandler({ db });
}), }),
getAdmins: adminProcedure.query(async () => {
return await db
.selectFrom("users")
.selectAll()
.where("isAdmin", "=", true)
.orderBy("id", "desc")
.execute();
}),
}); });