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 toast from "react-hot-toast";
import { useSession } from "~/providers/SessionProvider";
import type { UsersId } from "~/schemas/public/Users";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import type { UserWithAnagrafica } from "~/server/services/user.service";
import { api } from "~/utils/api";
import { Confirm } from "../confirm";
import { ImageFlbk } from "../ImageWithFallback";
import { LoadingPage } from "../loading";
import { LoadingSpinner } from "../spinner";
import { Button } from "../ui/button";
import {
Card,
@ -63,125 +65,35 @@ export const DocumentiPersonali = ({
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4">
<div>
<h4 className="mb-2 font-medium">
Documento d'identità - Fronte
</h4>
{userData.doc_personale_fronte?.storageId ? (
<div className="flex items-center gap-4">
<ImageFlbk
alt="documento personale fronte"
className="size-24 rounded-md object-cover"
height={50}
src={`/storage-api/get/${userData.doc_personale_fronte.storageId}`}
width={50}
/>
<div className="flex flex-col justify-center gap-2">
<Link
href={`/area-riservata/allegato-view/${userData.doc_personale_fronte.storageId}`}
target="_blank"
>
<Button size="sm">
<span>Visualizza</span>
<ExternalLink />
</Button>
</Link>
<Confirm
description="Sei sicuro di voler eliminare il documento?"
onConfirm={() => {
<DocumentiUploadSection
document={userData.doc_personale_fronte}
isAdmin={user.isAdmin}
isPending={isPending}
onRemove={() =>
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}
onUpload={(id) => handleUpload("doc_personale_fronte_ref", id)}
title="Documento d'identità - Fronte"
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={() => {
<DocumentiUploadSection
document={userData.doc_personale_retro}
isAdmin={user.isAdmin}
isPending={isPending}
onRemove={() =>
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}
onUpload={(id) => handleUpload("doc_personale_retro_ref", id)}
title="Documento d'identità - Retro"
userId={userData.id}
/>
)}
</div>
</div>
</div>
</CardContent>
<CardFooter>
@ -196,3 +108,96 @@ export const DocumentiPersonali = ({
</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 type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import { api } from "~/utils/api";
import { DocumentiUploadSection } from "../area-riservata/documenti_personali";
import {
Credenza,
CredenzaBody,
@ -22,7 +23,6 @@ import {
} from "../custom_ui/credenza";
import LoadingButton from "../custom_ui/loading-button";
import { Button } from "../ui/button";
import { type UploadCallback, UploadModal } from "../upload_modal";
export const FileSection = ({ storageId }: { storageId: string }) => {
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 { t } = useTranslation();
const { userId, servizio } = useServizio();
const { userId, servizio, isAdmin } = useServizio();
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.updateServizio.useMutation({
onSuccess: async () => {
@ -198,17 +167,15 @@ const DocCheckpoint = () => {
motivazione di transitorietà
</p>
<div className="flex flex-col gap-2">
<span>Documento:</span>
<DocComponent
cb={({ userStorageIds }) =>
userStorageIds.length > 0 &&
userStorageIds[0] &&
handleUpload(userStorageIds[0])
}
docInfo={servizio.doc_motivazione}
<DocumentiUploadSection
caricatoNote
document={servizio.doc_motivazione}
isAdmin={isAdmin}
isPending={isPending}
onUpload={handleUpload}
title="Documento:"
userId={userId}
/>
</div>
<br />
<br />

View file

@ -1,17 +1,12 @@
import {
CircleCheck,
CircleUser,
Navigation,
Phone,
TriangleAlert,
} from "lucide-react";
import { CircleUser, Navigation, Phone, TriangleAlert } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import { useMemo, useState } 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 { DocumentiUploadSection } from "../area-riservata/documenti_personali";
import {
Credenza,
CredenzaBody,
@ -26,22 +21,32 @@ import {
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 [open, setOpen] = useState(false);
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);
const [showDocStep, setShowDocStep] = useState(
open && (needsDocuments || !isUnlocked),
);
return (
<Credenza>
<Credenza
onOpenChange={(o) => {
setOpen(o);
if (o && (needsDocuments || !isUnlocked)) {
setShowDocStep(true);
}
}}
open={open}
>
<CredenzaTrigger asChild>
{data.open_contatti_at != null ? (
{isUnlocked ? (
<Button
aria-label="Vedi contatti proprietario"
className="w-fit"
@ -68,8 +73,8 @@ export const ContattiProprietarioModal = () => {
{t.contatto.descrizione}
</CredenzaDescription>
</CredenzaHeader>
{needsDocuments ? (
<DocCheckpoint />
{showDocStep ? (
<DocCheckpoint setStep={setShowDocStep} />
) : isUnlocked ? (
<PostUnlock />
) : (
@ -80,40 +85,9 @@ export const ContattiProprietarioModal = () => {
);
};
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 = ({ setStep }: { setStep: (value: boolean) => void }) => {
const { t } = useTranslation();
const { userId, servizio } = useServizio();
const { userId, servizio, isAdmin } = useServizio();
const utils = api.useUtils();
const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({
onSuccess: async () => {
@ -143,33 +117,47 @@ const DocCheckpoint = () => {
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 className="flex flex-col gap-4">
<div>
<p className="font-semibold">Carica i tuoi documenti</p>
<p>
se si ha una scansione unica fronte/retro dei doc, caricarla due
volte.
</p>
</div>
<br />
<br />
<DocumentiUploadSection
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">
I tuoi dati saranno conservati solamente per la durata del servizio,
in conformità con la nostra{" "}
@ -180,6 +168,18 @@ const DocCheckpoint = () => {
</div>
</CredenzaBody>
<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>
<Button aria-label="Chiudi" disabled={isPending} variant="outline">
{t.chiudi}