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

846 lines
22 KiB
TypeScript
Raw Normal View History

import { format } from "date-fns";
import {
CircleCheck,
ClipboardPaste,
FlaskConical,
Pen,
Trash2,
Wrench,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import toast from "react-hot-toast";
import { useDebounce } from "react-use";
import z from "zod";
import { it } from "~/i18n/it";
import { handleConsegna } from "~/lib/annuncio_details";
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 { Confirm } from "../confirm";
import { Counter } from "../counter";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza";
import { NumberInput } from "../custom_ui/InputNumber";
import {
type ListOption,
MultiSelect,
UnpackOptions,
} from "../custom_ui/multiselect";
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 { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { Switch } from "../ui/switch";
import { UploadModal } from "../upload_modal";
import { ServizioPacksInfos } from "./servizio_actions";
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"
size="sm"
variant={data.hasConfermaAdmin ? "success" : "orange"}
>
{data.hasConfermaAdmin ? <CircleCheck /> : <Wrench />}
<span>Conferma</span>
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] w-full px-3 sm:max-w-6xl">
<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-4">
<DocSection />
<Caparra />
{servizio.tipologia === TipologiaPosizioneEnum.Transitorio && (
<MotivazioneSection />
)}
<SendConferma />
</div>
</CredenzaBody>
<CredenzaFooter>
<DebugActions />
<CredenzaClose asChild>
<Button aria-label="Close Credenza" variant="outline">
Chiudi
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};
const DebugActions = () => {
const { servizioId, userId } = useServizio();
const { annuncioId } = useServizioAnnuncio();
const utils = api.useUtils();
const { mutate } = api.servizio.updateServizioAnnunci.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({
userId: userId,
});
await utils.servizio.getServizio.invalidate({
servizioId,
});
toast.success("Dati salvati con successo");
},
});
return (
<Popover>
<PopoverTrigger asChild>
<Button>
<FlaskConical />
<span>Debug</span>
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className="flex w-fit flex-col items-center gap-4"
>
<Confirm
description="Sei sicuro di voler eliminare l'annuncio?"
onConfirm={() => {
mutate({
annuncioId,
servizioId,
data: {
user_confirmed_at: null,
},
});
}}
title="Elimina annuncio"
>
<Button
aria-label="Delete Annuncio"
className="w-full"
size="sm"
variant="destructive"
>
<Trash2 />
<span>Elimina Conferma</span>
</Button>
</Confirm>
</PopoverContent>
</Popover>
);
};
const DocSection = () => {
const { userId } = useServizio();
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const utils = api.useUtils();
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.parametri.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>
{!servizio.skipDocMotivazione && (
<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 TransitorioSection = () => {
const { servizio } = useServizio();
const { data } = useServizioAnnuncio();
const { t } = useTranslation();
const { servizioId, userId } = useServizio();
const utils = api.useUtils();
const { mutate: update } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
toast.success("Servizio modificato con successo");
await utils.servizio.getUserServizi.invalidate();
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getPacksSolution.invalidate({ servizioId });
},
});
const handlePermanenzaChange = (value: ListOption) => {
update({
servizioId,
data: {
permanenza: value.value !== "" ? parseInt(value.value) : null,
},
});
};
const preferenzeDescrizione = data.desc_it?.match(/Permanenza:.*$/m);
const permanenzaAnnuncio = data.permanenza
? `${data.permanenza.join(" - ")} mesi`
: (preferenzeDescrizione?.[0] ??
"errore: testo non trovato in descrizione");
return (
<div className="flex flex-col items-start gap-4 rounded-md border border-border bg-muted p-2 sm:flex-row">
<div className="w-full">
<Label className="text-base" htmlFor="permanenza">
Permanenza (da parametri servizio):
</Label>
<MultiSelect
defaultValue={
servizio.permanenza
? {
label: t.parametri.permanenza[servizio.permanenza] || "",
value: servizio.permanenza.toString(),
}
: undefined
}
elementId="permanenza"
elementName="permanenza"
isMulti={false}
onValueChange={handlePermanenzaChange}
options={UnpackOptions({
options: t.parametri.permanenza,
})}
placeholder="Seleziona..."
/>
</div>
<div className="w-full space-y-2">
<p className="font-medium">Permanenza in annuncio:</p>
<p>{permanenzaAnnuncio}</p>
</div>
</div>
);
};
const StabileSection = () => {
const { servizio, servizioId, userId } = useServizio();
const { data } = useServizioAnnuncio();
const [value, setValue] = useState(servizio.budget);
const utils = api.useUtils();
const { mutate: update } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
toast.success("Servizio modificato con successo");
await utils.servizio.getUserServizi.invalidate();
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getPacksSolution.invalidate({ servizioId });
},
});
useDebounce(
() => {
if (value === servizio.budget) return;
update({
servizioId,
data: {
budget: value,
},
});
},
1000,
[value],
);
return (
<div className="flex flex-col gap-2 rounded-md border border-border bg-muted p-2 sm:flex-row">
<div className="flex w-full flex-col gap-2">
<span className="font-semibold">Budget (da preferenze servizio):</span>
<NumberInput
allowNegative={false}
defaultValue={value}
min={0}
onValueChange={(val) => {
if (val !== undefined) {
setValue(val);
}
}}
stepper={50}
suffix=" €"
/>
</div>
<div className="w-full space-y-2">
<p className="font-medium">Prezzo in descrizione:</p>
<p>{formatCurrency(data.prezzo / 100)}</p>
</div>
</div>
);
};
const GeneralSection = () => {
const { data } = useServizioAnnuncio();
const { t } = useTranslation();
const { servizio, servizioId, userId } = useServizio();
const utils = api.useUtils();
const { mutate: update } = api.servizio.updateServizio.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
toast.success("Servizio modificato con successo");
await utils.servizio.getUserServizi.invalidate();
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getPacksSolution.invalidate({ servizioId });
},
});
const handleDisponibilitàChange = (value: ListOption) => {
update({
servizioId,
data: {
entromese: value.value !== "" ? parseInt(value.value) : null,
},
});
};
const handleAdultiChange = (value: number) => {
update({
servizioId,
data: {
n_adulti: value,
},
});
};
const handleMinoriChange = (value: number) => {
update({
servizioId,
data: {
n_minori: value,
},
});
};
const personeDescrizione = data.desc_it?.match(/Preferenza:.*$/m);
const personeAnnuncio = data.persone
? data.persone.join(", ").toUpperCase()
: (personeDescrizione?.[0] ?? "errore: testo non trovato in descrizione");
const dispAnnuncio = data.disponibile_da
? format(new Date(data.disponibile_da), "dd/MM/yyyy")
: handleConsegna({
consegna: data.consegna,
aggiornamento: it.card.in_aggiornamento,
consegna_da: it.card.consegna_da,
mesi: it.parametri.mesi,
subito: it.card.consegna_subito,
});
return (
<>
<div className="flex flex-col gap-x-4 gap-y-2 rounded-md border border-border bg-muted p-2 sm:flex-row">
<div className="w-full space-y-2">
<span className="font-semibold">Persone:</span>
<div className="flex w-full gap-4">
<div className="flex w-1/2 flex-col gap-2">
<Label htmlFor="n_adulti">{t.parametri.nadulti_label}</Label>
<Counter
name="n_adulti"
onlyPositive
setValue={(v) => handleAdultiChange(Number(v))}
value={servizio.n_adulti || 0}
/>
</div>
<div className="flex w-1/2 flex-col gap-2">
<Label htmlFor="n_minori">{t.parametri.nminori_label}</Label>
<Counter
name="n_minori"
onlyPositive
setValue={(v) => handleMinoriChange(Number(v))}
value={servizio.n_minori || 0}
/>
</div>
</div>
</div>
<div className="w-full space-y-2">
<p className="font-medium">Persone in annuncio:</p>
<p>{personeAnnuncio}</p>
</div>
</div>
<div className="flex flex-col gap-x-4 gap-y-2 rounded-md border border-border bg-muted p-2 sm:flex-row">
<div className="w-full">
<Label className="text-base" htmlFor="select-mesi">
Entro mese (da parametri servizio):
</Label>
<MultiSelect
defaultValue={
servizio.entromese !== null
? {
// biome-ignore lint/style/noNonNullAssertion: <known size>
label: t.parametri.mesi[servizio.entromese]!,
value: String(servizio.entromese),
}
: undefined
}
elementId="select-mesi"
elementName="entro_mese"
isMulti={false}
onValueChange={handleDisponibilitàChange}
options={UnpackOptions({
options: t.parametri.mesi,
})}
placeholder="Seleziona..."
/>
</div>
<div className="w-full space-y-2">
<p className="font-medium">Disponibilità in annuncio:</p>
<p>{dispAnnuncio}</p>
</div>
</div>
</>
);
};
const SendConferma = () => {
const { servizio } = useServizio();
const { data } = useServizioAnnuncio();
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}`);
},
});
const docOk = data.doc_conferma_added && data.doc_conferma_ref;
const caparraOk =
data.caparra_iban &&
data.caparra_importo &&
data.caparra_intestazione &&
data.caparra_causale;
return (
<Card>
<CardHeader>
<CardTitle>Lavorazione Conferma</CardTitle>
<CardDescription>
controlla i parametri di ricerca prima!
</CardDescription>
</CardHeader>
<CardContent className="space-y-10">
<div className="flex flex-col gap-4">
<p>
<span className="font-semibold">Tipologia servizio: </span>
{servizio.tipologia}
</p>
{servizio.tipologia === TipologiaPosizioneEnum.Transitorio ? (
<TransitorioSection />
) : (
<StabileSection />
)}
<GeneralSection />
<ServizioPacksInfos />
</div>
<div className="space-y-2">
{!docOk && (
<p className="text-red-500">
Attenzione: il documento di conferma non è stato ancora caricato
</p>
)}
{!caparraOk && (
<p className="text-red-500">
Attenzione: i dati per il pagamento della caparra non sono
completi
</p>
)}
<div className="mt-4 flex items-center gap-3">
<Switch
aria-label="Conferma Lavorata"
checked={data.hasConfermaAdmin}
disabled={!docOk || !caparraOk}
id="conferma"
onCheckedChange={(c) => {
handleUpdate({
servizioId: servizio.servizio_id,
annuncioId: data.id,
status: c,
});
}}
/>
<Label className="text-base" htmlFor="conferma">
Imposta Conferma come Lavorata
</Label>
</div>
</div>
</CardContent>
</Card>
);
};