2026-03-27 18:10:17 +01:00
|
|
|
import type { Table } from "@tanstack/react-table";
|
|
|
|
|
import { format } from "date-fns";
|
2026-03-20 14:06:55 +01:00
|
|
|
import { NotebookPen, Toolbox } from "lucide-react";
|
2025-12-05 16:36:17 +01:00
|
|
|
import Image from "next/image";
|
2026-03-27 18:10:17 +01:00
|
|
|
import { type RefObject, useState } from "react";
|
2026-03-20 14:06:55 +01:00
|
|
|
import toast from "react-hot-toast";
|
|
|
|
|
import LoadingButton from "~/components/custom_ui/loading-button";
|
2026-01-16 15:47:38 +01:00
|
|
|
import {
|
|
|
|
|
AdminActions,
|
|
|
|
|
UserActions,
|
|
|
|
|
} from "~/components/servizio/annuncio_actions";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
2026-03-27 18:10:05 +01:00
|
|
|
import { Textarea } from "~/components/ui/textarea";
|
2026-03-20 14:06:55 +01:00
|
|
|
import { useMediaQuery } from "~/hooks/use-media-query";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { handleConsegna } from "~/lib/annuncio_details";
|
2025-11-28 15:11:14 +01:00
|
|
|
import { getStorageUrl } from "~/lib/storage_utils";
|
2026-03-27 18:10:17 +01:00
|
|
|
import { MutationPageRestore } from "~/lib/tableUtils";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { cn, formatCurrency } from "~/lib/utils";
|
|
|
|
|
import { useTranslation } from "~/providers/I18nProvider";
|
2026-01-16 15:47:38 +01:00
|
|
|
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
|
2026-03-25 17:30:33 +01:00
|
|
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
|
|
|
|
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
2025-11-17 19:01:21 +01:00
|
|
|
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
|
2026-03-27 18:10:17 +01:00
|
|
|
import type {
|
|
|
|
|
RichiesteData,
|
|
|
|
|
ServizioAnnuncioData,
|
|
|
|
|
} from "~/server/controllers/servizio.controller";
|
2026-03-20 14:06:55 +01:00
|
|
|
import { api } from "~/utils/api";
|
2026-01-16 17:11:53 +01:00
|
|
|
import { Button } from "../ui/button";
|
|
|
|
|
import {
|
|
|
|
|
Collapsible,
|
|
|
|
|
CollapsibleContent,
|
|
|
|
|
CollapsibleTrigger,
|
|
|
|
|
} from "../ui/collapsible";
|
2025-11-17 19:01:21 +01:00
|
|
|
import { AnnuncioDettaglio, TipoBadge } from "./annuncio_dettaglio";
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2026-01-16 17:11:53 +01:00
|
|
|
export const AnnuncioCard = ({ className }: { className?: string }) => {
|
2026-01-16 15:47:38 +01:00
|
|
|
const { isAdmin } = useServizio();
|
2025-08-28 18:27:07 +02:00
|
|
|
const { data } = useServizioAnnuncio();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
2026-03-25 17:30:33 +01:00
|
|
|
<AnnuncioCardContent
|
2025-08-29 16:18:32 +02:00
|
|
|
className={className}
|
2026-03-25 17:30:33 +01:00
|
|
|
data={data}
|
2026-01-16 17:11:53 +01:00
|
|
|
interactions={
|
|
|
|
|
isAdmin ? (
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<Collapsible className="space-y-2">
|
2026-01-19 10:23:16 +01:00
|
|
|
<CollapsibleTrigger asChild>
|
2026-03-12 17:02:35 +01:00
|
|
|
<Button size="sm" variant="outline">
|
2026-01-16 17:11:53 +01:00
|
|
|
<Toolbox />
|
|
|
|
|
<span>Azioni Utente</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</CollapsibleTrigger>
|
2026-02-24 12:02:02 +01:00
|
|
|
<CollapsibleContent className="rounded-md border border-card bg-card p-2">
|
2026-01-16 17:11:53 +01:00
|
|
|
<UserActions />
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
|
|
|
|
|
<AdminActions />
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<UserActions />
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-03-25 17:30:33 +01:00
|
|
|
isAdmin={isAdmin}
|
2025-08-28 18:27:07 +02:00
|
|
|
/>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
2026-01-09 13:13:17 +01:00
|
|
|
|
2026-04-03 18:57:39 +02:00
|
|
|
const AnnuncioCardContent = ({
|
2026-01-16 17:11:53 +01:00
|
|
|
interactions,
|
|
|
|
|
className,
|
|
|
|
|
data,
|
2026-03-25 17:30:33 +01:00
|
|
|
isAdmin,
|
2026-01-16 17:11:53 +01:00
|
|
|
}: {
|
|
|
|
|
interactions?: React.ReactNode;
|
|
|
|
|
className?: string;
|
2026-03-25 17:30:33 +01:00
|
|
|
data: ServizioAnnuncioData;
|
|
|
|
|
isAdmin: boolean;
|
2026-01-16 17:11:53 +01:00
|
|
|
}) => {
|
|
|
|
|
return (
|
2026-02-24 12:02:02 +01:00
|
|
|
<Card
|
|
|
|
|
className={cn(
|
2026-03-20 14:06:55 +01:00
|
|
|
"w-full max-w-7xl gap-2 rounded-md border-none bg-muted py-3",
|
2026-02-24 12:02:02 +01:00
|
|
|
className,
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-01-16 17:11:53 +01:00
|
|
|
<CardHeader className="flex flex-row flex-wrap items-center justify-between space-y-0 px-3 pb-2">
|
|
|
|
|
<CardTitle className="text-2xl">Annuncio {data.codice}</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="px-3">
|
|
|
|
|
<div
|
2026-03-20 14:06:55 +01:00
|
|
|
className={cn(
|
|
|
|
|
"grid grid-cols-1 gap-4",
|
|
|
|
|
isAdmin && "sm:grid-cols-[2fr_1fr]",
|
|
|
|
|
)}
|
2026-01-16 17:11:53 +01:00
|
|
|
>
|
2026-03-20 14:06:55 +01:00
|
|
|
<div
|
|
|
|
|
className={cn("@container flex w-full flex-col gap-4")}
|
|
|
|
|
id="container"
|
|
|
|
|
>
|
|
|
|
|
<AnnuncioDisplay data={data} />
|
|
|
|
|
|
|
|
|
|
<div className="flex w-full flex-col gap-4">{interactions}</div>
|
|
|
|
|
</div>
|
2026-03-25 17:30:33 +01:00
|
|
|
{isAdmin && (
|
|
|
|
|
<AnnuncioNote
|
|
|
|
|
annuncioId={data.id}
|
|
|
|
|
note={data.note}
|
|
|
|
|
servizioId={data.servizio_id}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-01-16 17:11:53 +01:00
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
};
|
2026-03-27 18:10:17 +01:00
|
|
|
export const AnnuncioCardRichieste = ({
|
|
|
|
|
tableRef,
|
|
|
|
|
data,
|
|
|
|
|
}: {
|
|
|
|
|
tableRef: RefObject<Table<RichiesteData> | null>;
|
|
|
|
|
data: ServizioAnnuncioData;
|
|
|
|
|
}) => {
|
|
|
|
|
const [newNote, setNewNote] = useState<string | null>(data.note);
|
|
|
|
|
const utils = api.useUtils();
|
|
|
|
|
const { onMutate, onSettled } = MutationPageRestore(tableRef);
|
|
|
|
|
const { mutate, isPending } = api.servizio.updateServizioAnnunci.useMutation({
|
|
|
|
|
onMutate,
|
|
|
|
|
onSettled,
|
|
|
|
|
onSuccess: async () => {
|
|
|
|
|
toast.success("Note aggiornate");
|
|
|
|
|
await utils.servizio.invalidate();
|
|
|
|
|
},
|
|
|
|
|
onError: () => {
|
|
|
|
|
toast.error("Errore nell'aggiornamento note");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const isDesktop = useMediaQuery("(min-width: 40rem)");
|
|
|
|
|
return (
|
|
|
|
|
<Card className="w-full max-w-7xl gap-2 rounded-md border-none bg-muted py-3">
|
|
|
|
|
<CardHeader className="flex flex-row flex-wrap items-center justify-between space-y-0 px-3 pb-2">
|
|
|
|
|
<CardTitle className="text-2xl">Annuncio {data.codice}</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="px-3">
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-[2fr_1fr]">
|
|
|
|
|
<div
|
|
|
|
|
className={cn("@container flex w-full flex-col gap-4")}
|
|
|
|
|
id="container"
|
|
|
|
|
>
|
|
|
|
|
<AnnuncioDisplay data={data} />
|
|
|
|
|
|
|
|
|
|
<div className="flex w-full flex-col gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<p>
|
|
|
|
|
Visto contatti:{" "}
|
|
|
|
|
{data.open_contatti_at
|
|
|
|
|
? format(data.open_contatti_at, "dd/MM/yyyy HH:mm")
|
|
|
|
|
: "Non ancora visto"}
|
|
|
|
|
</p>
|
|
|
|
|
{/*TODO CAPIRE COSA AGGIUNGERE */}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Collapsible
|
|
|
|
|
className={cn("space-y-2", isDesktop && "relative -top-10")}
|
|
|
|
|
open={isDesktop || undefined}
|
|
|
|
|
>
|
|
|
|
|
<CollapsibleTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
className={cn(isDesktop && "cursor-default")}
|
|
|
|
|
size="sm"
|
|
|
|
|
variant={isDesktop ? "flat" : "outline"}
|
|
|
|
|
>
|
|
|
|
|
<NotebookPen />
|
|
|
|
|
<span>Annotazioni</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<CollapsibleContent className="">
|
|
|
|
|
<div className="flex w-full flex-col gap-2">
|
|
|
|
|
<Textarea
|
|
|
|
|
className="min-h-64"
|
|
|
|
|
id={`note-${data.id}`}
|
|
|
|
|
onChange={(e) => setNewNote(e.target.value)}
|
|
|
|
|
placeholder=""
|
|
|
|
|
value={newNote || ""}
|
|
|
|
|
/>
|
|
|
|
|
<LoadingButton
|
|
|
|
|
disabled={newNote === data.note}
|
|
|
|
|
loading={isPending}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
mutate({
|
|
|
|
|
annuncioId: data.id,
|
|
|
|
|
servizioId: data.servizio_id,
|
|
|
|
|
data: { note: newNote },
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
type="button"
|
|
|
|
|
variant="success"
|
|
|
|
|
>
|
|
|
|
|
Salva
|
|
|
|
|
</LoadingButton>
|
|
|
|
|
</div>
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
};
|
2026-01-16 17:11:53 +01:00
|
|
|
|
2026-03-25 17:30:33 +01:00
|
|
|
const AnnuncioNote = ({
|
|
|
|
|
servizioId,
|
|
|
|
|
annuncioId,
|
|
|
|
|
note,
|
|
|
|
|
}: {
|
|
|
|
|
servizioId: ServizioServizioId;
|
|
|
|
|
annuncioId: AnnunciId;
|
|
|
|
|
note: string | null;
|
|
|
|
|
}) => {
|
|
|
|
|
const [newNote, setNewNote] = useState<string | null>(note);
|
2026-03-20 14:06:55 +01:00
|
|
|
const utils = api.useUtils();
|
|
|
|
|
const { mutate, isPending } = api.servizio.updateServizioAnnunci.useMutation({
|
|
|
|
|
onSuccess: async () => {
|
|
|
|
|
toast.success("Note aggiornate");
|
|
|
|
|
await utils.servizio.invalidate();
|
|
|
|
|
},
|
|
|
|
|
onError: () => {
|
|
|
|
|
toast.error("Errore nell'aggiornamento note");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const isDesktop = useMediaQuery("(min-width: 40rem)");
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Collapsible
|
|
|
|
|
className={cn("space-y-2", isDesktop && "relative -top-10")}
|
|
|
|
|
open={isDesktop || undefined}
|
|
|
|
|
>
|
|
|
|
|
<CollapsibleTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
className={cn(isDesktop && "cursor-default")}
|
|
|
|
|
size="sm"
|
|
|
|
|
variant={isDesktop ? "flat" : "outline"}
|
|
|
|
|
>
|
|
|
|
|
<NotebookPen />
|
|
|
|
|
<span>Annotazioni</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<CollapsibleContent className="">
|
|
|
|
|
<div className="flex w-full flex-col gap-2">
|
|
|
|
|
<Textarea
|
|
|
|
|
className="min-h-64"
|
2026-03-25 17:30:33 +01:00
|
|
|
id={`note-${annuncioId}`}
|
2026-03-20 14:06:55 +01:00
|
|
|
onChange={(e) => setNewNote(e.target.value)}
|
|
|
|
|
placeholder=""
|
2026-03-25 17:30:33 +01:00
|
|
|
value={newNote || ""}
|
2026-03-20 14:06:55 +01:00
|
|
|
/>
|
|
|
|
|
<LoadingButton
|
2026-03-25 17:30:33 +01:00
|
|
|
disabled={newNote === note}
|
2026-03-20 14:06:55 +01:00
|
|
|
loading={isPending}
|
2026-03-25 17:30:33 +01:00
|
|
|
onClick={() => {
|
2026-03-20 14:06:55 +01:00
|
|
|
mutate({
|
2026-03-25 17:30:33 +01:00
|
|
|
annuncioId,
|
2026-03-20 14:06:55 +01:00
|
|
|
servizioId,
|
|
|
|
|
data: { note: newNote },
|
2026-03-25 17:30:33 +01:00
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
type="button"
|
2026-03-20 14:06:55 +01:00
|
|
|
variant="success"
|
|
|
|
|
>
|
|
|
|
|
Salva
|
|
|
|
|
</LoadingButton>
|
|
|
|
|
</div>
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-09 13:13:17 +01:00
|
|
|
export const AnnuncioDisplay = ({
|
|
|
|
|
data,
|
2026-01-09 14:47:28 +01:00
|
|
|
className,
|
2026-01-09 13:13:17 +01:00
|
|
|
}: {
|
|
|
|
|
data: AnnuncioRicerca;
|
2026-01-09 14:47:28 +01:00
|
|
|
className?: string;
|
2026-01-09 13:13:17 +01:00
|
|
|
}) => {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
2026-03-20 14:06:55 +01:00
|
|
|
"grid @md:grid-cols-[12rem_1fr] grid-cols-[7rem_1fr] @sm:gap-x-4 gap-x-1 gap-y-2 rounded-md bg-card md:max-w-5xl lg:gap-x-4 xl:gap-x-6",
|
2026-01-09 14:47:28 +01:00
|
|
|
className,
|
2026-01-09 13:13:17 +01:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<div className="row-span-1 md:row-span-3">
|
|
|
|
|
<Image
|
|
|
|
|
alt={data.codice}
|
|
|
|
|
className="size-24 rounded-md bg-[#e6e9ec] object-cover md:size-48"
|
|
|
|
|
height={1080}
|
|
|
|
|
priority
|
|
|
|
|
src={
|
|
|
|
|
(data?.images[0] &&
|
|
|
|
|
getStorageUrl({
|
|
|
|
|
storageId: data.images[0].img,
|
|
|
|
|
params: {
|
|
|
|
|
cacheKey: data.media_updated_at?.toISOString(),
|
|
|
|
|
media: "image",
|
|
|
|
|
},
|
|
|
|
|
})) ||
|
|
|
|
|
"/fallback-image.png"
|
|
|
|
|
}
|
|
|
|
|
width={1920}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-25 15:30:53 +01:00
|
|
|
<div className="col-span-1 flex w-full max-w-xl flex-wrap gap-5 pt-2 md:col-span-1 md:mt-4 md:pr-8">
|
2026-01-09 13:13:17 +01:00
|
|
|
<span className="text-wrap">{data.titolo_it}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-20 14:06:55 +01:00
|
|
|
<div className="col-span-2 flex w-full max-w-xl items-center justify-between gap-2 pr-3 md:col-span-1 md:pr-8">
|
2026-01-09 13:13:17 +01:00
|
|
|
<TipoBadge mini tipo={data.tipo} />
|
|
|
|
|
<span className="max-w-auto truncate">
|
|
|
|
|
{handleConsegna({
|
|
|
|
|
aggiornamento: t.card.in_aggiornamento,
|
|
|
|
|
consegna: data.consegna,
|
|
|
|
|
consegna_da: t.card.consegna_da,
|
|
|
|
|
mesi: t.parametri.mesi,
|
|
|
|
|
subito: t.card.consegna_subito,
|
|
|
|
|
})}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="font-semibold">
|
|
|
|
|
{formatCurrency(data.prezzo / 1e2)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-03-12 17:02:35 +01:00
|
|
|
<div className="col-span-2 w-full max-w-xl md:col-span-1 md:pr-8">
|
2026-01-09 13:13:17 +01:00
|
|
|
<AnnuncioDettaglio data={data} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|