feat: enhance RichiesteTable with context menu for user actions and improve status resolution logic
This commit is contained in:
parent
508f00c02f
commit
bb8c796fa8
3 changed files with 245 additions and 94 deletions
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue