Refactor AnnuncioCard to use AdminActions and UserActions based on user role; remove unused Interactions component; update button sizes and styles across various components; enhance ServizioContent with mutation handling; improve user feedback on actions; update confirmation messages for clarity; adjust layout for better responsiveness in AnnunciRichiesti; streamline form submissions with updated data structures.

This commit is contained in:
Marco Pedone 2026-01-16 15:47:38 +01:00
parent 804b4fef90
commit 1baca2997a
30 changed files with 512 additions and 538 deletions

View file

@ -30,10 +30,10 @@ export const MappaDialogFullscreen = ({
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<Button className="w-auto grow" disabled={!lat || !long}>
<MapIcon className="size-5" /> {t.mappa}
<MapIcon /> {t.mappa}
</Button>
</DialogTrigger>
<DialogContent className="[&_[data-slot=dialog-close]>svg]:!size-6 h-full w-screen max-w-[100vw] p-0 sm:max-h-[60vh] sm:max-w-2xl [&_[data-slot=dialog-close]]:cursor-pointer [&_[data-slot=dialog-close]]:rounded-md [&_[data-slot=dialog-close]]:bg-white [&_[data-slot=dialog-close]]:opacity-100">
<DialogContent className="h-full w-screen max-w-[100vw] p-0 **:data-[slot=dialog-close]:cursor-pointer **:data-[slot=dialog-close]:rounded-md **:data-[slot=dialog-close]:bg-white **:data-[slot=dialog-close]:opacity-100 sm:max-h-[60vh] sm:max-w-2xl [&_[data-slot=dialog-close]>svg]:size-6!">
<DialogHeader className="hidden h-0">
<DialogTitle className="sr-only">{t.mappa}</DialogTitle>
<DialogDescription className="sr-only">{t.mappa}</DialogDescription>

View file

@ -55,7 +55,7 @@ export const CTA_TipologiaModal = () => {
>
<DialogTrigger asChild>
<Button className="h-9">
<CircleHelp className="size-5" />{" "}
<CircleHelp />{" "}
<span className="hidden sm:block">
{t.tipologia_tutorial.btn_title}
</span>

View file

@ -51,7 +51,7 @@ export const AnnuncioInteractions = ({
}}
>
<Button className="w-full">
<LogIn className="size-5" />
<LogIn />
<span>Accedi se hai un account</span>
</Button>
</Link> */}

View file

@ -19,7 +19,7 @@ export const ContattoAnnuncio = () => {
<Credenza>
<CredenzaTrigger asChild>
<Button className="w-full" variant="destructive">
<MessageCirclePlus className="size-5" /> Contattaci
<MessageCirclePlus /> Contattaci
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">

View file

@ -71,11 +71,7 @@ export const Sidebar = ({
onClick={() => toggleSidebar(!minimized)}
variant="ghost"
>
{minimized ? (
<Expand className="size-5" />
) : (
<Minimize2 className="size-5" />
)}
{minimized ? <Expand /> : <Minimize2 />}
<span className="sr-only">Toggle Menu</span>
</Button>
</div>

View file

@ -49,7 +49,7 @@ export const ComeFunziona = () => {
>
<ExpandableScreenTrigger className="w-full">
<Button className="w-full">
<HelpCircle className="size-5" />
<HelpCircle />
<span>Scopri come funziona</span>
</Button>
</ExpandableScreenTrigger>

View file

@ -76,7 +76,7 @@ export const UserHeaderSection = ({
>
{inSidebar && t.auth.login.titolo}
<LogIn className="size-5" />
<LogIn />
</Button>
) : (
<DropdownMenu onOpenChange={setOpen} open={open}>

View file

@ -27,7 +27,7 @@ export function RicercaCommand() {
<Dialog onOpenChange={setOpen} open={open}>
<DialogTrigger asChild>
<Button aria-label="ricercaBtn" className="p-2" variant="ghost">
<Search className="size-5" />
<Search />
</Button>
</DialogTrigger>
<DialogContent className="border-none bg-transparent **:data-[slot=dialog-close]:hidden sm:max-w-xl">

View file

@ -14,7 +14,7 @@ export const SettingsPopover = () => {
<Popover>
<PopoverTrigger asChild>
<Button aria-label="Settings" className="p-2" variant="ghost">
<Settings className="size-5" />
<Settings />
</Button>
</PopoverTrigger>
<PopoverContent className="flex w-fit justify-center gap-4 p-4">

View file

@ -109,7 +109,7 @@ export const OnboardTutorial = () => {
<AlertDialogFooter>
<Button onClick={handleClose} size="lg" variant="success">
<span>{t.onboarding_tutorial.button_text}</span>
<ArrowRight className="size-5" />
<ArrowRight />
</Button>
</AlertDialogFooter>
</AlertDialogContent>

View file

@ -40,13 +40,13 @@ export const PricingChoice = () => {
<Tabs className="mb-2 w-full max-w-xl" defaultValue="transitorio">
<TabsList className="flex h-auto w-full items-center justify-between gap-1 p-1 dark:bg-primary">
<TabsTrigger
className="w-full cursor-pointer border-0 bg-transitorio px-2 text-xl sm:px-3 dark:border-0 dark:text-fxd-foreground [&[data-state=active]]:border-0 [&[data-state=active]]:bg-transitorio dark:[&[data-state=active]]:border-0 dark:[&[data-state=active]]:bg-transitorio dark:[&[data-state=active]]:text-fxd-foreground"
className="w-full cursor-pointer border-0 bg-transitorio px-2 text-xl data-[state=active]:border-0 data-[state=active]:bg-transitorio sm:px-3 dark:border-0 dark:text-fxd-foreground dark:data-[state=active]:border-0 dark:data-[state=active]:bg-transitorio dark:data-[state=active]:text-fxd-foreground"
value="transitorio"
>
Transitorio
</TabsTrigger>
<TabsTrigger
className="w-full cursor-pointer border-0 bg-stabile px-2 text-xl sm:px-3 dark:border-0 dark:text-fxd-foreground [&[data-state=active]]:border-0 [&[data-state=active]]:bg-stabile dark:[&[data-state=active]]:border-0 dark:[&[data-state=active]]:bg-stabile dark:[&[data-state=active]]:text-fxd-foreground"
className="w-full cursor-pointer border-0 bg-stabile px-2 text-xl data-[state=active]:border-0 data-[state=active]:bg-stabile sm:px-3 dark:border-0 dark:text-fxd-foreground dark:data-[state=active]:border-0 dark:data-[state=active]:bg-stabile dark:data-[state=active]:text-fxd-foreground"
value="stabile"
>
Stabile
@ -86,7 +86,7 @@ export const PricingChoice = () => {
<Link aria-label="Link to pricing page" href="/prezzi">
<Button className="flex h-12 flex-row gap-2 px-6 py-4 text-2xl hover:underline">
<span>Scopri di più sui prezzi</span>
<ExternalLink className="size-5" />
<ExternalLink />
</Button>
</Link>
</div>

View file

@ -66,14 +66,11 @@ export const AdminLavoraConferma = () => {
<Button
aria-label="Open Lavora Conferma Section"
className="w-fit"
variant={data.hasConfermaAdmin ? "success" : "secondary"}
size="sm"
variant={data.hasConfermaAdmin ? "success" : "orange"}
>
{data.hasConfermaAdmin ? (
<CircleCheck className="size-6" />
) : (
<Wrench className="size-6" />
)}
Conferma
{data.hasConfermaAdmin ? <CircleCheck /> : <Wrench />}
<span>Conferma</span>
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] w-full px-3 sm:max-w-6xl">
@ -823,7 +820,7 @@ const SendConferma = () => {
<StabileSection />
)}
<GeneralSection />
<ServizioPacksInfos title="Prezzi in base a parametri:" />
<ServizioPacksInfos />
</div>
<div className="space-y-2">
{!docOk && (

View file

@ -38,13 +38,10 @@ export const AdminContratto = () => {
<Button
aria-label="Open Contratto Section"
className="w-fit"
size="sm"
variant={data.doc_contratto_added ? "success" : "secondary"}
>
{data.doc_contratto_added ? (
<CircleCheck className="size-6" />
) : (
<Wrench className="size-6" />
)}
{data.doc_contratto_added ? <CircleCheck /> : <Wrench />}
Contratto
</Button>
</CredenzaTrigger>

View file

@ -1,19 +1,35 @@
import { ExternalLink, FolderKanban, ShieldUser, Trash2 } from "lucide-react";
import Link from "next/link";
import toast from "react-hot-toast";
import { Confirm } from "~/components/confirm";
import { Button } from "~/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
ExternalLink,
Eye,
FileBadge,
FileText,
FolderKanban,
PackageCheck,
Pointer,
Printer,
Trash2,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import { Button, type ButtonConfigs } from "~/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/ui/tooltip";
import { cn } from "~/lib/utils";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import { api } from "~/utils/api";
import { Confirm } from "../confirm";
import { AdminLavoraConferma } from "./admin_conferma";
import { AdminContratto } from "./admin_contratto";
import { ConfermaAnnuncioModal, ConfermaInLavorazioneModal } from "./conferma";
import { ContattiProprietarioModal } from "./modale_contatti";
export const AnnuncioActions = () => {
export const AdminActions = () => {
const { servizioId, userId, isAdmin } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
@ -31,79 +47,330 @@ export const AnnuncioActions = () => {
toast.success("Annuncio rimosso");
},
});
if (!isAdmin) return null;
if ((!isAdmin && data.open_contatti_at == null) || isAdmin) {
return (
<div className="flex w-full max-w-fit flex-wrap justify-start gap-3 sm:justify-end">
{isAdmin && data.user_confirmed_at != null && <AdminLavoraConferma />}
{isAdmin && data.accettato_conferma_at != null && (
<>
<AdminContratto />
{data.gestionale_id && (
<Link
aria-label="Open Gestionale"
href={`https://win-jk822fggr6b:480/id/${data.gestionale_id}`}
target="_blank"
>
<Button>
<FolderKanban className="size-5" /> Gestionale
</Button>
</Link>
)}
</>
)}
{isAdmin && (
<Popover>
<PopoverTrigger asChild>
<Button>
<ShieldUser />
<span>Admin</span>
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className="flex w-fit flex-col items-center gap-4"
return (
<div className="flex w-full max-w-fit flex-wrap justify-start gap-3 sm:justify-end">
{data.user_confirmed_at != null && <AdminLavoraConferma />}
{data.accettato_conferma_at != null && (
<>
<AdminContratto />
{data.gestionale_id && (
<Link
aria-label="Open Gestionale"
href={`https://win-jk822fggr6b:480/id/${data.gestionale_id}`}
target="_blank"
>
<Link
href={`/area-riservata/admin/edit-annuncio/${annuncioId}`}
target="_blank"
>
<Button
aria-label="Edit Annuncio"
className="w-full"
size="sm"
variant="secondary"
>
<ExternalLink />
<span>Modifica Annuncio</span>
</Button>
</Link>
<Button size="sm">
<FolderKanban /> Gestionale
</Button>
</Link>
)}
</>
)}
<Link href={`/annuncio/${data.codice}`} target="_blank">
<Button size="sm" type="button" variant="outline">
<Eye /> pubblica
</Button>
</Link>
<Link
href={`/area-riservata/admin/edit-annuncio/${annuncioId}`}
target="_blank"
>
<Button
aria-label="Edit Annuncio"
className="w-fit"
size="sm"
variant="secondary"
>
<ExternalLink />
<span>Modifica</span>
</Button>
</Link>
<Confirm
description="Sei sicuro di voler eliminare l'annuncio?"
onConfirm={() => {
mutate({
annuncioId,
servizioId,
});
}}
title="Elimina annuncio"
>
<Button
aria-label="Delete Annuncio"
className="w-full"
size="sm"
variant="destructive"
>
<Trash2 />
<span>Elimina</span>
</Button>
</Confirm>
</PopoverContent>
</Popover>
<Link
href={`/area-riservata/admin/scheda-annuncio-stampa/${data.id}`}
target="_blank"
>
<Button size="sm" type="button" variant="outline">
<Printer /> Scheda
</Button>
</Link>
<Confirm
description="Sei sicuro di voler eliminare l'annuncio?"
onConfirm={() => {
mutate({
annuncioId,
servizioId,
});
}}
title="Elimina annuncio"
>
<Button
aria-label="Delete Annuncio"
className="w-fit"
size="sm"
variant="destructive"
>
<Trash2 />
<span>Elimina</span>
</Button>
</Confirm>
</div>
);
};
export const UserActions = () => {
const { servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
const ContattiSbloccati = data.open_contatti_at !== null;
const ConfermaUtente = data.user_confirmed_at !== null;
const ConfermaLavorata = data.doc_conferma_added && data.hasConfermaAdmin;
const AccettatoConferma = data.accettato_conferma_at !== null;
return (
<>
<div className="flex w-full flex-col flex-wrap gap-2 sm:flex-row">
<ContattiProprietarioModal />
{ContattiSbloccati && !ConfermaUtente && <ConfermaAnnuncioModal />}
{ConfermaUtente && !data.hasConfermaAdmin && (
<ConfermaInLavorazioneModal />
)}
{AccettatoConferma && (
<Link
aria-label="Conferma immobile"
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
>
<Button className="w-full sm:w-fit" variant="success">
<Pointer />
Rivedi la conferma
</Button>
</Link>
)}
{ConfermaLavorata &&
(data.doc_conferma_ref ? (
<Link
aria-label="Conferma immobile"
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
>
<Button className="w-full sm:w-fit" variant="success">
<Pointer />
Confermato - Procedi ora
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="w-full cursor-not-allowed opacity-50 sm:w-fit"
variant="success"
>
<Pointer />
Confermato - Procedi ora
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>
Se hai bisogno di una copia, contattaci via email o chat.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
{data.accettato_conferma_at != null && <PostConfermaSection />}
</>
);
};
const PostConfermaSection = () => {
const { servizio, servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
const saldoPaid = servizio.isOkSaldo;
const consulenzaPaid = servizio.isOkConsulenza;
if (!saldoPaid) {
return (
<div className="flex flex-col gap-1">
<h3 className="font-semibold">
Questo annuncio è stato confermato, procedi al saldo:
</h3>
<SaldoButton annuncioId={annuncioId} />
</div>
);
}
return null;
if (!data.doc_contratto_added) {
return (
<div className="flex flex-col gap-1">
<h3 className="font-semibold text-green-500">
Ci siamo quasi! Stiamo preparando il contratto per te.
</h3>
<h3 className="font-semibold">
Una volta pronto, riceverai una notifica via email.
</h3>
</div>
);
}
return (
<div className="flex flex-col gap-2">
<h3 className="font-semibold">
{consulenzaPaid
? "Il contratto è pronto, puoi visualizzarlo:"
: "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"}
</h3>
<div className="flex flex-wrap gap-2">
{data.doc_contratto_ref ? (
<Link
aria-label="Visualizza contratto"
href={`/servizio/contratto/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="w-full sm:w-fit">
<FileText />
Visualizza il contratto
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button className="w-fit cursor-not-allowed opacity-50">
<FileText />
Visualizza il contratto
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>Se hai bisogno di una copia, contattaci via email o chat.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{!consulenzaPaid && data.contratto_tipo && <SaldoConsulenzaButton />}
{data.doc_registrazione_added &&
(data.doc_registrazione_ref ? (
<Link
aria-label="Visualizza registrazione"
href={`/servizio/contratto-registrazione/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="w-fit">
<FileBadge />
Visualizza la registrazione
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button className="w-fit cursor-not-allowed opacity-50">
<FileBadge />
Visualizza la registrazione
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>
Se hai bisogno di una copia, contattaci via email o chat.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
</div>
);
};
export const SaldoButton = ({
annuncioId,
variant = "info",
className,
}: {
annuncioId: AnnunciId;
variant?: ButtonConfigs["variant"];
className?: string;
}) => {
const { servizioId } = useServizio();
const router = useRouter();
const { mutate } = api.servizio.setupSaldoServizio.useMutation({
onMutate: () => {
toast.loading("Caricamento ...", { id: "preparazioneSaldo" });
},
onSuccess: async (data) => {
toast.success("Pagamento saldo preparato", {
id: "preparazioneSaldo",
});
await router.push(`/servizio/pagamento/${data}`);
},
});
return (
<Button
aria-label="Procedi al saldo"
className={cn("w-fit", className)}
onClick={() => {
mutate({
annuncioId,
servizioId,
});
}}
variant={variant}
>
<PackageCheck />
Procedi al saldo
</Button>
);
};
const SaldoConsulenzaButton = ({
variant = "info",
className,
}: {
variant?: ButtonConfigs["variant"];
className?: string;
}) => {
const { annuncioId } = useServizioAnnuncio();
const { servizioId } = useServizio();
const router = useRouter();
const { mutate } = api.servizio.setupSaldoConsulenzaServizio.useMutation({
onMutate: () => {
toast.loading("Caricamento ...", { id: "preparazioneSaldo" });
},
onSuccess: async (data) => {
toast.success("Pagamento saldo preparato", {
id: "preparazioneSaldo",
});
await router.push(`/servizio/pagamento/${data}`);
},
});
return (
<Button
aria-label="Procedi al saldo consulenza"
className={cn("w-fit", className)}
onClick={() => {
mutate({
annuncioId,
servizioId,
});
}}
variant={variant}
>
<PackageCheck />
Procedi al saldo della consulenza
</Button>
);
};

View file

@ -1,12 +1,14 @@
import Image from "next/image";
import { AnnuncioActions } from "~/components/servizio/annuncio_actions";
import { Interactions } from "~/components/servizio/interactions";
import {
AdminActions,
UserActions,
} from "~/components/servizio/annuncio_actions";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { handleConsegna } from "~/lib/annuncio_details";
import { getStorageUrl } from "~/lib/storage_utils";
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizioAnnuncio } from "~/providers/ServizioProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
import { AnnuncioDettaglio, TipoBadge } from "./annuncio_dettaglio";
@ -17,11 +19,11 @@ export const AnnuncioCard = ({
className?: string;
confirming?: boolean;
}) => {
const { isAdmin } = useServizio();
const { data } = useServizioAnnuncio();
return (
<BasicAnnuncioCard
actions={<AnnuncioActions />}
className={className}
confirming={confirming}
data={{
@ -31,7 +33,7 @@ export const AnnuncioCard = ({
? new Date(data.media_updated_at)
: null,
}}
interactions={<Interactions />}
interactions={isAdmin ? <AdminActions /> : <UserActions />}
/>
);
};
@ -103,13 +105,11 @@ export const AnnuncioDisplay = ({
};
export const BasicAnnuncioCard = ({
actions,
interactions,
className,
data,
confirming,
}: {
actions?: React.ReactNode;
interactions?: React.ReactNode;
className?: string;
data: AnnuncioRicerca;
@ -119,7 +119,6 @@ export const BasicAnnuncioCard = ({
<Card className={cn("w-full gap-2 border-white py-3", className)}>
<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>
{actions}
</CardHeader>
<CardContent className="px-3">
<div

View file

@ -67,7 +67,7 @@ export const ConfermaAnnuncioModal = () => {
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild>
<Button aria-label="Conferma immobile" className="w-full sm:w-fit">
<Handshake className="size-5" /> {t.richieste.cta}
<Handshake /> {t.richieste.cta}
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
@ -218,7 +218,7 @@ export const ConfermaInLavorazioneModal = () => {
aria-label="Conferma in lavorazione"
className="w-full sm:w-fit"
>
<CircleCheck className="size-5" />
<CircleCheck />
Conferma in lavorazione
</Button>
</CredenzaTrigger>

View file

@ -1,286 +0,0 @@
import { FileBadge, FileText, PackageCheck, Pointer } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import { Button, type ButtonConfigs } from "~/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/ui/tooltip";
import { cn } from "~/lib/utils";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import { api } from "~/utils/api";
import { ConfermaAnnuncioModal, ConfermaInLavorazioneModal } from "./conferma";
import { ContattiProprietarioModal } from "./modale_contatti";
const SezioneConferma = () => {
const { servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
const ContattiSbloccati = data.open_contatti_at !== null;
const ConfermaUtente = data.user_confirmed_at !== null;
const ConfermaLavorata = data.doc_conferma_added && data.hasConfermaAdmin;
const AccettatoConferma = data.accettato_conferma_at !== null;
if (ContattiSbloccati && !ConfermaUtente) {
return <ConfermaAnnuncioModal />;
}
if (ConfermaUtente && !data.hasConfermaAdmin) {
return <ConfermaInLavorazioneModal />;
}
if (AccettatoConferma) {
return (
<Link
aria-label="Conferma immobile"
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
>
<Button className="w-full sm:w-fit" variant="success">
<Pointer className="size-5" />
Rivedi la conferma
</Button>
</Link>
);
}
if (ConfermaLavorata) {
if (data.doc_conferma_ref) {
return (
<Link
aria-label="Conferma immobile"
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
>
<Button className="w-full sm:w-fit" variant="success">
<Pointer className="size-5" />
Confermato - Procedi ora
</Button>
</Link>
);
}
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="w-full cursor-not-allowed opacity-50 sm:w-fit"
variant="success"
>
<Pointer className="size-5" />
Confermato - Procedi ora
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>Se hai bisogno di una copia, contattaci via email o chat.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return null;
};
const PostConfermaSection = () => {
const { servizio, servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
const saldoPaid = servizio.isOkSaldo;
const consulenzaPaid = servizio.isOkConsulenza;
if (!saldoPaid) {
return (
<div className="flex flex-col gap-1">
<h3 className="font-semibold">
Questo annuncio è stato confermato, procedi al saldo:
</h3>
<SaldoButton annuncioId={annuncioId} />
</div>
);
}
if (!data.doc_contratto_added) {
return (
<div className="flex flex-col gap-1">
<h3 className="font-semibold text-green-500">
Ci siamo quasi! Stiamo preparando il contratto per te.
</h3>
<h3 className="font-semibold">
Una volta pronto, riceverai una notifica via email.
</h3>
</div>
);
}
return (
<div className="flex flex-col gap-2">
<h3 className="font-semibold">
{consulenzaPaid
? "Il contratto è pronto, puoi visualizzarlo:"
: "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"}
</h3>
<div className="flex flex-wrap gap-2">
{data.doc_contratto_ref ? (
<Link
aria-label="Visualizza contratto"
href={`/servizio/contratto/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="w-full sm:w-fit">
<FileText className="size-5" />
Visualizza il contratto
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button className="w-fit cursor-not-allowed opacity-50">
<FileText className="size-5" />
Visualizza il contratto
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>Se hai bisogno di una copia, contattaci via email o chat.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{!consulenzaPaid && data.contratto_tipo && <SaldoConsulenzaButton />}
{data.doc_registrazione_added &&
(data.doc_registrazione_ref ? (
<Link
aria-label="Visualizza registrazione"
href={`/servizio/contratto-registrazione/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="w-fit">
<FileBadge className="size-5" />
Visualizza la registrazione
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button className="w-fit cursor-not-allowed opacity-50">
<FileBadge className="size-5" />
Visualizza la registrazione
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>
Se hai bisogno di una copia, contattaci via email o chat.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
</div>
);
};
export const Interactions = () => {
const { data } = useServizioAnnuncio();
return (
<>
<div className="flex w-full flex-col flex-wrap gap-2 sm:flex-row">
<ContattiProprietarioModal />
<SezioneConferma />
</div>
{data.accettato_conferma_at != null && <PostConfermaSection />}
</>
);
};
export const SaldoButton = ({
annuncioId,
variant = "info",
className,
}: {
annuncioId: AnnunciId;
variant?: ButtonConfigs["variant"];
className?: string;
}) => {
const { servizioId } = useServizio();
const router = useRouter();
const { mutate } = api.servizio.setupSaldoServizio.useMutation({
onMutate: () => {
toast.loading("Caricamento ...", { id: "preparazioneSaldo" });
},
onSuccess: async (data) => {
toast.success("Pagamento saldo preparato", {
id: "preparazioneSaldo",
});
await router.push(`/servizio/pagamento/${data}`);
},
});
return (
<Button
aria-label="Procedi al saldo"
className={cn("w-fit", className)}
onClick={() => {
mutate({
annuncioId,
servizioId,
});
}}
variant={variant}
>
<PackageCheck className="size-5" />
Procedi al saldo
</Button>
);
};
const SaldoConsulenzaButton = ({
variant = "info",
className,
}: {
variant?: ButtonConfigs["variant"];
className?: string;
}) => {
const { annuncioId } = useServizioAnnuncio();
const { servizioId } = useServizio();
const router = useRouter();
const { mutate } = api.servizio.setupSaldoConsulenzaServizio.useMutation({
onMutate: () => {
toast.loading("Caricamento ...", { id: "preparazioneSaldo" });
},
onSuccess: async (data) => {
toast.success("Pagamento saldo preparato", {
id: "preparazioneSaldo",
});
await router.push(`/servizio/pagamento/${data}`);
},
});
return (
<Button
aria-label="Procedi al saldo consulenza"
className={cn("w-fit", className)}
onClick={() => {
mutate({
annuncioId,
servizioId,
});
}}
variant={variant}
>
<PackageCheck className="size-5" />
Procedi al saldo della consulenza
</Button>
);
};

View file

@ -4,11 +4,11 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import toast from "react-hot-toast";
import { SaldoButton } from "~/components/servizio/annuncio_actions";
import {
AnnuncioCard,
AnnuncioDisplay,
} from "~/components/servizio/annuncio_card";
import { SaldoButton } from "~/components/servizio/interactions";
import { ServizioActions } from "~/components/servizio/servizio_actions";
import { Button } from "~/components/ui/button";
import {
@ -65,7 +65,30 @@ const ServizioCard = ({
export const ServizioContent = () => {
const { servizio, isAdmin, userId, servizioId } = useServizio();
const utils = api.useUtils();
const { mutate: updateServizio } = 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.getPacksSolution.invalidate({ servizioId });
},
});
const { mutate } = api.servizio.removeAnnuncioServizio.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante la rimozione dell'annuncio");
},
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
toast.success("Annuncio rimosso");
},
});
const router = useRouter();
useEffect(() => {
const id = router.asPath.split("#")[1];
@ -115,30 +138,6 @@ export const ServizioContent = () => {
}
if (!servizio.isOkAcconto || servizio.decorrenza === null) {
const utils = api.useUtils();
const { mutate: updateServizio } = 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.getPacksSolution.invalidate({ servizioId });
},
});
const { mutate } = api.servizio.removeAnnuncioServizio.useMutation({
onError: (error) => {
console.error(error);
toast.error("Errore durante la rimozione dell'annuncio");
},
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
toast.success("Annuncio rimosso");
},
});
return (
<ServizioCard
content={

View file

@ -2,6 +2,7 @@ import { CircleUser, Navigation, Phone, TriangleAlert } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
@ -20,7 +21,7 @@ import {
} from "../custom_ui/credenza";
import { LoadingPage } from "../loading";
import { WhatsAppIcon2 } from "../svgs";
import { Button } from "../ui/button";
import { Button, buttonVariants } from "../ui/button";
export const ContattiProprietarioModal = () => {
const { servizio } = useServizio();
@ -47,7 +48,7 @@ export const ContattiProprietarioModal = () => {
className="w-full md:w-fit"
variant="success"
>
<CircleUser className="size-5" />
<CircleUser />
Contatti
</Button>
) : (
@ -56,7 +57,7 @@ export const ContattiProprietarioModal = () => {
className="w-full md:w-fit"
variant="warning"
>
<CircleUser className="size-5" />
<CircleUser />
Ottieni i contatti
</Button>
)}
@ -340,7 +341,7 @@ const PostUnlock = () => {
</p>
{navigationUrl && (
<a
className="inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 hover:bg-accent"
className={cn(buttonVariants({ variant: "info" }))}
href={navigationUrl}
rel="noreferrer"
target="_blank"

View file

@ -182,19 +182,20 @@ const ServizioBasicActions = ({ className }: { className?: string }) => {
return null;
};
export const ServizioPacksInfos = ({
title = "Prezzi:",
}: {
title?: string;
}) => {
export const ServizioPacksInfos = () => {
const { servizio } = useServizio();
const { data: packs } = api.servizio.getPacksSolution.useQuery({
servizioId: servizio.servizio_id,
});
const title = "Prezzi:";
const sub = "(basati sui parametri di ricerca)";
if (!packs) return null;
return (
<div className="w-full space-y-2">
<h3 className="font-semibold text-lg">{title}</h3>
<div className="flex flex-wrap items-center gap-2">
<h3 className="font-semibold text-lg">{title}</h3>
<span className="text-muted-foreground text-sm">{sub}</span>
</div>
<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 ? (

View file

@ -31,38 +31,32 @@ export const AnnunciRichiesti = ({ userId }: { userId: UsersId }) => {
<Accordion collapsible type="single">
<AccordionItem className="space-y-4" value="compatibili">
<AccordionTrigger className="cursor-pointer rounded-md border-2 border-pink-500 bg-muted p-2">
Annunci Richiesti:
Annunci Richiesti dall'Utente ({data.length}):
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col flex-wrap gap-5">
{!data || data.length === 0 ? (
<div className="flex items-center justify-center gap-1 rounded-md bg-muted p-4 sm:flex">
<span>
Nessun annuncio compatibile trovato. Amplia la ricerca per
trovare annunci compatibili.
</span>
</div>
) : (
data.map((a) => {
return (
<BasicAnnuncioCard
className="w-full border-2 border-pink-500"
data={a}
interactions={
<Button
onClick={() => {
deleteAnnuncio({ annuncioId: a.id, userId });
}}
variant="destructive"
>
Rimuovi
<Trash2 className="size-4" />
</Button>
}
key={a.id}
/>
);
})
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{data.map((a) => {
return (
<BasicAnnuncioCard
className="w-full border-2 border-pink-500"
data={a}
interactions={
<Button
onClick={() => {
deleteAnnuncio({ annuncioId: a.id, userId });
}}
variant="destructive"
>
Rimuovi
<Trash2 className="size-4" />
</Button>
}
key={a.id}
/>
);
})}
{data.length % 2 === 1 && (
<div className="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>
</AccordionContent>

View file

@ -5,7 +5,7 @@ import type * as React from "react";
import { cn } from "~/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-hidden focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-5 shrink-0 [&_svg]:shrink-0 outline-hidden focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
defaultVariants: {
size: "default",

View file

@ -187,7 +187,7 @@ export const FormEditServizio = ({
type="button"
variant="destructive"
>
<X className="size-5" />
<X />
Reset Decorrenza
</Button>
</div>

View file

@ -488,7 +488,7 @@ export const FormNewServizio = ({
<FormControl>
<Button
className={cn(
"h-[42px] w-full pl-3 text-left font-medium",
"h-10.5 w-full pl-3 text-left font-medium",
!field.value && "text-muted-foreground",
"rounded-lg border border-neutral-300 shadow-xs outline-hidden file:border-0 file:bg-transparent file:font-medium file:text-sm disabled:cursor-not-allowed disabled:opacity-50 dark:focus:border-transparent",
)}

View file

@ -59,10 +59,13 @@ import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm";
import { useCatasto } from "~/providers/CatastoProvider";
import { useTranslation } from "~/providers/I18nProvider";
import type { Servizio } from "~/schemas/public/Servizio";
import type { Servizio, ServizioUpdate } from "~/schemas/public/Servizio";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { Users, UsersId } from "~/schemas/public/Users";
import type { UsersAnagrafica } from "~/schemas/public/UsersAnagrafica";
import type {
UsersAnagrafica,
UsersAnagraficaUpdate,
} from "~/schemas/public/UsersAnagrafica";
import { api } from "~/utils/api";
type InitialData = {
@ -400,56 +403,63 @@ export const FormNewServizioAcquisto = ({
});
function onSubmit(fields: FormNewServizioValues) {
const a: UsersAnagraficaUpdate = {
azienda: fields.azienda,
cap_residenza: fields.cap_recapiti,
civico_residenza: fields.civico_recapiti,
codice_destinatario: fields.codice_destinatario,
codice_fiscale: fields.codice_fiscale,
comune_residenza: fields.citta_recapiti,
data_nascita: fields.data_nascita,
fatturazione_aziendale: fields.fatturazione_aziendale,
idanagrafica: initialData.anagrafica?.idanagrafica,
legale_rappresentante: fields.legale_rappresentante,
luogo_nascita: fields.luogo_nascita,
nazione_nascita: fields.nazione_nascita,
nazione_residenza: fields.nazione_recapiti,
p_iva: fields.p_iva,
provincia_residenza: fields.provincia_recapiti,
ragione_sociale: fields.ragione_sociale,
sede_legale: fields.sede_legale,
sesso: fields.sesso,
userid: userId,
via_residenza: fields.indirizzo_recapiti,
};
const s: ServizioUpdate = {
animali: fields.animali,
arredato: fields.arredato,
ascensore: fields.ascensore,
budget: fields.budget,
entromese: fields.entromese,
fumatori: fields.fumatori,
giardino: fields.giardino,
motivazione_transitorio: fields.motivazione_transitorio,
n_adulti: fields.n_adulti,
n_minori: fields.n_minori,
parcheggio: fields.parcheggio,
permanenza: fields.permanenza,
pianoterra: fields.pianoterra,
reddito: fields.reddito,
terrazzo: fields.terrazzo,
tipologia: fields.tipologia,
skipPayment: fields.skipPayment,
scadenza_motivazione_transitoria: fields.scadenza_motivazione_transitoria,
};
const u: Pick<
Users,
"nome" | "cognome" | "email" | "telefono" | "username"
> = {
cognome: fields.cognome,
email: fields.email,
nome: fields.nome,
telefono: fields.telefono.replace(/\s/g, ""),
username: `${fields.nome.trim()} ${fields.cognome.trim()}`,
};
mutate({
anagrafica: {
azienda: fields.azienda,
cap_residenza: fields.cap_recapiti,
civico_residenza: fields.civico_recapiti,
codice_destinatario: fields.codice_destinatario,
codice_fiscale: fields.codice_fiscale,
comune_residenza: fields.citta_recapiti,
data_nascita: fields.data_nascita,
fatturazione_aziendale: fields.fatturazione_aziendale,
idanagrafica: initialData.anagrafica?.idanagrafica,
legale_rappresentante: fields.legale_rappresentante,
luogo_nascita: fields.luogo_nascita,
nazione_nascita: fields.nazione_nascita,
nazione_residenza: fields.nazione_recapiti,
p_iva: fields.p_iva,
provincia_residenza: fields.provincia_recapiti,
ragione_sociale: fields.ragione_sociale,
sede_legale: fields.sede_legale,
sesso: fields.sesso,
userid: userId,
via_residenza: fields.indirizzo_recapiti,
},
servizio: {
animali: fields.animali,
arredato: fields.arredato,
ascensore: fields.ascensore,
budget: fields.budget,
entromese: fields.entromese,
fumatori: fields.fumatori,
giardino: fields.giardino,
motivazione_transitorio: fields.motivazione_transitorio,
n_adulti: fields.n_adulti,
n_minori: fields.n_minori,
parcheggio: fields.parcheggio,
permanenza: fields.permanenza,
pianoterra: fields.pianoterra,
reddito: fields.reddito,
terrazzo: fields.terrazzo,
tipologia: fields.tipologia,
skipPayment: fields.skipPayment,
},
anagrafica: a,
servizio: s,
servizioId: initialData.servizio.servizio_id,
user: {
cognome: fields.cognome,
email: fields.email,
nome: fields.nome,
telefono: fields.telefono.replace(/\s/g, ""),
username: `${fields.nome.trim()} ${fields.cognome.trim()}`,
},
user: u,
userId,
});
}

View file

@ -182,7 +182,7 @@ const Filters = () => {
onClick={ResetParams}
variant="destructive"
>
<FilterX className="size-5" /> {t.annunci.filtri_reset}
<FilterX /> {t.annunci.filtri_reset}
</Button>
</>
);
@ -213,7 +213,7 @@ const DrowdownFilters = () => {
<Badge className="w-fit rounded-md bg-secondary px-1.5 py-0.5 text-secondary-foreground dark:group-hover:bg-transparent">
{nFiltri}
</Badge>
<Filter className="size-5" />{" "}
<Filter />{" "}
{nFiltri === 1 ? t.annunci.filtro_attivo : t.annunci.filtri_attivi}
<ChevronDown />
</Button>
@ -246,12 +246,11 @@ const RicercaHeader = () => {
>
{!map ? (
<>
<MapIcon className="size-5" />{" "}
<span className="hidden sm:block">Mappa</span>
<MapIcon /> <span className="hidden sm:block">Mappa</span>
</>
) : (
<>
<Table className="size-5" />
<Table />
<span className="hidden sm:block">Lista</span>
</>
)}

View file

@ -611,7 +611,7 @@ const ShareComponent = () => {
<Dialog>
<DialogTrigger asChild>
<Button>
<Share2 className="size-5" />
<Share2 />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">

View file

@ -6,7 +6,6 @@ import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page";
import { env } from "~/env";
import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider";
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zOrdineId } from "~/server/utils/zod_types";
@ -22,7 +21,7 @@ const ServicePurchasePage: NextPageWithLayout<ServizioPurchaseProps> = ({
const { data, isLoading } = api.servizio.getServizioPaymentData.useQuery({
ordineId,
});
const { t } = useTranslation();
const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY);
if (isLoading) {

View file

@ -69,7 +69,7 @@ const RiapriConferma: NextPageWithLayout<RiapriConfermaProps> = ({
onClick={() => router.back()}
variant="secondary"
>
<ArrowLeft className="size-5" />
<ArrowLeft />
{t.indietro}
</Button>
</div>

View file

@ -243,11 +243,12 @@ export const postAcquistoDataHandler = async ({
}: {
servizioId: ServizioServizioId;
userId: UsersId;
user: Pick<Users, "nome" | "cognome" | "email" | "telefono">;
user: Pick<Users, "nome" | "cognome" | "email" | "telefono" | "username">;
anagrafica: UsersAnagraficaUpdate;
servizio: ServizioUpdate;
}): Promise<{ ordineIdForPayment: OrdiniOrdineId | null }> => {
try {
console.log("servizio", servizio);
const servizioData = await db.transaction().execute(async (tx) => {
// Update user and anagrafica data
await tx
@ -810,7 +811,7 @@ export const getAllServizioAnnunci = async (userId: UsersId) => {
ob.asc().nullsLast(),
)
.orderBy("servizio_annunci.open_contatti_at", (ob) =>
ob.asc().nullsFirst(),
ob.asc().nullsLast(),
)
.orderBy("servizio_annunci.created_at", "asc")
.whereRef(
@ -1321,12 +1322,12 @@ export const userSendConfermaIntent = async ({
mailType: "generic",
props: {
noreply: true,
testo: `La tua conferma per l'immobile con codice ${annuncio_codice.codice} è stata ricevuta. Il nostro team la esaminerà a breve. Riceverai una notifica via email per procedere.`,
title: `Conferma immobile ${annuncio_codice.codice} ricevuta`,
testo: `La tua conferma per l'immobile con codice ${annuncio_codice.codice} è stata inviata con successo. Il nostro team la esaminerà a breve. Riceverai una notifica via email per procedere.`,
title: `Conferma immobile ${annuncio_codice.codice} inviata con successo`,
},
},
mail: {
subject: `Conferma immobile ${annuncio_codice.codice} ricevuta`,
subject: `Conferma immobile ${annuncio_codice.codice} inviata con successo`,
to: utente.email,
},
userId: utente.id,