infoalloggi-monorepo/apps/infoalloggi/src/components/schedaAnnuncioStampabile.tsx

425 lines
12 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import z from "zod";
import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card";
import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils";
import { it } from "~/i18n/it";
import { handleConsegna } from "~/lib/annuncio_details";
import { replaceWithBr } from "~/lib/newlineToBr";
import { cn, formatCurrency } from "~/lib/utils";
import type { Annunci } from "~/schemas/public/Annunci";
import { api } from "~/utils/api";
import { IconMatrix } from "./IconComponents";
import { GoogleMapsIcon, LogoSvg, WhatsAppIcon } from "./svgs";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
const blobToBase64 = (blob: Blob): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
if (typeof reader.result === "string") {
// Remove the Data-URL prefix (e.g. "data:application/pdf;base64,") to get just the base64
resolve(reader.result.split(",")[1] || "");
} else {
reject(new Error("Failed to convert blob to text"));
}
};
reader.readAsDataURL(blob);
});
};
export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
const [inputEmail, setInputEmail] = useState<string>("");
const [openPopover, setOpenPopover] = useState<boolean>(false);
const { mutate } = api.comunicazioni.sendSchedaAnnuncioEmail.useMutation({
onSuccess: () => {
toast.success("Email inviata con successo!", {
id: "send-scheda-annuncio-email",
});
},
onError: (error) => {
toast.error(`Errore durante l'invio dell'email: ${error.message}`, {
id: "send-scheda-annuncio-email",
});
},
});
const sendPdf = async () => {
if (!data || !inputEmail || !data.locatore || !data.numero) {
toast.error("Dati annuncio mancanti.");
return;
}
try {
toast.loading("Generazione PDF in corso...", {
id: "generate-pdf",
});
const response = await fetch(`/api/generate-pdf?id=${data.id}`);
if (!response.ok) {
throw new Error(
`Errore nella generazione del PDF: ${response.statusText}`,
);
}
const blob = await response.blob();
const pdfBase64 = await blobToBase64(blob);
mutate({
pdf: pdfBase64,
to: inputEmail,
codice: data.codice,
numero: data.numero,
nominativo: data.locatore,
indirizzo: `${data.indirizzo}, ${data.civico} ${data.comune} ${data.cap} (${data.provincia})`,
});
setInputEmail("");
toast.success("PDF generato con successo!", {
id: "generate-pdf",
});
} catch (error) {
toast.error(
`Errore durante la generazione del PDF: ${(error as Error).message}`,
{
id: "generate-pdf",
},
);
console.error("Error generating PDF:", error);
}
};
const generatePdf = async () => {
if (!data) {
toast.error("Dati annuncio mancanti.");
return;
}
try {
toast.loading("Generazione PDF in corso...", {
id: "generate-pdf",
});
const response = await fetch(`/api/generate-pdf?id=${data.id}`);
if (!response.ok) {
throw new Error(
`Errore nella generazione del PDF: ${response.statusText}`,
);
}
const blob = await response.blob();
toast.success("PDF generato con successo!", {
id: "generate-pdf",
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `scheda-annuncio-${data.codice}.pdf`;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
toast.error(
`Errore durante la generazione del PDF: ${(error as Error).message}`,
{
id: "generate-pdf",
},
);
console.error("Error generating PDF:", error);
}
};
return (
<div className="mx-auto max-w-4xl">
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<h1 className="font-bold text-2xl">Scheda Annuncio Stampabile</h1>
<div className="flex gap-2">
<Button onClick={() => generatePdf()}>Scarica Scheda</Button>
<Popover onOpenChange={setOpenPopover} open={openPopover}>
<PopoverTrigger asChild>
<Button className="print:hidden" variant="outline">
Invia tramite email
</Button>
</PopoverTrigger>
<PopoverContent className="flex w-80 flex-col gap-4 print:hidden">
<Label htmlFor="indirizzo">Indirizzo Email</Label>
<Input
id="indirizzo"
onChange={(e) => setInputEmail(e.target.value)}
onKeyDown={(e) => {
if (
e.key === "Enter" &&
z.safeParse(z.email(), inputEmail).success
) {
sendPdf();
}
}}
type="email"
/>
<Button
disabled={
z.safeParse(z.email(), inputEmail).success === false
}
onClick={sendPdf}
>
Invia
</Button>
</PopoverContent>
</Popover>
</div>
</div>
<SchedaAnnuncio data={data} />
</div>
</div>
);
}
export function SchedaAnnuncio({
data,
raw,
}: {
data: Annunci;
raw?: boolean;
}) {
const companyAddress =
"Via Beata Giovanna, 1 a Bassano del Grappa (VI)\nTel. +39 0424529869\nEmail: arca@infoalloggi.it";
const footer =
"Arcenia Srl. | Tel. +39 0424529869 | Email: arca@infoalloggi.it";
const [navigationUrl, setNavigationUrl] = useState<string | undefined>(
undefined,
);
const [printDate, setPrintDate] = useState<string>("");
const [disponibileDate, setDisponibileDate] = useState<string>("");
useEffect(() => {
setPrintDate(new Date().toLocaleDateString("it-IT"));
if (data.disponibile_da) {
setDisponibileDate(new Date(data.disponibile_da).toLocaleDateString());
} else if (data.consegna) {
setDisponibileDate(
handleConsegna({
aggiornamento: it.card.in_aggiornamento,
consegna: data.consegna,
consegna_da: it.card.consegna_da,
mesi: it.parametri.mesi,
subito: it.card.consegna_subito,
}),
);
} else {
setDisponibileDate("Non specificata");
}
if (!data) return;
if (typeof window === "undefined") return;
if (
navigator.userAgent.toUpperCase().includes("MAC") ||
navigator.userAgent.toUpperCase().includes("IPAD") ||
navigator.userAgent.toUpperCase().includes("IPHONE") ||
navigator.userAgent.toUpperCase().includes("IOS") ||
navigator.userAgent.toUpperCase().includes("IPOD")
) {
setNavigationUrl(
`maps://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${data.lat},${data.lon}`,
);
} else {
setNavigationUrl(
`https://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${data.lat},${data.lon}`,
);
}
}, [data]);
return (
<Card
className={cn(
"p-4 print:border-none print:shadow-none",
raw && "border-none shadow-none",
)}
>
<div className="space-y-8">
<div className="flex items-start justify-between">
<div>
<LogoSvg className="h-10 w-auto" />
</div>
<div className="relative">
<p className="text-right font-semibold text-lg">Arcenia Srl</p>
<p className="whitespace-pre-line text-right text-sm">
{companyAddress}
</p>
</div>
</div>
{/* Property Header */}
<div className="space-y-2 text-center">
<h2 className="font-bold text-primary text-xl">
{data.titolo_it || "Immobile in Affitto"}
</h2>
<p className="text-lg text-muted-foreground">
Riferimento: <strong>{data.codice}</strong>
</p>
</div>
<div className="rounded-lg bg-primary/5 p-4 outline-2 outline-green-500">
<h3 className="mb-2 inline-flex items-center gap-2 font-semibold text-lg text-primary">
<WhatsAppIcon className="size-6 fill-green-500 stroke-green-500" />{" "}
Contatti
<span className="text-base">
(mandare un messaggio WhatsApp prima di chiamare)
</span>
</h3>
<div className="space-y-1">
<p>{data.locatore}</p>
{data.numero && <p className="font-semibold">Tel. {data.numero}</p>}
</div>
</div>
{/* Property Details Grid */}
<div className="grid grid-cols-1 gap-6 pb-1">
{/* Location Details */}
<div className="space-y-3">
<h3 className="border-b pb-1 font-semibold text-lg">Indirizzo</h3>
<div className="flex items-center gap-8">
<p className="font-semibold">
{data.indirizzo}, {data.civico} {data.comune} {data.cap} (
{data.provincia})
</p>
{navigationUrl && (
<a
className="flex items-center gap-2 text-blue-600 underline underline-offset-2"
href={navigationUrl}
id="navigationLink"
rel="noreferrer"
target="_blank"
>
<span>Apri in Google Maps</span>
<GoogleMapsIcon className="size-6" />
</a>
)}
</div>
</div>
{/* Property Characteristics */}
{/* <div className="space-y-3">
<h3 className="border-b pb-1 font-semibold text-lg">
🏠 Caratteristiche
</h3>
<div className="space-y-2 text-sm">
{data.mq && (
<p>
<span className="font-medium">mq:</span> {data.mq} mq
</p>
)}
{data.numero_vani && (
<p>
<span className="font-medium">Locali:</span>{" "}
{data.numero_vani}
</p>
)}
{data.numero_camere && (
<p>
<span className="font-medium">Camere:</span>{" "}
{data.numero_camere}
</p>
)}
{data.numero_bagni && (
<p>
<span className="font-medium">Bagni:</span>{" "}
{data.numero_bagni}
</p>
)}
</div>
</div> */}
</div>
{/* Pricing Information */}
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4 text-sm">
{data.tipo && (
<div className="rounded bg-gray-50 p-3">
<p className="font-medium text-primary">Tipologia</p>
<p className="font-bold text-xl">Affitto {data.tipo}</p>
</div>
)}
{data.prezzo && (
<div className="rounded bg-gray-50 p-3">
<p className="font-medium text-primary">Canone Mensile</p>
<p className="font-bold text-xl">
{formatCurrency(data.prezzo / 100)}
</p>
</div>
)}
{(data.disponibile_da || data.consegna) && (
<div className="rounded bg-gray-50 p-3">
<p className="font-medium text-primary">Disponibile da</p>
<p className="font-bold text-xl">{disponibileDate}</p>
</div>
)}
</div>
</div>
{/* Additional Features */}
<div className="space-y-3">
<h3 className="border-b pb-1 font-semibold text-lg">
Dotazioni e Servizi
</h3>
<div className="grid grid-cols-4 gap-2 text-sm">
{data.caratteristiche &&
filteredCaratteristiche(data.caratteristiche).map((item) => {
if (!item.value) return null;
return (
<div
className="flex items-center justify-start gap-2"
key={item.text}
>
<span>
<IconMatrix type={item.icon} />
</span>
<span>
{item.text}: {item.value}
</span>
</div>
);
})}
</div>
</div>
{/* Description */}
{data.desc_it && (
<>
<div className="space-y-3">
<h3 className="border-b pb-1 font-semibold text-lg">
Descrizione
</h3>
<div className="text-justify text-sm leading-relaxed">
<p
dangerouslySetInnerHTML={{
__html: replaceWithBr(data.desc_it),
}}
></p>
</div>
</div>
</>
)}
<div className="mx-auto max-w-2xl pt-2 text-center text-muted-foreground text-xs">
<p>
Informazione strettamente riservata ed indirizzata all'Utente
Arca-Infoalloggi nel rispetto dell' Art. F. Precisazione dei Tuoi
Obblighi e responsabilità delle Condizioni Generali di Contratto
(CGC) accettate
</p>
</div>
<div className="pt-2 text-center text-muted-foreground text-xs">
<p>{footer}</p>
<p className="mt-1">Documento generato in data: {printDate}</p>
</div>
</div>
</Card>
);
}