feat: refactor and enhance Annuncio components for improved functionality and print support

This commit is contained in:
Marco Pedone 2025-08-29 18:18:37 +02:00
parent 4d95791391
commit 3284ccbf0d
10 changed files with 406 additions and 62 deletions

View file

@ -0,0 +1,262 @@
"use client";
import { useRef } from "react";
import { useReactToPrint } from "react-to-print";
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 type { Annunci } from "~/schemas/public/Annunci";
import { IconMatrix } from "./IconComponents";
import { LogoSvg } from "./svgs";
import { Separator } from "./ui/separator";
//todo finire
export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
const contentRef = useRef<HTMLDivElement>(null);
const reactToPrintFn = useReactToPrint({
contentRef,
//print: async (target) => {console.log("Printing...", target.contentDocument);},
});
return (
<div className="mx-auto max-w-4xl">
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Scheda Annuncio Stampabile</h1>
<Button className="print:hidden" onClick={() => reactToPrintFn()}>
Stampa
</Button>
</div>
<div ref={contentRef}>
<SchedaAnnuncio data={data} />
</div>
</div>
</div>
);
}
function SchedaAnnuncio({ data }: { data: Annunci }) {
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";
return (
<Card className="p-4 print:border-none print:shadow-none">
<div className="space-y-6">
<div className="flex items-start justify-between">
<div>
<LogoSvg className="[&>text]:fill-primary [&>text]:stroke-primary h-10 w-auto [&>g]:fill-red-500" />
</div>
<div className="relative">
<p className="font-semibold text-lg text-right">Arcenia Srl</p>
<p className="text-sm whitespace-pre-line text-right">
{companyAddress}
</p>
</div>
</div>
{/* Property Header */}
<div className="space-y-2 text-center">
<h2 className="text-xl font-bold text-primary">
{data.titolo_it || "Immobile in Affitto"}
</h2>
<p className="text-lg text-muted-foreground">
Riferimento: <strong>{data.codice}</strong>
</p>
</div>
{/* Property Details Grid */}
<div className="grid grid-cols-2 gap-6">
{/* Location Details */}
<div className="space-y-3">
<h3 className="text-lg font-semibold border-b pb-1">
📍 Ubicazione
</h3>
<div className="space-y-2 text-sm">
<p>
<span className="font-medium">Comune:</span> {data.comune} (
{data.provincia})
</p>
<p>
<span className="font-medium">CAP:</span> {data.cap}
</p>
<p>
<span className="font-medium">Indirizzo:</span> {data.indirizzo}
, {data.civico}
</p>
{data.piano_palazzo && (
<p>
<span className="font-medium">Piano:</span>{" "}
{data.piano_palazzo}
</p>
)}
</div>
</div>
{/* Property Characteristics */}
<div className="space-y-3">
<h3 className="text-lg font-semibold border-b pb-1">
🏠 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="bg-gray-50 p-3 rounded">
<p className="font-medium text-primary">Tipologia</p>
<p className="text-xl font-bold">Affitto {data.tipo}</p>
</div>
)}
{data.prezzo && (
<div className="bg-gray-50 p-3 rounded">
<p className="font-medium text-primary">Canone Mensile</p>
<p className="text-xl font-bold">
{data.prezzo.toLocaleString()}
</p>
</div>
)}
{(data.disponibile_da || data.consegna) && (
<div className="bg-gray-50 p-3 rounded">
<p className="font-medium text-primary">Disponibile da</p>
<p className="text-xl font-bold">
{data.disponibile_da
? new Date(data.disponibile_da).toLocaleDateString()
: data.consegna &&
handleConsegna({
aggiornamento: it.card.in_aggiornamento,
consegna: data.consegna,
consegna_da: it.card.consegna_da,
mesi: it.preferenze.mesi,
subito: it.card.consegna_subito,
})}
</p>
</div>
)}
</div>
</div>
{/* Additional Features */}
<div className="space-y-3">
<h3 className="text-lg font-semibold border-b pb-1">
Dotazioni e Servizi
</h3>
<div className="grid grid-cols-4 gap-2 text-sm">
{data.caratteristiche &&
filteredCaratteristiche(data.caratteristiche).map(
(item, index) => {
if (!item.value) return null;
return (
<div
className="flex items-center gap-2 justify-center"
key={index}
>
<span className="">
<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="text-lg font-semibold border-b pb-1">
📝 Descrizione
</h3>
<div className="text-sm leading-relaxed text-justify">
<p
dangerouslySetInnerHTML={{
__html: replaceWithBr(data.desc_it),
}}
></p>
</div>
</div>
</>
)}
{/* Energy Information */}
{data.classe && (
<>
<Separator />
<div className="space-y-3">
<h3 className="text-lg font-semibold border-b pb-1">
Prestazione Energetica
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
{data.classe && (
<p>
<span className="font-medium">Classe Energetica:</span>{" "}
{data.classe}
</p>
)}
</div>
</div>
</>
)}
{/* Contact Information */}
<Separator />
<div className="bg-primary/5 p-4 rounded-lg">
<h3 className="text-lg font-semibold text-primary mb-2">
📞 Contatti
</h3>
<div className="text-sm space-y-1">
<p>{data.locatore}</p>
{data.numero && <p>Tel. {data.numero}</p>}
</div>
</div>
<Separator />
<div className="text-muted-foreground pt-2 text-center text-xs">
<p>{footer}</p>
<p className="mt-1">
Documento generato in data: {new Date().toLocaleDateString("it-IT")}
</p>
</div>
</div>
</Card>
);
}

View file

@ -24,6 +24,7 @@ import {
CarouselPrevious,
} from "~/components/ui/carousel";
import { handleConsegna } from "~/lib/annuncio_details";
import { replaceWithBr } from "~/lib/newlineToBr";
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizioAnnuncio } from "~/providers/ServizioProvider";
@ -166,9 +167,7 @@ const AnnuncioDettaglio = ({
>;
}) => {
const { locale, t } = useTranslation();
function replaceWithBr(text: string) {
return text.replace(/\n/g, "<br />");
}
return (
<Credenza>
<CredenzaTrigger asChild>
@ -261,7 +260,7 @@ const AnnuncioDettaglio = ({
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceWithBr(
(locale === "it" ? data.desc_it : data.desc_it) || "",
(locale === "it" ? data.desc_it : data.desc_en) || "",
),
}}
/>

View file

@ -1,6 +1,6 @@
import { format } from "date-fns";
import { it } from "date-fns/locale";
import { CalendarIcon, Eye, RotateCw } from "lucide-react";
import { CalendarIcon, Eye, Printer, RotateCw } from "lucide-react";
import Link from "next/link";
import toast from "react-hot-toast";
import z from "zod";
@ -115,7 +115,9 @@ export const AnnuncioEditForm = ({ data }: { data: Annunci }) => {
const { mutate: edit } = api.annunci.editAnnuncio.useMutation({
onSettled: async () => {
await utils.annunci.getAnnuncio.invalidate({ cod: data.codice });
await utils.annunci.getAnnuncioByIdForEdit.invalidate({ id: data.id });
await utils.annunci.getAnnuncioById_rawImgUrls.invalidate({
id: data.id,
});
await utils.annunci.getAnnunciList.invalidate();
toast.success("Annuncio modificato con successo");
},
@ -202,17 +204,30 @@ export const AnnuncioEditForm = ({ data }: { data: Annunci }) => {
<Button
className="flex items-center gap-2"
size="sm"
type="button"
variant="outline"
>
<Eye /> pubblica
</Button>
</Link>
<Link
href={`/area-riservata/admin/scheda-annuncio-stampa/${data.id}`}
target="_blank"
>
<Button
className="flex items-center gap-2"
size="sm"
type="button"
variant="outline"
>
<Printer /> Scheda
</Button>
</Link>
<div className="hidden items-center gap-2 md:ml-auto md:flex">
<Button size="sm" variant="outline">
<Button size="sm" type="button" variant="outline">
Annulla
</Button>
<Button className="bg-green-500" size="sm">
<Button className="bg-green-500" size="sm" type="submit">
Salva
</Button>
</div>

View file

@ -6,77 +6,52 @@ import type { Caratteristiche } from "~/utils/kanel-types";
export type CaratteristicheFiltered = ReturnType<
typeof filteredCaratteristiche
>;
export const filteredCaratteristiche = (caratteristiche: Caratteristiche) => {
const {
Scheda_Grado,
Scheda_OneriUrbanizzazione,
Scheda_AffittoMuriAnno,
Scheda_ScadenzaContratto,
Scheda_CanoniAnticipati,
Scheda_OrariApertura,
Scheda_PossibilitàAcquistoMuri,
Scheda_SpCondominialiAnno,
Scheda_Cauzione,
Scheda_IncassoAnnuo,
Scheda_NumeroVetrine,
Scheda_NumeroDipendenti,
Scheda_PostiSedere,
Scheda_TipoGestione,
Scheda_TipoRegistr,
Scheda_TipoContratto,
Scheda_Destinazione,
Scheda_Pavimento,
Scheda_PavimentoCucina,
Scheda_PavimentoBagno,
Scheda_PavimentoNotte,
Scheda_PavimentoGiorno,
Scheda_LatiLiberi,
Scheda_ElettricitaGas,
Scheda_IndiceEdificabilita,
Scheda_CambioBiancheria,
Scheda_Categoria,
Scheda_Panorama,
Scheda_TipoSaracinesca,
Scheda_Appartamenti,
...rest
} = caratteristiche;
export const filteredCaratteristiche = (c: Caratteristiche) => {
const filtered = [
{
icon: "heater" as IconType,
text: "Tipo Impianto",
value: rest.Scheda_TipoImpianto,
value: c.Scheda_TipoImpianto,
},
{
icon: "thermometer-sun" as IconType,
text: "Riscaldamento",
value: rest.Scheda_Riscaldamento,
value: c.Scheda_Riscaldamento,
},
{
icon: "sofa" as IconType,
text: "Arredi",
value: rest.Scheda_Arredi,
value: c.Scheda_Arredi,
},
{
icon: "building" as IconType,
text: "Piano",
value: rest.Scheda_Piano,
value: c.Scheda_Piano,
},
{
icon: "building2" as IconType,
text: "Piani Edificio",
value: rest.Scheda_TotalePiani,
value: c.Scheda_TotalePiani,
},
{
icon: "dot" as IconType,
text: "Orientamento",
value: rest.Scheda_Orientamento,
value: c.Scheda_Orientamento,
},
{
icon: "wifi" as IconType,
text: "Internet",
value: rest.Scheda_Internet,
value: c.Scheda_Internet,
},
{
icon: "accessibility" as IconType,
text: "Accesso Disabili",
value: c.Scheda_AccessoDisabili,
},
{
icon: "dot" as IconType,
text: "Spazio Parcheggio",
value: c.Scheda_SpazioParcheggio,
},
];
return filtered;

View file

@ -0,0 +1,3 @@
export function replaceWithBr(text: string) {
return text.replace(/\n/g, "<br />");
}

View file

@ -51,6 +51,7 @@ import {
} from "~/hooks/schedaAnnuncioUtils";
import { useMediaQuery } from "~/hooks/use-media-query";
import { handleConsegna } from "~/lib/annuncio_details";
import { replaceWithBr } from "~/lib/newlineToBr";
import { cn } from "~/lib/utils";
import { AnnuncioContext } from "~/providers/AnnuncioProvider";
import { useTranslation } from "~/providers/I18nProvider";
@ -330,9 +331,6 @@ const TestiAnnuncio = ({
}[]
| null;
}) => {
function replaceWithBr(text: string) {
return text.replace(/\n/g, "<br />");
}
return (
<>
<h1 className="text-3xl font-bold uppercase">{titolo}</h1>

View file

@ -18,7 +18,7 @@ type AnnuncioEditProps = {
const AnnuncioEdit: NextPageWithLayout<AnnuncioEditProps> = ({
id,
}: AnnuncioEditProps) => {
const { data, isLoading } = api.annunci.getAnnuncioByIdForEdit.useQuery(
const { data, isLoading } = api.annunci.getAnnuncioById_rawImgUrls.useQuery(
{
id,
},
@ -29,7 +29,7 @@ const AnnuncioEdit: NextPageWithLayout<AnnuncioEditProps> = ({
if (!data) {
return <Status500 />;
}
//todo avere scheda di condivizione scaricabile
return (
<>
<AnnuncioEditForm data={data} />
@ -52,7 +52,7 @@ export const getServerSideProps = (async (context) => {
const ssg = generateSSGHelper();
await ssg.annunci.getAnnuncioByIdForEdit.prefetch({ id: check.data });
await ssg.annunci.getAnnuncioById_rawImgUrls.prefetch({ id: check.data });
return {
props: {

View file

@ -0,0 +1,91 @@
import type { GetServerSideProps } from "next";
import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { SchedaAnnuncioStampabile } from "~/components/schedaAnnuncioStampabile";
import { Status500 } from "~/components/status-page";
import type { NextPageWithLayout } from "~/pages/_app";
import type { AnnunciId } from "~/schemas/public/Annunci";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zAnnuncioId } from "~/server/utils/zod_types";
import { api } from "~/utils/api";
type PageProps = {
id: AnnunciId;
};
const SchedaAnnuncioPage: NextPageWithLayout<PageProps> = ({
id,
}: PageProps) => {
const { data, isLoading } = api.annunci.getAnnuncioById_rawImgUrls.useQuery({
id,
});
if (isLoading) {
return <LoadingPage />;
}
if (!data) {
return <Status500 />;
}
return (
<div className="mx-auto w-full p-4">
<SchedaAnnuncioStampabile data={data} />
</div>
);
};
SchedaAnnuncioPage.getLayout = function getLayout(page) {
return (
<AreaRiservataLayout noFooter noSidebar>
{page}
</AreaRiservataLayout>
);
};
export default SchedaAnnuncioPage;
export const getServerSideProps = (async (context) => {
const id = context.params?.id;
if (typeof id !== "string") {
console.error("Error: id is not a string");
return {
redirect: {
destination: "/500",
permanent: false,
},
};
}
const parsed = zAnnuncioId.safeParse(parseInt(id));
if (!parsed.success) {
console.error("Error parsing ordineId", parsed.error);
return {
redirect: {
destination: "/500",
permanent: false,
},
};
}
const access_token = context.req.cookies.access_token;
const helper = await TrpcAuthedFetchingIstance({ access_token });
if (helper) {
await helper.trpc.annunci.getAnnuncioById_rawImgUrls.prefetch({
id: parsed.data,
});
return {
props: {
id: parsed.data,
trpcState: helper.trpc.dehydrate(),
},
};
}
return {
redirect: {
destination: "/500",
permanent: false,
},
};
}) satisfies GetServerSideProps<PageProps>;

View file

@ -10,7 +10,7 @@ import {
editAnnuncioHandler,
get_AnnunciPositionsHandler,
getAnnunciByCod,
getAnnunciById,
getAnnunciById_rawImgUrls,
getAnnunciListHandler,
getAnnunciMetaByCod,
getCodici_AnnunciHandler,
@ -45,15 +45,16 @@ export const annunciRouter = createTRPCRouter({
.query(async ({ input }) => {
return await getAnnunciByCod({ ...input });
}),
getAnnuncioByIdForEdit: publicProcedure
getAnnuncioById_rawImgUrls: publicProcedure
.input(
z.object({
id: zAnnuncioId,
}),
)
.query(async ({ input }) => {
return await getAnnunciById({ ...input });
return await getAnnunciById_rawImgUrls({ ...input });
}),
getAnnuncioMeta: publicProcedure
.input(
z.object({

View file

@ -116,7 +116,7 @@ export const getAnnunciMetaByCod = async ({ cod }: { cod: string }) => {
}
};
export const getAnnunciById = async ({
export const getAnnunciById_rawImgUrls = async ({
id,
}: {
id: AnnunciId;