feat: integrate DocumentiUploadSection for document handling in various components

This commit is contained in:
Marco Pedone 2025-11-11 15:41:20 +01:00
parent 4408295f56
commit d116ebd3d5
3 changed files with 207 additions and 235 deletions

View file

@ -1,13 +1,15 @@
import { ExternalLink, Trash2 } from "lucide-react"; import { CircleCheck, ExternalLink, Trash2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useSession } from "~/providers/SessionProvider"; import { useSession } from "~/providers/SessionProvider";
import type { UsersId } from "~/schemas/public/Users";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage"; import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import type { UserWithAnagrafica } from "~/server/services/user.service"; import type { UserWithAnagrafica } from "~/server/services/user.service";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { Confirm } from "../confirm"; import { Confirm } from "../confirm";
import { ImageFlbk } from "../ImageWithFallback"; import { ImageFlbk } from "../ImageWithFallback";
import { LoadingPage } from "../loading"; import { LoadingPage } from "../loading";
import { LoadingSpinner } from "../spinner";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { import {
Card, Card,
@ -63,125 +65,35 @@ export const DocumentiPersonali = ({
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-4"> <DocumentiUploadSection
<div> document={userData.doc_personale_fronte}
<h4 className="mb-2 font-medium"> isAdmin={user.isAdmin}
Documento d'identità - Fronte isPending={isPending}
</h4> onRemove={() =>
mutate({
userId: userData.id,
data: { doc_personale_fronte_ref: null },
})
}
onUpload={(id) => handleUpload("doc_personale_fronte_ref", id)}
title="Documento d'identità - Fronte"
userId={userData.id}
/>
{userData.doc_personale_fronte?.storageId ? ( <DocumentiUploadSection
<div className="flex items-center gap-4"> document={userData.doc_personale_retro}
<ImageFlbk isAdmin={user.isAdmin}
alt="documento personale fronte" isPending={isPending}
className="size-24 rounded-md object-cover" onRemove={() =>
height={50} mutate({
src={`/storage-api/get/${userData.doc_personale_fronte.storageId}`} userId: userData.id,
width={50} data: { doc_personale_retro_ref: null },
/> })
}
<div className="flex flex-col justify-center gap-2"> onUpload={(id) => handleUpload("doc_personale_retro_ref", id)}
<Link title="Documento d'identità - Retro"
href={`/area-riservata/allegato-view/${userData.doc_personale_fronte.storageId}`} userId={userData.id}
target="_blank" />
>
<Button size="sm">
<span>Visualizza</span>
<ExternalLink />
</Button>
</Link>
<Confirm
description="Sei sicuro di voler eliminare il documento?"
onConfirm={() => {
mutate({
userId: userData.id,
data: { doc_personale_fronte_ref: null },
});
}}
title="Elimina Documento"
>
<Button
aria-label="Elimina Doc"
size="sm"
variant="destructive"
>
<span>Elimina Documento</span>
<Trash2 />
</Button>
</Confirm>
</div>
</div>
) : (
<UploadModal
cb_onUpload={({ userStorageIds }) =>
userStorageIds.length > 0 &&
userStorageIds[0] &&
handleUpload("doc_personale_fronte_ref", userStorageIds[0])
}
isAdmin={user.isAdmin}
maxFiles={1}
userId={userData.id}
/>
)}
</div>
</div>
<div className="flex flex-col gap-4">
<div>
<h4 className="mb-2 font-medium">Documento d'identità - Retro</h4>
{userData.doc_personale_retro?.storageId ? (
<div className="flex items-center gap-4">
<ImageFlbk
alt="documento personale retro"
className="size-24 rounded-md object-cover"
height={50}
src={`/storage-api/get/${userData.doc_personale_retro.storageId}`}
width={50}
/>
<div className="flex flex-col justify-center gap-2">
<Link
href={`/area-riservata/allegato-view/${userData.doc_personale_retro.storageId}`}
target="_blank"
>
<Button size="sm">
<span>Visualizza</span>
<ExternalLink />
</Button>
</Link>
<Confirm
description="Sei sicuro di voler eliminare il documento?"
onConfirm={() => {
mutate({
userId: userData.id,
data: { doc_personale_retro_ref: null },
});
}}
title="Elimina Documento"
>
<Button
aria-label="Elimina Doc"
size="sm"
variant="destructive"
>
<span>Elimina Documento</span>
<Trash2 />
</Button>
</Confirm>
</div>
</div>
) : (
<UploadModal
cb_onUpload={({ userStorageIds }) =>
userStorageIds.length > 0 &&
userStorageIds[0] &&
handleUpload("doc_personale_retro_ref", userStorageIds[0])
}
isAdmin={user.isAdmin}
maxFiles={1}
userId={userData.id}
/>
)}
</div>
</div>
</div> </div>
</CardContent> </CardContent>
<CardFooter> <CardFooter>
@ -196,3 +108,96 @@ export const DocumentiPersonali = ({
</Card> </Card>
); );
}; };
export const DocumentiUploadSection = ({
title,
document,
onUpload,
onRemove,
isAdmin,
userId,
isPending,
caricatoNote,
}: {
title: string;
document: {
storageId: string;
ref: UsersStorageUserStorageId | null;
} | null;
onUpload: (id: UsersStorageUserStorageId) => void;
onRemove?: () => void;
isAdmin: boolean;
userId: UsersId;
isPending: boolean;
caricatoNote?: boolean;
}) => {
if (document?.storageId) {
return (
<div className="flex flex-col gap-2">
<h4 className="font-medium">{title}</h4>
{caricatoNote && (
<div className="flex items-center gap-2 text-green-500">
<CircleCheck className="size-5" />
<span>Documento caricato</span>
</div>
)}
<div className="flex items-center gap-4">
{isPending ? (
<LoadingSpinner className="size-24 stroke-neutral-300" />
) : (
<ImageFlbk
alt={title}
className="size-24 rounded-md border object-cover"
height={96}
src={`/storage-api/get/${document.storageId}`}
width={96}
/>
)}
<div className="flex flex-col justify-center gap-2">
<Link
href={`/area-riservata/allegato-view/${document.storageId}`}
target="_blank"
>
<Button size="sm" variant="outline">
<span>Visualizza</span>
<ExternalLink className="size-4" />
</Button>
</Link>
{onRemove && (
<Confirm
description="Sei sicuro di voler eliminare il documento?"
onConfirm={onRemove}
title="Elimina Documento"
>
<Button
aria-label="Elimina documento"
disabled={isPending}
size="sm"
variant="destructive"
>
<span>Rimuovi</span>
<Trash2 className="size-4" />
</Button>
</Confirm>
)}
</div>
</div>
</div>
);
}
return (
<div className="flex flex-col gap-2">
<h4 className="font-medium">{title}</h4>
<UploadModal
cb_onUpload={({ userStorageIds }) => {
const id = userStorageIds[0];
if (id) onUpload(id);
}}
isAdmin={isAdmin}
maxFiles={1}
userId={userId}
/>
</div>
);
};

View file

@ -9,6 +9,7 @@ import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage"; import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { DocumentiUploadSection } from "../area-riservata/documenti_personali";
import { import {
Credenza, Credenza,
CredenzaBody, CredenzaBody,
@ -22,7 +23,6 @@ import {
} from "../custom_ui/credenza"; } from "../custom_ui/credenza";
import LoadingButton from "../custom_ui/loading-button"; import LoadingButton from "../custom_ui/loading-button";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { type UploadCallback, UploadModal } from "../upload_modal";
export const FileSection = ({ storageId }: { storageId: string }) => { export const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({ const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
@ -134,40 +134,9 @@ const InvioConferma = ({ setOpen }: { setOpen: (value: boolean) => void }) => {
); );
}; };
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 DocCheckpoint = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { userId, servizio } = useServizio(); const { userId, servizio, isAdmin } = useServizio();
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate, isPending } = api.servizio.updateServizio.useMutation({ const { mutate, isPending } = api.servizio.updateServizio.useMutation({
onSuccess: async () => { onSuccess: async () => {
@ -198,17 +167,15 @@ const DocCheckpoint = () => {
motivazione di transitorietà motivazione di transitorietà
</p> </p>
<div className="flex flex-col gap-2"> <DocumentiUploadSection
<span>Documento:</span> caricatoNote
<DocComponent document={servizio.doc_motivazione}
cb={({ userStorageIds }) => isAdmin={isAdmin}
userStorageIds.length > 0 && isPending={isPending}
userStorageIds[0] && onUpload={handleUpload}
handleUpload(userStorageIds[0]) title="Documento:"
} userId={userId}
docInfo={servizio.doc_motivazione} />
/>
</div>
<br /> <br />
<br /> <br />

View file

@ -1,17 +1,12 @@
import { import { CircleUser, Navigation, Phone, TriangleAlert } from "lucide-react";
CircleCheck,
CircleUser,
Navigation,
Phone,
TriangleAlert,
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider"; import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage"; import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { DocumentiUploadSection } from "../area-riservata/documenti_personali";
import { import {
Credenza, Credenza,
CredenzaBody, CredenzaBody,
@ -26,22 +21,32 @@ import {
import { LoadingPage } from "../loading"; import { LoadingPage } from "../loading";
import { WhatsAppIcon2 } from "../svgs"; import { WhatsAppIcon2 } from "../svgs";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { type UploadCallback, UploadModal } from "../upload_modal";
export const ContattiProprietarioModal = () => { export const ContattiProprietarioModal = () => {
const { servizio } = useServizio(); const { servizio } = useServizio();
const [open, setOpen] = useState(false);
const { data } = useServizioAnnuncio(); const { data } = useServizioAnnuncio();
const { t } = useTranslation(); const { t } = useTranslation();
const isUnlocked = data.open_contatti_at != null; const isUnlocked = data.open_contatti_at != null;
const needsDocuments = const needsDocuments =
!servizio.skipControlloDoc && !servizio.skipControlloDoc &&
(!servizio.doc_personale_fronte || !servizio.doc_personale_retro); (!servizio.doc_personale_fronte || !servizio.doc_personale_retro);
const [showDocStep, setShowDocStep] = useState(
open && (needsDocuments || !isUnlocked),
);
return ( return (
<Credenza> <Credenza
onOpenChange={(o) => {
setOpen(o);
if (o && (needsDocuments || !isUnlocked)) {
setShowDocStep(true);
}
}}
open={open}
>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
{data.open_contatti_at != null ? ( {isUnlocked ? (
<Button <Button
aria-label="Vedi contatti proprietario" aria-label="Vedi contatti proprietario"
className="w-fit" className="w-fit"
@ -68,8 +73,8 @@ export const ContattiProprietarioModal = () => {
{t.contatto.descrizione} {t.contatto.descrizione}
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
{needsDocuments ? ( {showDocStep ? (
<DocCheckpoint /> <DocCheckpoint setStep={setShowDocStep} />
) : isUnlocked ? ( ) : isUnlocked ? (
<PostUnlock /> <PostUnlock />
) : ( ) : (
@ -80,40 +85,9 @@ export const ContattiProprietarioModal = () => {
); );
}; };
const DocComponent = ({ const DocCheckpoint = ({ setStep }: { setStep: (value: boolean) => void }) => {
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 { t } = useTranslation();
const { userId, servizio } = useServizio(); const { userId, servizio, isAdmin } = useServizio();
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({ const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({
onSuccess: async () => { onSuccess: async () => {
@ -143,33 +117,47 @@ const DocCheckpoint = () => {
return ( return (
<> <>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5"> <CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<div className="space-y-5"> <div className="flex flex-col gap-4">
<p>Devi ancora caricare un tuo documento</p> <div>
<div className="flex flex-col gap-2"> <p className="font-semibold">Carica i tuoi documenti</p>
<span>Fronte documento:</span> <p>
<DocComponent se si ha una scansione unica fronte/retro dei doc, caricarla due
cb={({ userStorageIds }) => volte.
userStorageIds.length > 0 && </p>
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> </div>
<br /> <DocumentiUploadSection
<br /> caricatoNote
document={servizio.doc_personale_fronte}
isAdmin={isAdmin}
isPending={isPending}
onRemove={() =>
mutate({
userId,
data: { doc_personale_fronte_ref: null },
})
}
onUpload={(id) => handleUpload("doc_personale_fronte_ref", id)}
title="Documento d'identità - Fronte"
userId={userId}
/>
<DocumentiUploadSection
caricatoNote
document={servizio.doc_personale_retro}
isAdmin={isAdmin}
isPending={isPending}
onRemove={() =>
mutate({
userId,
data: { doc_personale_retro_ref: null },
})
}
onUpload={(id) => handleUpload("doc_personale_retro_ref", id)}
title="Documento d'identità - Retro"
userId={userId}
/>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
I tuoi dati saranno conservati solamente per la durata del servizio, I tuoi dati saranno conservati solamente per la durata del servizio,
in conformità con la nostra{" "} in conformità con la nostra{" "}
@ -180,6 +168,18 @@ const DocCheckpoint = () => {
</div> </div>
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<Button
aria-label="Procedi"
disabled={
isPending ||
!servizio.doc_personale_fronte?.storageId ||
!servizio.doc_personale_retro?.storageId
}
onClick={() => setStep(false)}
variant="success"
>
Procedi
</Button>
<CredenzaClose asChild> <CredenzaClose asChild>
<Button aria-label="Chiudi" disabled={isPending} variant="outline"> <Button aria-label="Chiudi" disabled={isPending} variant="outline">
{t.chiudi} {t.chiudi}