infoalloggi-monorepo/apps/infoalloggi/src/components/servizio/modale_contatti.tsx

401 lines
10 KiB
TypeScript
Raw Normal View History

import {
CircleCheck,
CircleUser,
Navigation,
Phone,
TriangleAlert,
} from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import { api } from "~/utils/api";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza";
import { LoadingPage } from "../loading";
import { WhatsAppIcon2 } from "../svgs";
import { Button } from "../ui/button";
import { type UploadCallback, UploadModal } from "../upload_modal";
export const ContattiProprietarioModal = () => {
const { servizio } = useServizio();
const { data } = useServizioAnnuncio();
const { t } = useTranslation();
const isUnlocked = data.open_contatti_at != null;
const needsDocuments =
!servizio.skipControlloDoc &&
(!servizio.doc_personale_fronte || !servizio.doc_personale_retro);
return (
<Credenza>
<CredenzaTrigger asChild>
{data.open_contatti_at != null ? (
<Button
aria-label="Vedi contatti proprietario"
className="w-fit"
variant="success"
>
<CircleUser className="size-7" />
Vedi i contatti
</Button>
) : (
<Button
aria-label="Contatta proprietario"
className="w-fit"
variant="warning"
>
<CircleUser className="size-7" />
Ottieni i contatti
</Button>
)}
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] sm:max-w-2xl">
<CredenzaHeader>
<CredenzaTitle className="text-xl">{t.contatto.title}</CredenzaTitle>
<CredenzaDescription className="sr-only">
{t.contatto.descrizione}
</CredenzaDescription>
</CredenzaHeader>
{needsDocuments ? (
<DocCheckpoint />
) : isUnlocked ? (
<PostUnlock />
) : (
<PreUnlock />
)}
</CredenzaContent>
</Credenza>
);
};
const DocComponent = ({
docInfo,
cb,
}: {
docInfo: {
storageId: string;
ref: UsersStorageUserStorageId | null;
} | null;
cb: UploadCallback;
}) => {
const { isAdmin, userId } = useServizio();
if (docInfo?.storageId) {
return (
<div className="flex items-center gap-2 text-green-500">
<CircleCheck className="size-5" />
<span>Documento caricato</span>
</div>
);
}
return (
<UploadModal
cb_onUpload={cb}
isAdmin={isAdmin}
maxFiles={1}
userId={userId}
/>
);
};
const DocCheckpoint = () => {
const { t } = useTranslation();
const { userId, servizio } = useServizio();
const utils = api.useUtils();
const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({
userId,
});
await utils.storage.retrieveUserFileData.invalidate({ userId });
await utils.servizio.getServizio.invalidate({
servizioId: servizio.servizio_id,
});
},
onError: (err) => {
toast.error(`Errore aggiornamento servizio: ${err.message}`);
},
});
const handleUpload = (
field: "doc_personale_fronte_ref" | "doc_personale_retro_ref",
id: UsersStorageUserStorageId,
) => {
mutate({
userId,
data: {
[field]: id,
},
});
};
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<div className="space-y-5">
<p>Devi ancora caricare un tuo documento</p>
<div className="flex flex-col gap-2">
<span>Fronte documento:</span>
<DocComponent
cb={({ userStorageIds }) =>
userStorageIds.length > 0 &&
userStorageIds[0] &&
handleUpload("doc_personale_fronte_ref", userStorageIds[0])
}
docInfo={servizio.doc_personale_fronte}
/>
</div>
<div className="flex flex-col gap-2">
<span>Retro documento:</span>
<DocComponent
cb={({ userStorageIds }) =>
userStorageIds.length > 0 &&
userStorageIds[0] &&
handleUpload("doc_personale_retro_ref", userStorageIds[0])
}
docInfo={servizio.doc_personale_retro}
/>
</div>
<br />
<br />
<p className="text-muted-foreground text-sm">
I tuoi dati saranno conservati solamente per la durata del servizio,
in conformità con la nostra{" "}
<Link className="underline" href="/privacy-policy" target="_blank">
Informativa sulla Privacy
</Link>
</p>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi" disabled={isPending} variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</>
);
};
const PreUnlock = () => {
const { userId, servizioId } = useServizio();
const { annuncioId } = useServizioAnnuncio();
const { t } = useTranslation();
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.unlockServizioContatti.useMutation(
{
onError: (err) => {
toast.error(err.message, {
id: "unlock-contacts",
});
},
onMutate: () => {
toast.loading("Caricamento ...", {
id: "unlock-contacts",
});
},
onSuccess: async (data) => {
if (!data) {
toast.error("Limite di sblocchi raggiunto", {
id: "unlock-contacts",
});
return;
}
toast.success("Successo", {
id: "unlock-contacts",
});
await utils.servizio.getAllServizioAnnunci.invalidate({
userId,
});
},
},
);
const currentHour = new Date().getHours();
const isOutsideBusinessHours = currentHour < 9 || currentHour >= 20;
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<div className="space-y-5">
<p>{t.contatto.disclaimer}</p>
<p>{t.contatto.disclaimer2}</p>
<p className="text-muted-foreground text-xs">
{t.contatto.disclaimer3}
</p>
{isOutsideBusinessHours && (
<div className="flex items-center gap-2 rounded-md bg-yellow-200 p-2">
<TriangleAlert className="size-8" />
<p>
Il proprietario riceverà la notifica alle ore 9.00{" "}
{currentHour >= 20 && "di domani"}
</p>
</div>
)}
</div>
</CredenzaBody>
<CredenzaFooter>
<Button
aria-label="Contatta proprietario"
disabled={isPending}
onClick={() =>
mutate({
annuncioId,
servizioId,
})
}
variant="success"
>
{t.accetta}
</Button>
<CredenzaClose asChild>
<Button
aria-label="Chiudi modal credenza"
disabled={isPending}
variant="outline"
>
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</>
);
};
function getNavigationUrl(lat: string, lon: string): string {
if (typeof window === "undefined") {
return `https://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${lat},${lon}`;
}
const isIOS = /iPad|iPhone|iPod|Mac/.test(navigator.userAgent);
const protocol = isIOS ? "maps" : "https";
return `${protocol}://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${lat},${lon}`;
}
const PostUnlock = () => {
const { servizioId } = useServizio();
const { annuncioId } = useServizioAnnuncio();
const { data, isLoading } = api.annunci.getProprietarioData.useQuery({
annuncioId,
servizioId,
});
const { t } = useTranslation();
// Memoize navigation URL
const navigationUrl = useMemo(() => {
if (!data?.propData || !data.propData.lat || !data.propData.lon)
return undefined;
return getNavigationUrl(data.propData.lat, data.propData.lon);
}, [data?.propData]);
if (isLoading) {
return (
<CredenzaBody className="flex justify-center p-8">
<LoadingPage />
</CredenzaBody>
);
}
if (!data || !data.propData) {
return (
<CredenzaBody>
<p className="text-destructive">{t.contatto.error}</p>
</CredenzaBody>
);
}
const { opened_at, propData } = data;
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5 sm:max-w-xl">
<div className="space-y-10">
{/* Proprietario Section */}
<section>
<h3 className="mb-2 font-semibold">{t.contatto.propietario}:</h3>
<p className="mb-2">
{t.contatto.nome}: {propData.locatore}
</p>
<div className="flex flex-wrap items-center gap-5">
<a
className="flex items-center gap-2 text-blue-600 hover:underline"
href={`tel:${propData.numero}`}
>
<Phone className="size-4" />
<span>{propData.numero}</span>
</a>
<Link href={`https://wa.me/${propData.numero}`} target="_blank">
<Button size="sm" variant="success">
<WhatsAppIcon2 className="size-4 fill-white" />
WhatsApp
</Button>
</Link>
</div>
</section>
{/* Immobile Section */}
<section>
<h3 className="mb-2 font-semibold">{t.contatto.immobile}:</h3>
<p className="mb-2">
{t.contatto.indirizzo}: {propData.indirizzo}, {propData.civico}{" "}
{propData.comune} ({propData.provincia})
</p>
{navigationUrl && (
<a
className="inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 hover:bg-accent"
href={navigationUrl}
rel="noreferrer"
target="_blank"
>
<Navigation className="size-4" />
<span>{t.contatto.apri_nav}</span>
</a>
)}
</section>
{/* Warning */}
<div className="flex w-fit items-start gap-3 rounded-md bg-red-500 p-2 text-white">
<TriangleAlert className="size-6 shrink-0" />
<div className="space-y-1">
<p className="font-medium">{t.contatto.contatta}</p>
<p className="text-sm">{t.contatto.orari}</p>
</div>
</div>
{/* Metadata */}
<p className="text-muted-foreground text-sm">
{t.contatto.sbloccato_il}:{" "}
{opened_at.toLocaleString("it", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza" variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</>
);
};