2026-03-20 16:45:14 +01:00
|
|
|
import { add, addBusinessDays, isBefore } from "date-fns";
|
2025-12-04 17:16:49 +01:00
|
|
|
import {
|
|
|
|
|
Bug,
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ClockAlert,
|
|
|
|
|
Cog,
|
2026-02-25 15:30:53 +01:00
|
|
|
Euro,
|
2026-01-16 17:11:53 +01:00
|
|
|
ExternalLink,
|
2026-03-08 01:02:57 +01:00
|
|
|
Logs,
|
2025-12-04 17:16:49 +01:00
|
|
|
Plus,
|
2026-02-24 10:40:32 +01:00
|
|
|
Search,
|
2025-12-18 12:18:24 +01:00
|
|
|
SlidersHorizontal,
|
2025-12-18 15:12:05 +01:00
|
|
|
Trash2,
|
2025-12-04 17:16:49 +01:00
|
|
|
} from "lucide-react";
|
2026-01-16 17:11:53 +01:00
|
|
|
import Link from "next/link";
|
2026-03-08 01:02:57 +01:00
|
|
|
import { useRouter } from "next/router";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { useState } from "react";
|
2025-08-04 17:45:44 +02:00
|
|
|
import toast from "react-hot-toast";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
Credenza,
|
|
|
|
|
CredenzaBody,
|
|
|
|
|
CredenzaContent,
|
|
|
|
|
CredenzaDescription,
|
|
|
|
|
CredenzaHeader,
|
|
|
|
|
CredenzaTitle,
|
|
|
|
|
CredenzaTrigger,
|
|
|
|
|
} from "~/components/custom_ui/credenza";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { MultiSelect } from "~/components/custom_ui/multiselect";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { LoadingPage } from "~/components/loading";
|
2025-08-04 17:45:44 +02:00
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
AlertDialogTrigger,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/components/ui/alert-dialog";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { Button } from "~/components/ui/button";
|
2025-08-04 17:45:44 +02:00
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
Collapsible,
|
|
|
|
|
CollapsibleContent,
|
|
|
|
|
CollapsibleTrigger,
|
|
|
|
|
} from "~/components/ui/collapsible";
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogTrigger,
|
|
|
|
|
} from "~/components/ui/dialog";
|
2026-03-20 16:45:14 +01:00
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from "~/components/ui/tooltip";
|
2025-08-28 18:27:07 +02:00
|
|
|
import {
|
|
|
|
|
type FormValues as EditFormValues,
|
|
|
|
|
FormEditServizio,
|
|
|
|
|
} from "~/forms/FormEditServizioAdmin";
|
2026-03-08 01:02:57 +01:00
|
|
|
import { FormNewOrder } from "~/forms/FormNewOrdine";
|
2026-03-12 16:27:24 +01:00
|
|
|
import { FormServizio, type FormValues } from "~/forms/FormServizio";
|
2026-03-20 13:54:29 +01:00
|
|
|
import { formatCurrency } from "~/lib/utils";
|
2026-02-25 15:30:53 +01:00
|
|
|
import { useTranslation } from "~/providers/I18nProvider";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { useServizio } from "~/providers/ServizioProvider";
|
2025-08-28 18:27:07 +02:00
|
|
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
2025-08-19 16:44:38 +02:00
|
|
|
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
2026-01-16 17:11:53 +01:00
|
|
|
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
2025-08-19 16:44:38 +02:00
|
|
|
import type { UsersId } from "~/schemas/public/Users";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { api } from "~/utils/api";
|
2025-12-18 15:12:05 +01:00
|
|
|
import { Confirm } from "../confirm";
|
2026-03-08 01:02:57 +01:00
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableRow,
|
|
|
|
|
} from "../ui/table";
|
|
|
|
|
|
|
|
|
|
export const ServizioInfos = () => {
|
|
|
|
|
return (
|
|
|
|
|
<div className="grid w-full grid-cols-2 gap-2 sm:grid-cols-4">
|
|
|
|
|
<ServizioPrezzo />
|
2026-03-20 13:54:29 +01:00
|
|
|
<UserActions />
|
2026-03-08 01:02:57 +01:00
|
|
|
<ServizioOrders />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2026-03-20 13:54:29 +01:00
|
|
|
export const AdminServizioActions = () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { servizio, isAdmin, userId } = useServizio();
|
2026-02-25 15:30:53 +01:00
|
|
|
if (!isAdmin) return null;
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
2026-02-25 15:30:53 +01:00
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
|
|
|
<AddAnnuncio servizioId={servizio.servizio_id} userId={userId} />
|
|
|
|
|
<EditServizioAdmin />
|
2025-08-28 18:27:07 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
2025-08-24 15:47:08 +02:00
|
|
|
};
|
2025-12-04 17:16:49 +01:00
|
|
|
|
2026-03-20 13:54:29 +01:00
|
|
|
const UserActions = () => {
|
2025-12-04 17:16:49 +01:00
|
|
|
const { servizio, isAdmin } = useServizio();
|
|
|
|
|
|
2026-02-25 15:30:53 +01:00
|
|
|
const canUserEdit = servizio.isOkAcconto && servizio.decorrenza !== null;
|
|
|
|
|
/*&&
|
2025-12-18 14:12:16 +01:00
|
|
|
new Date() <= add(new Date(servizio.decorrenza), { days: 60 }) &&
|
2025-12-18 15:12:25 +01:00
|
|
|
!servizio.annunci.some((a) => a.user_confirmed_at !== null) &&
|
|
|
|
|
!servizio.isInterrotto &&
|
2026-02-25 15:30:53 +01:00
|
|
|
!servizio.annunci.some((a) => a.user_confirmed_at !== null);*/
|
2025-12-04 17:16:49 +01:00
|
|
|
|
|
|
|
|
if (isAdmin || canUserEdit) {
|
|
|
|
|
return (
|
2026-03-20 13:54:29 +01:00
|
|
|
<>
|
2025-12-18 12:11:08 +01:00
|
|
|
<EditParametri />
|
2025-12-04 17:16:49 +01:00
|
|
|
<InterruzioneServizio />
|
2026-03-20 13:54:29 +01:00
|
|
|
</>
|
2025-12-04 17:16:49 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
2025-12-17 14:14:30 +01:00
|
|
|
|
2026-03-08 01:02:57 +01:00
|
|
|
const ServizioPrezzo = () => {
|
|
|
|
|
const { servizio } = useServizio();
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const { data: packs } = api.servizio.getPacksSolution.useQuery({
|
|
|
|
|
servizioId: servizio.servizio_id,
|
|
|
|
|
});
|
|
|
|
|
if (!packs) return null;
|
|
|
|
|
return (
|
|
|
|
|
<Dialog>
|
|
|
|
|
<DialogTrigger asChild>
|
|
|
|
|
<Button className="flex-1" variant="outline">
|
|
|
|
|
<Euro />
|
|
|
|
|
<span>Prezzi</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogTrigger>
|
|
|
|
|
<DialogContent className="sm:max-w-2xl">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Prezzi Servizio</DialogTitle>
|
|
|
|
|
<DialogDescription className="sr-only">
|
|
|
|
|
Prezzi disponibili per questo servizio
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
{t.servizio.prezzi.sub}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex w-full flex-col gap-4 md:flex-row">
|
|
|
|
|
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
|
|
|
|
{packs.acconto.success ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
{packs.acconto.pack.nome_it}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
{packs.acconto.pack.desc_it}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-semibold text-lg">
|
|
|
|
|
{formatCurrency(packs.acconto.pack.prezzo_cent / 100)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
COD: {packs.acconto.pack.idprezziario}
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="font-medium">{packs.acconto.message}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
|
|
|
|
{packs.saldo.success ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
{packs.saldo.pack.nome_it}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
{packs.saldo.pack.desc_it}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-semibold text-lg">
|
|
|
|
|
{formatCurrency(packs.saldo.pack.prezzo_cent / 100)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
COD: {packs.saldo.pack.idprezziario}
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="font-medium">{packs.saldo.message}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{servizio.tipologia === TipologiaPosizioneEnum.Stabile && (
|
|
|
|
|
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
|
|
|
|
<>
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
{t.servizio.prezzi.consulenza}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
{t.servizio.prezzi.consulenza_desc}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-semibold">
|
|
|
|
|
<Link
|
|
|
|
|
className="flex items-center gap-2 underline underline-offset-2"
|
|
|
|
|
href={"/prezzi#contratti"}
|
|
|
|
|
target="_blank"
|
|
|
|
|
>
|
|
|
|
|
<span>{t.servizio.prezzi.gotoprezzi}</span>
|
|
|
|
|
<ExternalLink className="size-4 stroke-foreground" />
|
|
|
|
|
</Link>
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
const ServizioOrders = () => {
|
|
|
|
|
const { isAdmin, servizio } = useServizio();
|
|
|
|
|
const [openOrders, setOpenOrders] = useState(false);
|
|
|
|
|
const [openEdit, setOpenEdit] = useState(false);
|
|
|
|
|
|
|
|
|
|
const swapToEdit = () => {
|
|
|
|
|
setOpenOrders(false);
|
|
|
|
|
setOpenEdit(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const swapToOrders = () => {
|
|
|
|
|
setOpenEdit(false);
|
|
|
|
|
setOpenOrders(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Dialog onOpenChange={setOpenOrders} open={openOrders}>
|
|
|
|
|
<DialogTrigger asChild>
|
|
|
|
|
<Button className="flex-1" variant="outline">
|
|
|
|
|
<Logs />
|
|
|
|
|
<span>Ordini</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogTrigger>
|
|
|
|
|
<DialogContent className="sm:max-w-2xl">
|
|
|
|
|
<DialogHeader className="flex-row items-center gap-4">
|
|
|
|
|
<DialogTitle>Ordini Servizio</DialogTitle>
|
|
|
|
|
<DialogDescription className="sr-only">
|
|
|
|
|
Ordini disponibili per questo servizio
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
{isAdmin && (
|
|
|
|
|
<Button
|
|
|
|
|
className="w-fit"
|
|
|
|
|
onClick={swapToEdit}
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
>
|
|
|
|
|
<Plus />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<div className="flex flex-col items-center gap-4 overflow-auto">
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead>Data</TableHead>
|
|
|
|
|
<TableHead>ID</TableHead>
|
|
|
|
|
<TableHead>Importo</TableHead>
|
|
|
|
|
<TableHead>Status</TableHead>
|
|
|
|
|
<TableHead></TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{servizio.ordini.length === 0 && (
|
|
|
|
|
<tr>
|
|
|
|
|
<td className="py-4 text-center" colSpan={4}>
|
|
|
|
|
<span>Nessun ordine disponibile</span>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
{servizio.ordini.map((ordine) => (
|
|
|
|
|
<TableRow key={ordine.ordine_id}>
|
|
|
|
|
<TableCell>
|
|
|
|
|
{ordine.created_at.toLocaleDateString("it-IT")}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="font-medium">
|
|
|
|
|
{ordine.packid}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
{formatCurrency(ordine.amount_cent / 100)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
{ordine.isActive ? "Attivo" : "Non attivo"}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Link
|
|
|
|
|
href={`/area-riservata/${isAdmin ? "admin/user-view/" : ""}ordini${isAdmin ? `/${ordine.userid}` : ""}?ordineId=${ordine.ordine_id}`}
|
|
|
|
|
target="_blank"
|
|
|
|
|
>
|
|
|
|
|
<Button size="sm" variant="outline">
|
|
|
|
|
<Search />
|
|
|
|
|
</Button>
|
|
|
|
|
</Link>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
<Dialog onOpenChange={setOpenEdit} open={openEdit}>
|
|
|
|
|
<DialogContent>
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Nuovo Ordine</DialogTitle>
|
|
|
|
|
<DialogDescription className="sr-only">nuovo</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<AddOrderDialogContent swapBack={swapToOrders} />
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const AddOrderDialogContent = ({ swapBack }: { swapBack: () => void }) => {
|
|
|
|
|
const { servizioId, userId } = useServizio();
|
|
|
|
|
const { data, isLoading } = api.prezziario.getPrezziario.useQuery();
|
|
|
|
|
|
|
|
|
|
if (isLoading) return <LoadingPage />;
|
|
|
|
|
|
|
|
|
|
if (!data)
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<span>Errore nel caricamento del prezziario.</span>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<FormNewOrder
|
|
|
|
|
prezziario={data}
|
|
|
|
|
servizioId={servizioId}
|
|
|
|
|
setOpen={swapBack}
|
|
|
|
|
userId={userId}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 15:47:38 +01:00
|
|
|
export const ServizioPacksInfos = () => {
|
2025-12-17 14:14:30 +01:00
|
|
|
const { servizio } = useServizio();
|
2026-02-25 15:30:53 +01:00
|
|
|
const { t } = useTranslation();
|
2025-12-17 14:14:30 +01:00
|
|
|
const { data: packs } = api.servizio.getPacksSolution.useQuery({
|
|
|
|
|
servizioId: servizio.servizio_id,
|
|
|
|
|
});
|
|
|
|
|
if (!packs) return null;
|
2026-02-25 15:30:53 +01:00
|
|
|
|
2025-12-17 14:14:30 +01:00
|
|
|
return (
|
2026-02-25 15:30:53 +01:00
|
|
|
<Collapsible className="flex flex-col gap-2">
|
|
|
|
|
<CollapsibleTrigger asChild>
|
|
|
|
|
<Button
|
2026-03-06 18:54:48 +01:00
|
|
|
className="h-fit w-full flex-wrap items-center justify-start py-1 sm:w-fit sm:justify-center"
|
|
|
|
|
size="sm"
|
2026-02-25 15:30:53 +01:00
|
|
|
variant="outline"
|
|
|
|
|
>
|
|
|
|
|
<Euro className="size-5" />
|
2025-12-18 13:29:39 +01:00
|
|
|
|
2026-02-25 15:30:53 +01:00
|
|
|
<h3 className="font-semibold text-lg">{t.servizio.prezzi.title}</h3>
|
|
|
|
|
|
2026-03-06 18:54:48 +01:00
|
|
|
<ChevronDown className="ml-auto size-4 sm:ml-0" />
|
2026-02-25 15:30:53 +01:00
|
|
|
</Button>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<CollapsibleContent className="flex flex-col gap-2">
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
{t.servizio.prezzi.sub}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex w-full flex-col gap-4 md:flex-row">
|
2026-01-16 17:11:53 +01:00
|
|
|
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
2026-02-25 15:30:53 +01:00
|
|
|
{packs.acconto.success ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
{packs.acconto.pack.nome_it}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
{packs.acconto.pack.desc_it}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-semibold text-lg">
|
|
|
|
|
{formatCurrency(packs.acconto.pack.prezzo_cent / 100)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
COD: {packs.acconto.pack.idprezziario}
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="font-medium">{packs.acconto.message}</span>
|
|
|
|
|
)}
|
2026-01-16 17:11:53 +01:00
|
|
|
</div>
|
2026-02-25 15:30:53 +01:00
|
|
|
|
|
|
|
|
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
|
|
|
|
{packs.saldo.success ? (
|
|
|
|
|
<>
|
|
|
|
|
<span className="font-medium">{packs.saldo.pack.nome_it}</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
{packs.saldo.pack.desc_it}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-semibold text-lg">
|
|
|
|
|
{formatCurrency(packs.saldo.pack.prezzo_cent / 100)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
COD: {packs.saldo.pack.idprezziario}
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="font-medium">{packs.saldo.message}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{servizio.tipologia === TipologiaPosizioneEnum.Stabile && (
|
|
|
|
|
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
|
|
|
|
<>
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
{t.servizio.prezzi.consulenza}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
{t.servizio.prezzi.consulenza_desc}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-semibold">
|
|
|
|
|
<Link
|
|
|
|
|
className="flex items-center gap-2 underline underline-offset-2"
|
|
|
|
|
href={"/prezzi#contratti"}
|
|
|
|
|
target="_blank"
|
|
|
|
|
>
|
|
|
|
|
<span>{t.servizio.prezzi.gotoprezzi}</span>
|
|
|
|
|
<ExternalLink className="size-4 stroke-foreground" />
|
|
|
|
|
</Link>
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
2025-12-17 14:14:30 +01:00
|
|
|
);
|
|
|
|
|
};
|
2025-12-18 15:12:05 +01:00
|
|
|
|
|
|
|
|
const AddAnnuncio = ({
|
2025-08-28 18:27:07 +02:00
|
|
|
servizioId,
|
2025-08-19 16:44:38 +02:00
|
|
|
}: {
|
2025-08-28 18:27:07 +02:00
|
|
|
servizioId: ServizioServizioId;
|
|
|
|
|
userId: UsersId;
|
2025-08-19 16:44:38 +02:00
|
|
|
}) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { data: codici, isLoading: loading } =
|
|
|
|
|
api.servizio.getAnnunciAvailableToAdd.useQuery({
|
|
|
|
|
servizioId,
|
|
|
|
|
});
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const [selectedAnnunci, setSelectedAnnunci] = useState<
|
|
|
|
|
{ value: AnnunciId; label: string }[]
|
|
|
|
|
>([]);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const [openModal, setOpenModal] = useState(false);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const utils = api.useUtils();
|
|
|
|
|
const { mutate } = api.servizio.addServizioAnnunci.useMutation({
|
2025-08-29 16:18:32 +02:00
|
|
|
onError: (error) => {
|
|
|
|
|
console.error(error);
|
|
|
|
|
toast.error("Errore durante l'aggiunta dell'annuncio");
|
|
|
|
|
},
|
2025-08-28 18:27:07 +02:00
|
|
|
onSuccess: async () => {
|
2026-03-08 01:02:57 +01:00
|
|
|
await utils.servizio.invalidate();
|
2025-08-28 18:27:07 +02:00
|
|
|
toast.success("Annuncio aggiunto");
|
|
|
|
|
setOpenModal(false);
|
|
|
|
|
setSelectedAnnunci([]);
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
2025-08-29 16:18:32 +02:00
|
|
|
<Dialog onOpenChange={setOpenModal} open={openModal}>
|
2025-08-28 18:27:07 +02:00
|
|
|
<DialogTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
aria-label="Add Annuncio"
|
2025-11-03 10:59:45 +01:00
|
|
|
className=""
|
2025-08-28 18:27:07 +02:00
|
|
|
onClick={() => {
|
|
|
|
|
setSelectedAnnunci([]);
|
|
|
|
|
}}
|
2025-08-29 16:18:32 +02:00
|
|
|
variant="outline"
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
|
|
|
|
<Plus />
|
|
|
|
|
Annuncio
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogTrigger>
|
|
|
|
|
<DialogContent
|
2026-03-09 11:07:35 +01:00
|
|
|
className="sm:max-w-2xl"
|
2025-08-28 18:27:07 +02:00
|
|
|
onInteractOutside={(e) => {
|
|
|
|
|
if (selectedAnnunci.length !== 0) {
|
|
|
|
|
e.preventDefault(); // Prevent closing the dialog if there are selected announcements
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Aggiungi annuncio</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
Salva un annuncio nella ricerca dell'utente
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<div className="grid gap-4 py-4">
|
|
|
|
|
{loading ? (
|
|
|
|
|
<LoadingPage />
|
|
|
|
|
) : (
|
|
|
|
|
<MultiSelect
|
2025-08-29 16:18:32 +02:00
|
|
|
defaultValue={undefined}
|
2025-08-28 18:27:07 +02:00
|
|
|
elementId="select-annuncio"
|
|
|
|
|
elementName="annuncio"
|
|
|
|
|
isMulti={true}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
setSelectedAnnunci(
|
|
|
|
|
value.map((v) => ({
|
|
|
|
|
label: v.label,
|
2025-08-29 16:18:32 +02:00
|
|
|
value: parseInt(v.value) as AnnunciId,
|
2025-08-28 18:27:07 +02:00
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
}}
|
2025-08-29 16:18:32 +02:00
|
|
|
options={
|
|
|
|
|
codici?.map((cod) => ({
|
2026-03-09 10:41:34 +01:00
|
|
|
label: `${cod.codice} - ${cod.titolo_it}`,
|
2025-08-29 16:18:32 +02:00
|
|
|
value: cod.id.toString(),
|
|
|
|
|
})) || []
|
|
|
|
|
}
|
|
|
|
|
placeholder=""
|
2025-08-28 18:27:07 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<Button
|
|
|
|
|
aria-label="Cancel Add Annuncio"
|
2025-08-29 16:18:32 +02:00
|
|
|
disabled={!selectedAnnunci.length}
|
2025-08-28 18:27:07 +02:00
|
|
|
onClick={() => {
|
|
|
|
|
if (!selectedAnnunci.length) return;
|
|
|
|
|
mutate({
|
|
|
|
|
annunci: selectedAnnunci.map((annuncio) => annuncio.value),
|
2025-08-29 16:18:32 +02:00
|
|
|
servizioId,
|
2025-08-28 18:27:07 +02:00
|
|
|
});
|
|
|
|
|
}}
|
2025-08-29 16:18:32 +02:00
|
|
|
type="button"
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
|
|
|
|
Aggiungi
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
2025-08-19 16:44:38 +02:00
|
|
|
|
2025-08-04 17:45:44 +02:00
|
|
|
const InterruzioneServizio = () => {
|
2026-03-08 01:02:57 +01:00
|
|
|
const { servizio, isAdmin } = useServizio();
|
2026-02-25 15:30:53 +01:00
|
|
|
const { t } = useTranslation();
|
2025-08-28 18:27:07 +02:00
|
|
|
const utils = api.useUtils();
|
|
|
|
|
const { mutate } = api.servizio.interruzioneServizio.useMutation({
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
console.error(error);
|
|
|
|
|
toast.error("Errore durante la richiesta di interruzione");
|
|
|
|
|
},
|
2025-08-29 16:18:32 +02:00
|
|
|
onSuccess: async () => {
|
2026-03-08 01:02:57 +01:00
|
|
|
await utils.servizio.invalidate();
|
2025-08-29 16:18:32 +02:00
|
|
|
toast.success("Interruzione richiesta");
|
|
|
|
|
},
|
2025-08-28 18:27:07 +02:00
|
|
|
});
|
2026-03-20 16:45:14 +01:00
|
|
|
if (!servizio.decorrenza) return null;
|
|
|
|
|
const fineInterruzione = add(servizio.decorrenza, {
|
|
|
|
|
days: 14,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const canInterrompere =
|
|
|
|
|
!servizio.isInterrotto &&
|
|
|
|
|
(isAdmin ||
|
|
|
|
|
(isBefore(new Date(), fineInterruzione) &&
|
|
|
|
|
servizio.annunci.every((a) => a.accettato_conferma_at === null)));
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger asChild>
|
2026-03-20 16:45:14 +01:00
|
|
|
{canInterrompere ? (
|
|
|
|
|
<Button
|
|
|
|
|
aria-label="Request Interruzione"
|
|
|
|
|
className="w-full"
|
|
|
|
|
disabled={!canInterrompere}
|
|
|
|
|
>
|
|
|
|
|
<ClockAlert /> <span>{t.servizio.interruzione.btn}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Tooltip delayDuration={0}>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<span className="inline-block w-full">
|
|
|
|
|
<Button
|
|
|
|
|
aria-label="Request Interruzione"
|
|
|
|
|
className="w-full"
|
|
|
|
|
disabled={!canInterrompere}
|
|
|
|
|
>
|
|
|
|
|
<ClockAlert /> <span>{t.servizio.interruzione.btn}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</span>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>
|
|
|
|
|
<p>Periodo di interruzione scaduto, contattaci.</p>
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
)}
|
2025-08-28 18:27:07 +02:00
|
|
|
</AlertDialogTrigger>
|
2026-03-20 16:45:14 +01:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
2026-02-25 15:30:53 +01:00
|
|
|
<AlertDialogTitle>{t.servizio.interruzione.title}</AlertDialogTitle>
|
2026-03-20 16:45:14 +01:00
|
|
|
<AlertDialogDescription className="space-y-2">
|
|
|
|
|
<p>
|
|
|
|
|
<b>
|
|
|
|
|
Fine periodo di interruzione:{" "}
|
|
|
|
|
{fineInterruzione.toLocaleDateString("it-IT")}
|
|
|
|
|
</b>
|
|
|
|
|
</p>
|
|
|
|
|
<p className="whitespace-pre-line">
|
|
|
|
|
{t.servizio.interruzione.description}
|
|
|
|
|
</p>
|
2025-08-28 18:27:07 +02:00
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
2026-02-25 15:30:53 +01:00
|
|
|
<AlertDialogCancel>{t.annulla}</AlertDialogCancel>
|
2025-08-28 18:27:07 +02:00
|
|
|
<AlertDialogAction
|
|
|
|
|
aria-label="Confirm Interruzione"
|
|
|
|
|
onClick={() => mutate({ servizioId: servizio.servizio_id })}
|
|
|
|
|
>
|
2026-02-25 15:30:53 +01:00
|
|
|
{t.conferma}
|
2025-08-28 18:27:07 +02:00
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
2025-12-18 12:11:08 +01:00
|
|
|
const EditParametri = () => {
|
2026-03-08 01:02:57 +01:00
|
|
|
const { servizioId, isAdmin, servizio } = useServizio();
|
2026-02-25 15:30:53 +01:00
|
|
|
const { t } = useTranslation();
|
2025-12-18 12:11:08 +01:00
|
|
|
const [openEditParametri, setOpenEditParametri] = useState(false);
|
2026-03-06 18:23:35 +01:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const utils = api.useUtils();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const { mutate: update } = api.servizio.updateServizio.useMutation({
|
2025-08-29 16:18:32 +02:00
|
|
|
onError: (error) => {
|
|
|
|
|
toast.error(error.message);
|
|
|
|
|
},
|
2025-08-28 18:27:07 +02:00
|
|
|
onSuccess: async () => {
|
|
|
|
|
toast.success("Servizio modificato con successo");
|
2026-03-08 01:02:57 +01:00
|
|
|
await utils.servizio.invalidate();
|
2026-03-06 18:23:35 +01:00
|
|
|
|
2025-12-18 12:11:08 +01:00
|
|
|
setOpenEditParametri(false);
|
2025-08-28 18:27:07 +02:00
|
|
|
},
|
|
|
|
|
});
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
function onSubmit(fields: FormValues) {
|
|
|
|
|
update({
|
|
|
|
|
data: {
|
|
|
|
|
...fields,
|
|
|
|
|
},
|
2025-08-29 16:18:32 +02:00
|
|
|
servizioId,
|
2025-08-28 18:27:07 +02:00
|
|
|
});
|
|
|
|
|
}
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
2025-12-18 12:11:08 +01:00
|
|
|
<Credenza onOpenChange={setOpenEditParametri} open={openEditParametri}>
|
2025-08-28 18:27:07 +02:00
|
|
|
<CredenzaTrigger asChild>
|
2025-11-06 17:03:42 +01:00
|
|
|
<Button
|
2025-12-18 12:11:08 +01:00
|
|
|
aria-label="Parametri di ricerca"
|
2026-03-20 13:54:29 +01:00
|
|
|
className="w-full"
|
2025-11-06 17:03:42 +01:00
|
|
|
disabled={
|
|
|
|
|
!isAdmin &&
|
|
|
|
|
(servizio.isInterrotto ||
|
|
|
|
|
servizio.annunci.some((a) => a.hasConfermaAdmin))
|
|
|
|
|
}
|
2025-12-04 17:16:49 +01:00
|
|
|
variant="secondary"
|
2025-11-06 17:03:42 +01:00
|
|
|
>
|
2025-12-18 12:18:24 +01:00
|
|
|
<SlidersHorizontal />
|
2026-02-25 15:30:53 +01:00
|
|
|
{t.servizio.parametri.btn}
|
2025-08-28 18:27:07 +02:00
|
|
|
</Button>
|
|
|
|
|
</CredenzaTrigger>
|
|
|
|
|
<CredenzaContent className="max-h-[90vh] w-full sm:max-w-5xl">
|
|
|
|
|
<CredenzaHeader>
|
2026-02-25 15:30:53 +01:00
|
|
|
<CredenzaTitle>{t.servizio.parametri.title}</CredenzaTitle>
|
2025-08-28 18:27:07 +02:00
|
|
|
<CredenzaDescription className="sr-only">
|
2026-02-25 15:30:53 +01:00
|
|
|
{t.servizio.parametri.title}
|
2025-08-28 18:27:07 +02:00
|
|
|
</CredenzaDescription>
|
|
|
|
|
</CredenzaHeader>
|
2025-11-04 19:18:13 +01:00
|
|
|
<CredenzaBody className="max-h-[80vh] w-full max-w-3xl overflow-y-auto pb-5 sm:max-w-5xl">
|
2026-03-12 16:27:24 +01:00
|
|
|
<FormServizio
|
2026-03-06 18:23:35 +01:00
|
|
|
initialData={servizio}
|
|
|
|
|
isLimited={!isAdmin}
|
|
|
|
|
onSubmit={onSubmit}
|
|
|
|
|
/>
|
2025-08-28 18:27:07 +02:00
|
|
|
</CredenzaBody>
|
|
|
|
|
</CredenzaContent>
|
|
|
|
|
</Credenza>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
2025-08-23 17:13:43 +02:00
|
|
|
|
|
|
|
|
const EditServizioAdmin = () => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { servizioId, userId, servizio } = useServizio();
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const utils = api.useUtils();
|
2026-03-08 01:02:57 +01:00
|
|
|
const router = useRouter();
|
2025-08-28 18:27:07 +02:00
|
|
|
const { mutate: update } = api.servizio.updateServizio.useMutation({
|
2025-08-29 16:18:32 +02:00
|
|
|
onError: (error) => {
|
|
|
|
|
toast.error(error.message);
|
|
|
|
|
},
|
2025-08-28 18:27:07 +02:00
|
|
|
onSuccess: async () => {
|
|
|
|
|
toast.success("Servizio modificato con successo");
|
2026-03-08 01:02:57 +01:00
|
|
|
await utils.servizio.invalidate();
|
2025-08-28 18:27:07 +02:00
|
|
|
setOpen(false);
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-12-18 15:12:05 +01:00
|
|
|
const { mutate: remove } = api.servizio.deleteServizio.useMutation({
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
toast.error(error.message);
|
|
|
|
|
},
|
2026-03-08 01:02:57 +01:00
|
|
|
onMutate: async () => {
|
|
|
|
|
await router.push(`/area-riservata/admin/user-view/ricerca/${userId}`);
|
|
|
|
|
},
|
|
|
|
|
|
2025-12-18 15:12:05 +01:00
|
|
|
onSuccess: async () => {
|
|
|
|
|
toast.success("Servizio rimosso con successo");
|
2026-03-08 01:02:57 +01:00
|
|
|
|
|
|
|
|
await utils.servizio.invalidate();
|
2025-12-18 15:12:05 +01:00
|
|
|
},
|
|
|
|
|
});
|
2025-08-23 17:13:43 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
function onSubmit(fields: EditFormValues) {
|
|
|
|
|
update({
|
|
|
|
|
data: {
|
|
|
|
|
...fields,
|
|
|
|
|
},
|
2025-08-29 16:18:32 +02:00
|
|
|
servizioId,
|
2025-08-28 18:27:07 +02:00
|
|
|
});
|
|
|
|
|
}
|
2025-08-23 17:13:43 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
2025-08-29 16:18:32 +02:00
|
|
|
<Credenza onOpenChange={setOpen} open={open}>
|
2025-08-28 18:27:07 +02:00
|
|
|
<CredenzaTrigger asChild>
|
2025-11-03 10:59:45 +01:00
|
|
|
<Button aria-label="Modifica Servizio Admin">
|
2025-12-04 17:16:49 +01:00
|
|
|
<Cog />
|
|
|
|
|
Modifica
|
2025-08-28 18:27:07 +02:00
|
|
|
</Button>
|
|
|
|
|
</CredenzaTrigger>
|
|
|
|
|
<CredenzaContent className="max-h-[90vh] w-full sm:max-w-5xl">
|
|
|
|
|
<CredenzaHeader>
|
|
|
|
|
<CredenzaTitle>Modifica Servizio Admin</CredenzaTitle>
|
|
|
|
|
<CredenzaDescription className="sr-only">
|
|
|
|
|
Modifica
|
|
|
|
|
</CredenzaDescription>
|
|
|
|
|
</CredenzaHeader>
|
|
|
|
|
<CredenzaBody className="max-h-[80vh] w-full space-y-2 overflow-y-auto pb-5">
|
2026-03-06 18:23:35 +01:00
|
|
|
<FormEditServizio initialData={servizio} onSubmit={onSubmit} />
|
|
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
<Collapsible>
|
|
|
|
|
<CollapsibleTrigger asChild>
|
2025-11-03 10:59:45 +01:00
|
|
|
<Button variant="warning">
|
2025-08-28 18:27:07 +02:00
|
|
|
<Bug /> Debug Info Servizio
|
|
|
|
|
</Button>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<CollapsibleContent>
|
|
|
|
|
{(() => {
|
|
|
|
|
const { annunci, ...rest } = servizio;
|
|
|
|
|
return <pre>{JSON.stringify(rest, null, 2)}</pre>;
|
|
|
|
|
})()}
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
2025-12-18 15:12:05 +01:00
|
|
|
<Confirm
|
|
|
|
|
description="Sei sicuro di voler eliminare questo servizio? L'azione è irreversibile."
|
|
|
|
|
onConfirm={() => {
|
|
|
|
|
remove({ servizioId });
|
|
|
|
|
}}
|
|
|
|
|
title="Elimina servizio"
|
|
|
|
|
>
|
|
|
|
|
<Button aria-label="Elimina Servizio" variant="destructive">
|
|
|
|
|
<Trash2 className="size-6" />
|
|
|
|
|
Elimina Servizio
|
|
|
|
|
</Button>
|
|
|
|
|
</Confirm>
|
2025-08-28 18:27:07 +02:00
|
|
|
</CredenzaBody>
|
|
|
|
|
</CredenzaContent>
|
|
|
|
|
</Credenza>
|
|
|
|
|
);
|
2025-08-23 17:13:43 +02:00
|
|
|
};
|