infoalloggi-monorepo/apps/infoalloggi/src/components/servizio/servizio_actions.tsx
Marco Pedone 539f4dc4f8 chore: update biome schema version and refactor code style
- Updated biome schema version from 2.3.1 to 2.3.11 in biome.json.
- Refactored package.json for better readability and updated dependencies.
- Adjusted class names in InformationBubble, ChatAttachments, ChatBubble, FileUpload, Loading, and other components for consistent styling.
- Changed height classes from fixed pixel values to relative values for better responsiveness.
- Removed unnecessary comments and unused variables in user.controller.ts.
2026-01-14 14:21:57 +01:00

583 lines
16 KiB
TypeScript

import { add } from "date-fns";
import {
Bug,
CalendarClock,
ChevronDown,
ChevronUp,
ClockAlert,
Cog,
Info,
Plus,
SlidersHorizontal,
Trash2,
UserCircle,
} from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import {
Credenza,
CredenzaBody,
CredenzaContent,
CredenzaDescription,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "~/components/custom_ui/credenza";
import { MultiSelect } from "~/components/custom_ui/multiselect";
import { LoadingPage } from "~/components/loading";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
import { Button } from "~/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "~/components/ui/collapsible";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import {
type FormValues as EditFormValues,
FormEditServizio,
} from "~/forms/FormEditServizioAdmin";
import { FormNewServizio, type FormValues } from "~/forms/FormNewServizio";
import { cn, formatCurrency } from "~/lib/utils";
import { useServizio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
import { Confirm } from "../confirm";
import { Progress } from "../ui/progress";
export const ServizioActions = () => {
const { servizio, isAdmin, userId } = useServizio();
return (
<div className="flex w-full flex-col flex-wrap justify-between gap-3">
{isAdmin && (
<div className="flex items-center gap-3">
<span className="font-semibold">Azioni Admin:</span>
<AddAnnuncio servizioId={servizio.servizio_id} userId={userId} />
<EditServizioAdmin />
</div>
)}
<ServizioInfos />
</div>
);
};
const ServizioInfos = () => {
const { servizio } = useServizio();
const [open, setOpen] = useState(false);
const openContattiAnnunci = servizio.annunci.filter(
(a) => a.open_contatti_at !== null,
);
if (!servizio.decorrenza) return null;
const dec_txt = new Date(servizio.decorrenza).toLocaleDateString("it-IT");
const end_txt = new Date(
add(servizio.decorrenza, { days: 60 }),
).toLocaleDateString("it-IT");
return (
<Collapsible className="space-y-2" onOpenChange={setOpen} open={open}>
<CollapsibleTrigger asChild>
{open ? (
<Button
className="w-full justify-between data-[state=open]:bg-accent dark:data-[state=open]:bg-input/50"
variant="outline"
>
<span className="flex items-center gap-2">
<Info />
<span>Informazioni servizio</span>
</span>
<span>
<ChevronUp />
</span>
</Button>
) : (
<Button className="w-full justify-between" variant="outline">
<div className="flex items-center gap-6">
<span className="flex items-center gap-2">
<UserCircle className="size-4 text-primary" />
<span>{openContattiAnnunci.length} / 10</span>
</span>
<span className="flex items-center gap-2">
<CalendarClock className="size-4 text-primary" />
<span>{end_txt}</span>
</span>
</div>
<div className="flex items-center gap-2">
<span>Dettagli</span>
<ChevronDown />
</div>
</Button>
)}
</CollapsibleTrigger>
<CollapsibleContent className="mb-4 flex w-full flex-col gap-8 rounded-md border bg-card p-2 sm:items-start sm:gap-4">
<ServizioBasicActions />
<div className="flex w-full flex-col gap-8 sm:flex-row sm:items-start">
<div className="flex flex-2 flex-col gap-2 sm:w-1/2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<UserCircle className="text-primary" />
<span className="font-medium">Contatti sbloccati</span>
</div>
<span className="text-sm">{openContattiAnnunci.length} / 10</span>
</div>
<Progress className="h-2" value={openContattiAnnunci.length * 10} />
</div>
<div className="flex flex-col flex-wrap gap-2 sm:w-1/2 sm:flex-row sm:items-center">
<div className="flex items-center gap-2">
<CalendarClock className="text-primary" />
<span className="font-medium">Durata servizio:</span>
</div>
<span>
{dec_txt} - {end_txt}
</span>
</div>
</div>
<ServizioPacksInfos />
</CollapsibleContent>
</Collapsible>
);
};
const ServizioBasicActions = ({ className }: { className?: string }) => {
const { servizio, isAdmin } = useServizio();
const canUserEdit =
servizio.isOkAcconto &&
servizio.decorrenza !== null &&
new Date() <= add(new Date(servizio.decorrenza), { days: 60 }) &&
!servizio.annunci.some((a) => a.user_confirmed_at !== null) &&
!servizio.isInterrotto &&
!servizio.annunci.some((a) => a.user_confirmed_at !== null);
if (isAdmin || canUserEdit) {
return (
<div className={cn("flex flex-wrap items-center gap-3", className)}>
<EditParametri />
<InterruzioneServizio />
</div>
);
}
return null;
};
export const ServizioPacksInfos = ({
title = "Prezzi:",
}: {
title?: string;
}) => {
const { servizio } = useServizio();
const { data: packs } = api.servizio.getPacksSolution.useQuery({
servizioId: servizio.servizio_id,
});
if (!packs) return null;
return (
<div className="w-full space-y-2">
<h3 className="font-semibold text-lg">{title}</h3>
<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>
</div>
</div>
);
};
const AddAnnuncio = ({
servizioId,
userId,
}: {
servizioId: ServizioServizioId;
userId: UsersId;
}) => {
const { data: codici, isLoading: loading } =
api.servizio.getAnnunciAvailableToAdd.useQuery({
servizioId,
});
const [selectedAnnunci, setSelectedAnnunci] = useState<
{ value: AnnunciId; label: string }[]
>([]);
const [openModal, setOpenModal] = useState(false);
const utils = api.useUtils();
const { mutate } = api.servizio.addServizioAnnunci.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiunta dell'annuncio");
},
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getAnnunciAvailableToAdd.invalidate({
servizioId,
});
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
await utils.servizio.getUserServizi.invalidate({ userId });
toast.success("Annuncio aggiunto");
setOpenModal(false);
setSelectedAnnunci([]);
},
});
return (
<Dialog onOpenChange={setOpenModal} open={openModal}>
<DialogTrigger asChild>
<Button
aria-label="Add Annuncio"
className=""
onClick={() => {
setSelectedAnnunci([]);
}}
variant="outline"
>
<Plus />
Annuncio
</Button>
</DialogTrigger>
<DialogContent
className="sm:max-w-106.25"
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&apos;utente
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
{loading ? (
<LoadingPage />
) : (
<MultiSelect
defaultValue={undefined}
elementId="select-annuncio"
elementName="annuncio"
isMulti={true}
onValueChange={(value) => {
setSelectedAnnunci(
value.map((v) => ({
label: v.label,
value: parseInt(v.value) as AnnunciId,
})),
);
}}
options={
codici?.map((cod) => ({
label: cod.codice,
value: cod.id.toString(),
})) || []
}
placeholder=""
/>
)}
</div>
<DialogFooter>
<Button
aria-label="Cancel Add Annuncio"
disabled={!selectedAnnunci.length}
onClick={() => {
if (!selectedAnnunci.length) return;
mutate({
annunci: selectedAnnunci.map((annuncio) => annuncio.value),
servizioId,
});
}}
type="button"
>
Aggiungi
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
const InterruzioneServizio = () => {
const { servizio, userId, isAdmin } = useServizio();
const beforeInterruzioneLimit =
servizio.decorrenza != null &&
new Date() <=
add(servizio.decorrenza, {
days: 14,
});
const utils = api.useUtils();
const { mutate } = api.servizio.interruzioneServizio.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante la richiesta di interruzione");
},
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
toast.success("Interruzione richiesta");
},
});
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
aria-label="Request Interruzione"
className=""
disabled={
!isAdmin &&
(!beforeInterruzioneLimit ||
servizio.isInterrotto ||
servizio.annunci.some((a) => a.user_confirmed_at !== null))
}
>
<ClockAlert /> <span>Richiedi Interruzione</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Sei sicuro di voler richiedere l&apos;interruzione?
</AlertDialogTitle>
<AlertDialogDescription>
Questa azione è irreversibile e comporta la disattivazione dei
servizi attivati. Confermando l&apos;azione recedi dal servizio
secondo le condizioni contrattuali. Verrai notificato via email
della conferma dell&apos;avvenuta interruzione.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annulla</AlertDialogCancel>
<AlertDialogAction
aria-label="Confirm Interruzione"
onClick={() => mutate({ servizioId: servizio.servizio_id })}
>
Continua
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
const EditParametri = () => {
const { servizioId, userId, isAdmin, servizio } = useServizio();
const [openEditParametri, setOpenEditParametri] = useState(false);
const { data, isLoading } = api.servizio.getServizio.useQuery({ servizioId });
const utils = api.useUtils();
const { mutate: update } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
toast.success("Servizio modificato con successo");
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getServizio.invalidate({ servizioId });
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
await utils.servizio.getPacksSolution.invalidate({ servizioId });
setOpenEditParametri(false);
},
});
function onSubmit(fields: FormValues) {
update({
data: {
...fields,
},
servizioId,
});
}
return (
<Credenza onOpenChange={setOpenEditParametri} open={openEditParametri}>
<CredenzaTrigger asChild>
<Button
aria-label="Parametri di ricerca"
className=""
disabled={
!isAdmin &&
(servizio.isInterrotto ||
servizio.annunci.some((a) => a.hasConfermaAdmin))
}
variant="secondary"
>
<SlidersHorizontal />
Parametri
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] w-full sm:max-w-5xl">
<CredenzaHeader>
<CredenzaTitle>Parametri di ricerca</CredenzaTitle>
<CredenzaDescription className="sr-only">
Parametri
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] w-full max-w-3xl overflow-y-auto pb-5 sm:max-w-5xl">
{isLoading ? (
<LoadingPage />
) : (
<FormNewServizio
initialData={data}
isLimited={!isAdmin}
onSubmit={onSubmit}
/>
)}
</CredenzaBody>
</CredenzaContent>
</Credenza>
);
};
const EditServizioAdmin = () => {
const { servizioId, userId, servizio } = useServizio();
const [open, setOpen] = useState(false);
const { data, isLoading } = api.servizio.getServizio.useQuery({ servizioId });
const utils = api.useUtils();
const { mutate: update } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
toast.success("Servizio modificato con successo");
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getServizio.invalidate({ servizioId });
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
await utils.servizio.getPacksSolution.invalidate({ servizioId });
setOpen(false);
},
});
const { mutate: remove } = api.servizio.deleteServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
toast.success("Servizio rimosso con successo");
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getServizio.invalidate({ servizioId });
},
});
function onSubmit(fields: EditFormValues) {
update({
data: {
...fields,
},
servizioId,
});
}
return (
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild>
<Button aria-label="Modifica Servizio Admin">
<Cog />
Modifica
</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">
{isLoading ? (
<LoadingPage />
) : (
<FormEditServizio initialData={data} onSubmit={onSubmit} />
)}
<Collapsible>
<CollapsibleTrigger asChild>
<Button variant="warning">
<Bug /> Debug Info Servizio
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
{(() => {
const { annunci, ...rest } = servizio;
return <pre>{JSON.stringify(rest, null, 2)}</pre>;
})()}
</CollapsibleContent>
</Collapsible>
<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>
</CredenzaBody>
</CredenzaContent>
</Credenza>
);
};