feat: enhance RichiesteTable with context menu for user actions and improve status resolution logic

This commit is contained in:
Marco Pedone 2026-03-30 16:26:03 +02:00
parent 508f00c02f
commit bb8c796fa8
3 changed files with 245 additions and 94 deletions

View file

@ -328,8 +328,8 @@ const Content = () => {
</span>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="" size="sm" variant="outline">
<CircleHelp /> Info
<Button className="px-1!" size="sm" variant="outline">
<CircleHelp />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
@ -552,10 +552,10 @@ const Main = () => {
}
//servizio scaduto se decorrenza + 60 giorni è passato e non è ok saldo
if (
new Date() >
add(servizio.decorrenza, {
days: DEFAULT_SERVIZIO_DURATION_DAYS,
}) &&
isBefore(
add(servizio.decorrenza, { days: DEFAULT_SERVIZIO_DURATION_DAYS }),
new Date(),
) &&
!servizio.isOkSaldo
) {
//todo capire come fare con saldo quando scade
@ -586,6 +586,30 @@ const Main = () => {
</ServizioAnnuncioProvider>
);
})}
{isAdmin && (
<div className="w-full space-y-2">
<h4 className="text-lg">Altri annunci:</h4>
<div className="@container grid grid-flow-row @sm:grid-cols-2 grid-cols-1 gap-4">
{servizio.annunci
.filter((a) => !a.accettato_conferma_at)
.map((data) => {
return (
<ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizio.servizio_id}
>
<AnnuncioCard />
</ServizioAnnuncioProvider>
);
})}
{servizio.annunci.length > 1 &&
servizio.annunci.length % 2 === 1 && (
<div className="@sm:block hidden size-full rounded-md border border-(--pattern-fg) bg-[repeating-linear-gradient(45deg,var(--pattern-fg)_0,var(--pattern-fg)_1px,transparent_0,transparent_50%)] bg-size-[10px_10px] bg-fixed opacity-10 [--pattern-fg:#696969] dark:[--pattern-fg:#e1e1e1]" />
)}
</div>
</div>
)}
</>
);
}

View file

@ -1,22 +1,36 @@
import type { ColumnDef, Table } from "@tanstack/react-table";
import { add, format, formatDuration, intervalToDuration } from "date-fns";
import {
add,
format,
formatDuration,
intervalToDuration,
isBefore,
} from "date-fns";
import { it } from "date-fns/locale";
import {
CircleCheck,
CircleCheckBig,
CircleEllipsis,
ClockFading,
ExternalLink,
SearchX,
UserCog,
} from "lucide-react";
import Link from "next/link";
import { useRef } from "react";
import toast from "react-hot-toast";
import { NotePopover } from "~/components/notePopover";
import { AnnuncioCardRichieste } from "~/components/servizio/annuncio_card";
import { WhatsAppIcon2 } from "~/components/svgs";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuTrigger,
} from "~/components/ui/context-menu";
import { DataTable } from "~/components/ui/data-table";
import { DataTableColumnHeader } from "~/components/ui/dataTable-header";
import type {
@ -39,9 +53,11 @@ import {
import { UserAvatar } from "~/components/user_avatar";
import { MutationPageRestore } from "~/lib/tableUtils";
import { cn } from "~/lib/utils";
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { RichiesteData } from "~/server/controllers/servizio.controller";
import { api } from "~/utils/api";
import { DEFAULT_SERVIZIO_DURATION_DAYS } from "~/utils/config";
type RichiesteTableProps = {
data: RichiesteData[];
@ -101,20 +117,55 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
{
accessorKey: "username",
cell: ({ row }) => {
const data = row.original;
return (
<Link
className="flex items-center gap-2"
href={`/area-riservata/admin/user-view/ricerca/${row.original.user_id}`}
>
<UserAvatar
className="size-8"
userId={row.original.user_id}
username={row.original.username}
/>
<span className="font-semibold text-sm">
{row.original.username}
</span>
</Link>
<ContextMenu>
<ContextMenuTrigger asChild>
<Link
className="flex items-center gap-2"
href={`/area-riservata/admin/user-view/ricerca/${data.user_id}`}
>
<UserAvatar
className="size-8"
userId={data.user_id}
username={data.username}
/>
<span className="font-semibold text-sm">{data.username}</span>
</Link>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuLabel className="flex h-8 items-center gap-2">
<UserAvatar
className="size-5"
style={{ height: "1.25rem", width: "1.25rem" }}
userId={data.user_id}
username={data.username}
/>
<span className="font-semibold text-sm">{data.username}</span>
</ContextMenuLabel>
<ContextMenuItem>
<Link
aria-label="Visualizza Profilo"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/edit-user/${data.user_id}`}
>
<UserCog className="text-muted-foreground" /> Profilo
</Link>
</ContextMenuItem>
<ContextMenuItem>
<Link
className="flex w-full items-center gap-2"
href={`https://wa.me/${data.telefono}`}
target="_blank"
>
<WhatsAppIcon2 className="size-4 fill-muted-foreground stroke-0" />
WhatsApp
</Link>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
},
enableHiding: false,
@ -215,44 +266,154 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
cell: ({ row }) => {
let badgeClass = "";
let contents: React.ReactNode;
const annunci = row.original.annunci_servizio;
const awaitingConferma = annunci.some(
(a) => a.user_confirmed_at !== null && !a.hasConfermaAdmin,
);
const hasConfermaAdmin = annunci.some((a) => a.hasConfermaAdmin);
if (row.original.isInterrotto) {
badgeClass = "border-destructive bg-destructive/10 text-destructive";
contents = (
<>
<SearchX className="stroke-red-500 dark:stroke-red-400" />
Interrotto
</>
const resolveStatus = () => {
if (row.original.isInterrotto) {
badgeClass =
"border-destructive bg-destructive/10 text-destructive";
contents = (
<>
<SearchX className="stroke-red-500 dark:stroke-red-400" />
Interrotto
</>
);
return;
}
if (!row.original.onboardOk) {
badgeClass = "border-pink-500 bg-pink-500/10 text-pink-600";
contents = (
<>
<ClockFading className="stroke-pink-500 dark:stroke-pink-400" />
Onboarding
</>
);
return;
}
if (!row.original.isOkAcconto || !row.original.decorrenza) {
const acconti = row.original.ordini_servizio.filter(
(o) => o.type === OrderTypeEnum.Acconto,
);
const activeAcconto = acconti.find((a) => a.isActive);
if (activeAcconto) {
badgeClass = "border-yellow-500 bg-yellow-500/10 text-yellow-600";
contents = (
<>
<ClockFading className="stroke-yellow-500 dark:stroke-yellow-400" />
Attesa attivazione
</>
);
} else {
badgeClass = "border-yellow-500 bg-yellow-500/10 text-yellow-600";
contents = (
<>
<ClockFading className="stroke-yellow-500 dark:stroke-yellow-400" />
Attesa pagamento acconto
</>
);
}
return;
}
if (
isBefore(
add(row.original.decorrenza, {
days: DEFAULT_SERVIZIO_DURATION_DAYS,
}),
new Date(),
) &&
!row.original.isOkSaldo
) {
badgeClass = "border-red-500 bg-red-500/10 text-red-500";
contents = (
<>
<SearchX className="stroke-red-500 dark:stroke-red-400" />
Scaduto
</>
);
return;
}
const contrattoInserito = row.original.annunci_servizio.filter(
(a) => a.doc_contratto_added,
);
} else if (!row.original.onboardOk) {
badgeClass = "border-yellow-500 bg-yellow-500/10 text-yellow-600";
contents = (
<>
<ClockFading className="stroke-yellow-500 dark:stroke-yellow-400" />
Attesa attivazione
</>
if (contrattoInserito.length > 0) {
if (row.original.isOkSaldo) {
if (row.original.isOkConsulenza) {
badgeClass = "border-blue-500 bg-blue-500/10 text-blue-500";
contents = (
<>
<CircleCheckBig className="stroke-blue-500 dark:stroke-blue-400" />
Confermato
</>
);
return;
}
if (
row.original.ordini_servizio.some(
(o) => o.type === OrderTypeEnum.Consulenza && !o.isActive,
)
) {
badgeClass =
"border-yellow-500 bg-yellow-500/10 text-yellow-600";
contents = (
<>
<ClockFading className="stroke-yellow-500 dark:stroke-yellow-400" />
Consulenza da pagare
</>
);
return;
}
badgeClass = "border-yellow-500 bg-yellow-500/10 text-yellow-600";
contents = (
<>
<ClockFading className="stroke-yellow-500 dark:stroke-yellow-400" />
Consulenza da inserire
</>
);
return;
}
}
const annuncioConfermato = row.original.annunci_servizio.filter(
(a) => a.accettato_conferma_at !== null,
);
} else if (awaitingConferma) {
badgeClass = "border-sky-500 bg-sky-500/10 text-sky-500";
contents = (
<>
<CircleEllipsis className="stroke-sky-500 dark:stroke-sky-400" />
In Conferma
</>
if (annuncioConfermato.length > 0) {
if (row.original.isOkSaldo) {
badgeClass = "border-blue-500 bg-blue-500/10 text-blue-500";
contents = (
<>
<CircleCheckBig className="stroke-blue-500 dark:stroke-blue-400" />
Confermato
</>
);
} else {
badgeClass = "border-yellow-500 bg-yellow-500/10 text-yellow-600";
contents = (
<>
<ClockFading className="stroke-yellow-500 dark:stroke-yellow-400" />
Attesa saldo
</>
);
}
return;
}
const annuncioAttesaConferma = row.original.annunci_servizio.filter(
(a) => a.accettato_conferma_at == null && a.doc_conferma_added,
);
} else if (row.original.isOkSaldo && hasConfermaAdmin) {
badgeClass = "border-blue-500 bg-blue-500/10 text-blue-500";
contents = (
<>
<CircleCheckBig className="stroke-blue-500 dark:stroke-blue-400" />
Saldo pagato
</>
);
} else if (row.original.isOkAcconto) {
if (annuncioAttesaConferma.length > 0) {
badgeClass = "border-blue-500 bg-blue-500/10 text-blue-500";
contents = (
<>
<CircleCheckBig className="stroke-blue-500 dark:stroke-blue-400" />
Attesa accettazione conferma
</>
);
return;
}
badgeClass = "border-green-500 bg-green-500/10 text-green-500";
contents = (
<>
@ -260,7 +421,8 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
Attivo
</>
);
}
};
resolveStatus();
return (
<Badge
className={cn(
@ -334,41 +496,6 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
cell: ({ row }) => {
const annunci = row.original.annunci_servizio;
return (
// <Popover>
// <PopoverTrigger asChild>
// <Button
// className="data-[state=open]:border-primary data-[state=open]:bg-primary/10"
// size="sm"
// variant="outline"
// >
// {annunci.length} Annunci
// </Button>
// </PopoverTrigger>
// <PopoverContent
// align="end"
// className="max-h-[50vh] w-[80vw] overflow-y-auto sm:max-h-220"
// collisionPadding={{
// top: 20,
// }}
// >
// <PopoverHeader>
// <PopoverTitle>Annunci Inseriti</PopoverTitle>
// <PopoverDescription className="sr-only">
// Desc
// </PopoverDescription>
// </PopoverHeader>
// <div className="flex flex-col gap-4 divide-y">
// {annunci.map((a) => (
// <AnnuncioCardRichieste
// data={a}
// key={a.annunci_id}
// tableRef={tableRef}
// />
// ))}
// </div>
// </PopoverContent>
// </Popover>
<Sheet>
<SheetTrigger asChild>
<Button

View file

@ -1475,7 +1475,7 @@ export const getOrdineRicevutaData = async ({
}
};
export type RichiesteData = Servizio &
Pick<Users, "username" | "note"> & {
Pick<Users, "username" | "note" | "telefono"> & {
annunci_servizio: ServizioAnnuncioData[];
ordini_servizio: (Ordini & Pick<Prezziario, "testo_condizioni">)[];
};
@ -1487,7 +1487,7 @@ export const getRichieste = async (): Promise<RichiesteData[]> => {
.selectFrom("servizio")
.innerJoin("users", "users.id", "servizio.user_id")
.selectAll("servizio")
.select(["users.username", "users.note"])
.select(["users.username", "users.note", "users.telefono"])
.select((eb) => [
jsonArrayFrom(
eb