infoalloggi-monorepo/apps/infoalloggi/src/components/servizio/admin_conferma.tsx
Marco Pedone 23525d8f14 feat: add AdminLavoraConferma and AdminContratto components for managing service confirmations and contracts
- Implemented AdminLavoraConferma component to handle confirmation processes with document management.
- Added AdminContratto component for managing rental contracts and related documents.
- Introduced shared components for document selection and display (DocInfo and DocCombo).
- Enhanced storage controller with a new function to retrieve storage ID from user storage ID.
- Updated database interactions to improve error handling and data retrieval.
2025-11-06 17:03:42 +01:00

540 lines
14 KiB
TypeScript

import { format } from "date-fns";
import { CircleCheck, ClipboardPaste, Pen, Trash2, Wrench } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import toast from "react-hot-toast";
import z from "zod";
import { formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
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 {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza";
import { LoadingPage } from "../loading";
import { Button } from "../ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../ui/card";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import { UploadModal } from "../upload_modal";
import { DocCombo, DocInfo } from "./shared";
export const AdminLavoraConferma = () => {
const { servizio } = useServizio();
const { data } = useServizioAnnuncio();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button
aria-label="Open Lavora Conferma Section"
className="w-fit"
variant={data.hasConfermaAdmin ? "success" : "secondary"}
>
{data.hasConfermaAdmin ? (
<CircleCheck className="size-6" />
) : (
<Wrench className="size-6" />
)}
Conferma
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] w-full px-3 sm:max-w-4xl">
<CredenzaHeader>
<CredenzaTitle>Lavorazione conferma</CredenzaTitle>
<CredenzaDescription className="sr-only">
Consegna status modal
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] w-full overflow-y-auto pb-5">
<div className="flex flex-col gap-8">
<DocSection />
<Caparra />
{servizio.tipologia === TipologiaPosizioneEnum.Transitorio && (
<MotivazioneSection />
)}
<SendConferma />
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Close Credenza" variant="outline">
Chiudi
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};
const DocSection = () => {
const { userId } = useServizio();
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const utils = api.useUtils();
// const { mutate: updateServizio } = api.servizio.UpdateConfermaDoc.useMutation(
// {
// onError: (error) => {
// toast.error(error.message);
// },
// onSuccess: async () => {
// await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
// await utils.servizio.getServizio.invalidate({
// servizioId,
// });
// toast.success("Dati salvati con successo");
// },
// },
// );
const { data: files, isLoading } = api.storage.getAll.useQuery();
const { mutate: setUserStorage } =
api.storage.addUserStorageEntry.useMutation({
onSuccess: async () => {
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
},
});
if (isLoading) return <LoadingPage />;
if (files === undefined) {
return <p>Errore nel caricamento dei documenti</p>;
}
return (
<Card>
<CardHeader>
<CardTitle>Documento di Conferma</CardTitle>
<CardDescription>
documento che l&apos;utente vedra alla conferma
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{data.doc_conferma_ref && (
<div className="flex flex-wrap items-start gap-3">
<DocInfo storageId={data.doc_conferma_ref}>
<Button
aria-label="Reset Conferma"
className="flex items-center gap-3"
onClick={async () => {
await updateServizioAnnunci({
doc_conferma_added: false,
doc_conferma_ref: null,
});
}}
variant="destructive"
>
<Trash2 />
</Button>
</DocInfo>
</div>
)}
<div className="flex flex-wrap items-center gap-3">
<DocCombo
current={data.doc_conferma_ref}
files={files}
onSelect={async (f) => {
await updateServizioAnnunci({
doc_conferma_added: true,
doc_conferma_ref: f.id,
});
setUserStorage({
storageId: f.id,
userId,
});
}}
/>
<UploadModal isAdmin userId={userId} />
</div>
</CardContent>
</Card>
);
};
const CaparraSchema = z.object({
caparra_causale: z.string().nullable(),
caparra_iban: z.string().nullable(),
caparra_importo: z.string().nullable(),
caparra_intestazione: z.string().nullable(),
});
type CaparraObj = z.infer<typeof CaparraSchema>;
const parseCaparra = (ingest: string) => {
let parsedJson: unknown;
try {
parsedJson = JSON.parse(ingest);
} catch {
toast.error(
'Stringa non valida, clicca "copia stringa" nel gestionale e poi questo pulsante',
);
return undefined;
}
const parsed = CaparraSchema.safeParse(parsedJson);
if (parsed.success) {
return parsed.data;
}
console.error("Invalid caparra format:", parsed.error);
return undefined;
};
const Caparra = () => {
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const [caparra, setCaparra] = useState<CaparraObj | undefined>({
caparra_causale: data.caparra_causale,
caparra_iban: data.caparra_iban,
caparra_importo: data.caparra_importo,
caparra_intestazione: data.caparra_intestazione,
});
const handleSave = async () => {
await updateServizioAnnunci({
caparra_causale: caparra?.caparra_causale || null,
caparra_iban: caparra?.caparra_iban || null,
caparra_importo: caparra?.caparra_importo || null,
caparra_intestazione: caparra?.caparra_intestazione || null,
});
};
return (
<Card>
<CardHeader>
<CardTitle>Dati Pagamento Caparra</CardTitle>
<CardDescription>
dati per il pagamento della caparra, da mostrare all&apos;utente
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap items-center gap-3">
<Button
aria-label="Import Caparra"
onClick={async () => {
const clipboardText = await navigator.clipboard.readText();
setCaparra(parseCaparra(clipboardText));
}}
variant="info"
>
<ClipboardPaste className="size-4" />
Dal gestionale
</Button>
<Button
aria-label="Reset Caparra"
onClick={async () => {
setCaparra({
caparra_causale: null,
caparra_iban: null,
caparra_importo: null,
caparra_intestazione: null,
});
}}
variant="destructive"
>
<Trash2 className="size-4" />
</Button>
</div>
{caparra ? (
<div className="flex flex-col gap-2">
<p>
<span className="font-semibold">Intestazione: </span>
{caparra.caparra_intestazione}{" "}
<Button
aria-label="Edit Intestazione"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica l'intestazione della caparra:",
caparra.caparra_intestazione || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev
? { ...prev, caparra_intestazione: newValue }
: undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">IBAN: </span>
{caparra.caparra_iban}{" "}
<Button
aria-label="Edit IBAN"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica l'IBAN della caparra:",
caparra.caparra_iban || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev ? { ...prev, caparra_iban: newValue } : undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Importo: </span>
{caparra.caparra_importo}{" "}
<Button
aria-label="Edit Importo"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica l'importo della caparra:",
caparra.caparra_importo || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev ? { ...prev, caparra_importo: newValue } : undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Causale: </span>
{caparra.caparra_causale}{" "}
<Button
aria-label="Edit Causale"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica la causale della caparra:",
caparra.caparra_causale || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev ? { ...prev, caparra_causale: newValue } : undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
</div>
) : (
<p>Inserisci la stringa dal gestionale e elabora</p>
)}
</CardContent>
<CardFooter>
<Button
aria-label="Save Caparra"
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};
const MotivazioneSection = () => {
const { servizioId } = useServizio();
const { t } = useTranslation();
const { data: servizio } = api.servizio.getServizio.useQuery({ servizioId });
if (!servizio) return null;
return (
<Card>
<CardHeader>
<CardTitle>Motivazione di transitorietà</CardTitle>
<CardDescription>
Informazioni relative alla motivazione di transitorietà inserite
dall&apos;utente
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col gap-2">
<p>
<span className="font-semibold">Motivazione specificata: </span>
{servizio.motivazione_transitorio ? (
<span>
{
t.preferenze.motivazioni_mappings.find(
(m) => m.id === servizio.motivazione_transitorio,
)?.title
}
</span>
) : (
<span className="text-red-500 italic">Non inserita</span>
)}
</p>
<p>
<span className="font-semibold">
Data scadenza motivazione inserita:{" "}
</span>
{servizio.scadenza_motivazione_transitoria ? (
format(servizio.scadenza_motivazione_transitoria, "dd/MM/yyyy")
) : (
<span className="text-red-500 italic">Non inserita</span>
)}
</p>
<p>
<span className="font-semibold">Documento caricato: </span>
{servizio.doc_motivazione_ref ? (
<FileLink userStorageId={servizio.doc_motivazione_ref} />
) : (
<span className="text-red-500 italic">Non caricato</span>
)}
</p>
</div>
</CardContent>
</Card>
);
};
const FileLink = ({
userStorageId,
}: {
userStorageId: UsersStorageUserStorageId;
}) => {
const { data: storageId } =
api.storage.getStorageIdFromUserStorageId.useQuery({
userStorageId,
});
if (!storageId) return null;
return (
<Link
className="underline hover:text-primary"
href={`/area-riservata/allegato-view/${storageId}`}
rel="noreferrer"
target="_blank"
>
Vedi documento
</Link>
);
};
const SendConferma = () => {
const { servizio } = useServizio();
const { data } = useServizioAnnuncio();
const { t } = useTranslation();
const [check, setCheck] = useState(data.hasConfermaAdmin);
const utils = api.useUtils();
const { mutate: handleUpdate } =
api.servizio.adminUpdateConfermaStatus.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate();
await utils.servizio.getServizio.invalidate({
servizioId: servizio.servizio_id,
});
toast.success("Status aggiornato con successo");
},
onError: (error) => {
toast.error(`Errore durante l'aggiornamento: ${error.message}`);
},
});
return (
<Card>
<CardHeader>
<CardTitle>Lavorazione Conferma</CardTitle>
<CardDescription>
controlla i parametri di ricerca prima!
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col gap-2">
<p>Tipologia servizio: {servizio.tipologia}</p>
{(() => {
if (servizio.tipologia === TipologiaPosizioneEnum.Transitorio) {
return (
<>
<p>
Permanenza specificata dall'utente:{" "}
{servizio.permanenza
? t.preferenze.permanenza[servizio.permanenza]
: "non inserita"}
</p>
<p>
Permanenza in descrizione: {(() => {
const match = data.desc_it?.match(/Permanenza:.*$/m);
return match
? `"${match[0]}"`
: "errore: testo non trovato in descrizione";
})()}
</p>
</>
);
}
return (
<>
<p>Budget (da preferenze): {formatCurrency(servizio.budget)}</p>
<p>
Canone Mensile (da descrizione):{" "}
{formatCurrency(data.prezzo / 100)}
</p>
</>
);
})()}
</div>
<div className="flex items-center gap-3">
<Switch
aria-label="Conferma Lavorata"
checked={check}
id="conferma"
onCheckedChange={(c) => {
setCheck(c);
}}
/>
<Label htmlFor="conferma">Conferma lavorata</Label>
</div>
</CardContent>
<CardFooter>
<Button
aria-label="Save Conferma Status"
onClick={() => {
handleUpdate({
servizioId: servizio.servizio_id,
annuncioId: data.id,
status: check,
});
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};