feat: enhance Potenziali management with delete functionality and user assignment
This commit is contained in:
parent
c459da5d23
commit
2ef18a0b5b
3 changed files with 316 additions and 111 deletions
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { format } from "date-fns";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
KanbanBoard,
|
||||
KanbanCard,
|
||||
|
|
@ -12,7 +12,10 @@ import {
|
|||
} from "~/components/ui/kanban";
|
||||
import { usePotenziali } from "~/providers/PotenzialiProvider";
|
||||
import type { PotenzialiGroupsId } from "~/schemas/public/PotenzialiGroups";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import type { PotenzialeData } from "~/server/services/potenziali.service";
|
||||
import { api } from "~/utils/api";
|
||||
import { Confirm } from "../confirm";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
|
|
@ -28,11 +31,19 @@ import Input from "../custom_ui/input";
|
|||
import { Textarea } from "../custom_ui/textarea";
|
||||
import { Button } from "../ui/button";
|
||||
import { Label } from "../ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { UserAvatar } from "../user_avatar";
|
||||
|
||||
export const Potenziali = () => {
|
||||
const { data, columns, setData, updateCall, setEditing, editingItem } =
|
||||
usePotenziali();
|
||||
const { data, columns, setData, updateCall, removeColumn } = usePotenziali();
|
||||
return (
|
||||
<>
|
||||
<KanbanProvider
|
||||
|
|
@ -59,24 +70,57 @@ export const Potenziali = () => {
|
|||
<span>{column.name}</span>
|
||||
|
||||
<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>
|
||||
</KanbanHeader>
|
||||
<KanbanCards className="w-68" id={column.id}>
|
||||
{(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
|
||||
cardOnClick={() => {
|
||||
setEditing(data.id);
|
||||
setIsEditingOpen(true);
|
||||
}}
|
||||
column={column.id}
|
||||
column={columnId}
|
||||
id={data.id}
|
||||
key={data.id}
|
||||
name={data.name}
|
||||
>
|
||||
<>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="m-0 flex-1 font-medium text-sm">
|
||||
{data.name}
|
||||
</p>
|
||||
<p className="m-0 flex-1 font-medium text-sm">{data.name}</p>
|
||||
<p className="m-0 text-muted-foreground text-xs">
|
||||
{data.descrizione}
|
||||
</p>
|
||||
|
|
@ -92,31 +136,27 @@ export const Potenziali = () => {
|
|||
<p className="mt-2 text-muted-foreground text-xs">
|
||||
{format(data.created_at, "dd/MM/yyyy HH:mm")}
|
||||
</p>
|
||||
</KanbanCard>
|
||||
)}
|
||||
</KanbanCards>
|
||||
</KanbanBoard>
|
||||
)}
|
||||
</KanbanProvider>
|
||||
<Credenza
|
||||
onOpenChange={(v) => {
|
||||
if (!v) {
|
||||
setEditing(null);
|
||||
}
|
||||
}}
|
||||
open={!!editingItem}
|
||||
>
|
||||
<EditingContent />
|
||||
</Credenza>
|
||||
{/** biome-ignore lint/a11y/noNoninteractiveElementInteractions: <ok> */}
|
||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: <ok> */}
|
||||
{/** biome-ignore lint/a11y/useKeyWithClickEvents: <ok> */}
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<EditingItem
|
||||
data={data}
|
||||
open={isEditingOpen}
|
||||
setOpen={setIsEditingOpen}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</KanbanCard>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewColumn = () => {
|
||||
const [newColumnName, setNewColumnName] = useState("");
|
||||
const { addColumn } = usePotenziali();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Credenza>
|
||||
<Credenza onOpenChange={setOpen} open={open}>
|
||||
<CredenzaTrigger asChild>
|
||||
<Button>Nuova colonna</Button>
|
||||
</CredenzaTrigger>
|
||||
|
|
@ -141,6 +181,7 @@ export const NewColumn = () => {
|
|||
onClick={() => {
|
||||
addColumn(newColumnName);
|
||||
setNewColumnName("");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Aggiungi
|
||||
|
|
@ -154,18 +195,32 @@ export const NewColumn = () => {
|
|||
export const NewCard = ({ columnId }: { columnId: PotenzialiGroupsId }) => {
|
||||
const [newItemName, setNewItemName] = useState("");
|
||||
const [newItemDescrizione, setNewItemDescrizione] = useState("");
|
||||
const [newItemUserid, setNewItemUserid] = useState<UsersId | null>(null);
|
||||
const { addItem } = usePotenziali();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const resetState = () => {
|
||||
setNewItemName("");
|
||||
setNewItemDescrizione("");
|
||||
setNewItemUserid(null);
|
||||
};
|
||||
const handleAddItem = () => {
|
||||
addItem({
|
||||
name: newItemName,
|
||||
descrizione: newItemDescrizione,
|
||||
potenziali_group_id: columnId,
|
||||
userid: newItemUserid,
|
||||
});
|
||||
setNewItemName("");
|
||||
setNewItemDescrizione("");
|
||||
resetState();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
resetState();
|
||||
}
|
||||
}, [open]);
|
||||
const { data: admins, isLoading } = api.users.getAdmins.useQuery();
|
||||
return (
|
||||
<Credenza onOpenChange={setOpen} open={open}>
|
||||
<CredenzaTrigger asChild>
|
||||
|
|
@ -197,26 +252,86 @@ export const NewCard = ({ columnId }: { columnId: PotenzialiGroupsId }) => {
|
|||
placeholder=""
|
||||
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>
|
||||
<CredenzaFooter>
|
||||
<Button disabled={!newItemName.length} onClick={handleAddItem}>
|
||||
Aggiungi
|
||||
</Button>
|
||||
<CredenzaClose>Chiudi</CredenzaClose>
|
||||
<CredenzaClose asChild>
|
||||
<Button>Chiudi</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
};
|
||||
|
||||
const EditingContent = () => {
|
||||
const { editingItem, updateItem } = usePotenziali();
|
||||
const EditingItem = ({
|
||||
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);
|
||||
if (!state) return null;
|
||||
|
||||
const { data: admins, isLoading } = api.users.getAdmins.useQuery();
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setState(data);
|
||||
setHasChanges(false);
|
||||
}
|
||||
}, [open]);
|
||||
return (
|
||||
<Credenza onOpenChange={setOpen} open={open}>
|
||||
<CredenzaContent className="max-h-[90vh]">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle className="text-center font-semibold text-2xl">
|
||||
|
|
@ -249,19 +364,89 @@ const EditingContent = () => {
|
|||
placeholder=""
|
||||
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>
|
||||
<CredenzaFooter>
|
||||
<Button
|
||||
disabled={!hasChanges || !state}
|
||||
onClick={() => {
|
||||
updateItem(state);
|
||||
setHasChanges(false);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Salva
|
||||
</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>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import { createContext, useContext, useState } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
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 { api } from "~/utils/api";
|
||||
|
||||
type PotenzialiContextType = {
|
||||
data: PotenzialeData[];
|
||||
columns: PotenzialiGroups[];
|
||||
editingItem: PotenzialeData | null;
|
||||
setData: (data: PotenzialeData[]) => void;
|
||||
updateCall: () => void;
|
||||
setEditing: (id: PotenzialiId | null) => void;
|
||||
addItem: (item: NewPotenziali) => void;
|
||||
removeItem: (id: PotenzialiId) => void;
|
||||
removeColumn: (id: PotenzialiGroupsId) => void;
|
||||
addColumn: (name: string) => void;
|
||||
updateItem: (item: PotenzialeData) => void;
|
||||
};
|
||||
|
|
@ -28,13 +30,6 @@ export const PotenzialiProvider = ({ children }: PotenzialiProviderProps) => {
|
|||
|
||||
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 { mutate: addItemMutation } = api.potenziali.addPotenziale.useMutation({
|
||||
onSuccess: async () => {
|
||||
|
|
@ -100,9 +95,27 @@ export const PotenzialiProvider = ({ children }: PotenzialiProviderProps) => {
|
|||
},
|
||||
});
|
||||
const updateItem = (item: PotenzialeData) => {
|
||||
const { column, username: _username, ...rest } = item;
|
||||
updateItemMutation({
|
||||
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={{
|
||||
data: data || [],
|
||||
columns: columns || [],
|
||||
editingItem,
|
||||
setData,
|
||||
updateCall,
|
||||
setEditing,
|
||||
addItem,
|
||||
removeItem,
|
||||
addColumn,
|
||||
updateItem,
|
||||
removeColumn,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -98,4 +98,12 @@ export const usersRouter = createTRPCRouter({
|
|||
getUsersWChatInfo: adminProcedure.query(async () => {
|
||||
return await getUsersWithChatInfoHandler({ db });
|
||||
}),
|
||||
getAdmins: adminProcedure.query(async () => {
|
||||
return await db
|
||||
.selectFrom("users")
|
||||
.selectAll()
|
||||
.where("isAdmin", "=", true)
|
||||
.orderBy("id", "desc")
|
||||
.execute();
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue