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

1259 lines
31 KiB
TypeScript
Raw Normal View History

2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
Check,
ChevronsUpDown,
CircleCheck,
ClipboardPaste,
ExternalLink,
FolderKanban,
Pen,
Plus,
Trash2,
Wrench,
2025-08-04 17:45:44 +02:00
} from "lucide-react";
2025-08-28 18:27:07 +02:00
import Link from "next/link";
import { useRef, useState } from "react";
import toast from "react-hot-toast";
import { z } from "zod/v4";
2025-08-04 17:45:44 +02:00
import { ExtIcon } from "~/components/area-riservata/allegati";
2025-08-28 18:27:07 +02:00
import { Confirm } from "~/components/confirm";
2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
AnimatedButton,
type AnimatedButtonRef,
} from "~/components/custom_ui/animated-button";
2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
2025-08-04 17:45:44 +02:00
} from "~/components/custom_ui/credenza";
import { LoadingPage } from "~/components/loading";
2025-08-28 18:27:07 +02:00
import { Button, buttonVariants } from "~/components/ui/button";
2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
2025-08-04 17:45:44 +02:00
} from "~/components/ui/card";
2025-08-28 18:27:07 +02:00
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "~/components/ui/command";
import { Label } from "~/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { Separator } from "~/components/ui/separator";
2025-08-04 17:45:44 +02:00
import { Switch } from "~/components/ui/switch";
2025-08-28 18:27:07 +02:00
import { UploadModal } from "~/components/upload_modal";
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
2025-08-04 17:45:44 +02:00
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
2025-08-28 18:27:07 +02:00
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
import type {
Storageindex,
StorageindexId,
} from "~/schemas/public/Storageindex";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
2025-08-28 18:27:07 +02:00
import { api } from "~/utils/api";
2025-08-04 17:45:44 +02:00
export const AnnuncioActions = () => {
2025-08-28 18:27:07 +02:00
const { servizioId, userId, isAdmin } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const utils = api.useUtils();
const { mutate } = api.servizio.removeAnnuncioServizio.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
toast.success("Annuncio rimosso");
},
onError: (error) => {
console.error(error);
toast.error("Errore durante la rimozione dell'annuncio");
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
if ((!isAdmin && data.open_contatti_at == null) || isAdmin) {
return (
<div className="flex w-full max-w-fit flex-wrap justify-start gap-3 sm:justify-end">
{isAdmin && data.user_confirmed_at != null && <AdminLavoraConferma />}
{isAdmin && data.accettato_conferma_at != null && (
<>
<AdminContratto />
{data.gestionale_id && (
<Link
aria-label="Open Gestionale"
href={`https://win-jk822fggr6b:480/id/${data.gestionale_id}`}
target="_blank"
>
<Button className="flex items-center gap-2">
<FolderKanban className="size-5" /> Gestionale
</Button>
</Link>
)}
</>
)}
{isAdmin && (
<>
<Link
href={`/area-riservata/admin/edit-annuncio/${annuncioId}`}
target="_blank"
>
<Button
aria-label="Edit Annuncio"
variant="secondary"
className="flex w-full items-center gap-2"
size="sm"
>
<Wrench className="size-6" />
</Button>
</Link>
2025-08-28 18:27:07 +02:00
<Confirm
title="Elimina annuncio"
description="Sei sicuro di voler eliminare l'annuncio?"
onConfirm={() => {
mutate({
servizioId,
annuncioId,
});
}}
>
<Button
aria-label="Delete Annuncio"
variant="destructive"
className="flex items-center gap-2"
size="sm"
>
<Trash2 className="size-6" />
</Button>
</Confirm>
</>
)}
</div>
);
}
return null;
2025-08-04 17:45:44 +02:00
};
const ContrattoImportSchema = z.object({
2025-08-28 18:27:07 +02:00
gestionale_id: z.number().nullable(),
contratto_decorrenza: z.string().nullable(),
contratto_scadenza: z.string().nullable(),
contratto_tipo: z
.enum([
"Transitorio",
"Agevolato",
"4 + 4",
"30 Giorni",
"Transi.Age",
"Foresteria",
"Commerciale",
])
.nullable(),
2025-08-04 17:45:44 +02:00
});
interface ContrattoImportObj
2025-08-28 18:27:07 +02:00
extends Omit<
z.infer<typeof ContrattoImportSchema>,
"contratto_decorrenza" | "contratto_scadenza"
> {
contratto_decorrenza: Date | null;
contratto_scadenza: Date | null;
}
2025-08-04 17:45:44 +02:00
const parseContrattoImport = (ingest: string) => {
2025-08-28 18:27:07 +02:00
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 = ContrattoImportSchema.safeParse(parsedJson);
if (parsed.success) {
return {
...parsed.data,
contratto_decorrenza: parsed.data.contratto_decorrenza
? new Date(parsed.data.contratto_decorrenza)
: null,
contratto_scadenza: parsed.data.contratto_scadenza
? new Date(parsed.data.contratto_scadenza)
: null,
};
} else {
console.error("Invalid format:", parsed.error);
return undefined;
}
2025-08-04 17:45:44 +02:00
};
const ContrattoImport = ({
2025-08-28 18:27:07 +02:00
handleUpdate,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
handleUpdate: (upd: ServizioAnnunciUpdate) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { data } = useServizioAnnuncio();
const [contratto, setContratto] = useState<ContrattoImportObj | undefined>({
gestionale_id: data.gestionale_id,
contratto_decorrenza: data.contratto_decorrenza
? new Date(data.contratto_decorrenza)
: null,
contratto_scadenza: data.contratto_scadenza
? new Date(data.contratto_scadenza)
: null,
contratto_tipo: data.contratto_tipo as ContrattoImportObj["contratto_tipo"],
});
const handleSave = () => {
handleUpdate({
...contratto,
});
};
return (
<Card>
<CardHeader>
<CardTitle>Dati Contratto</CardTitle>
<CardDescription className="sr-only">desc</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-3">
<Button
aria-label="Import Contratto"
onClick={async () => {
const clipboardText = await navigator.clipboard.readText();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
setContratto(parseContrattoImport(clipboardText));
}}
className="flex items-center gap-2"
variant="info"
>
<ClipboardPaste className="size-4" />
Clicca qui per importare la stringa dal gestionale
</Button>
<Button
aria-label="Reset Contratto"
onClick={async () => {
setContratto({
gestionale_id: null,
contratto_decorrenza: null,
contratto_scadenza: null,
contratto_tipo: null,
});
}}
className="flex items-center gap-2"
variant="destructive"
>
<Trash2 className="size-4" />
Reset
</Button>
</div>
{contratto ? (
<div className="flex flex-col gap-2">
<p>
<span className="font-semibold">ID Gestionale: </span>
{contratto.gestionale_id}{" "}
<Button
aria-label="Edit Gestionale ID"
onClick={() => {
const newValue = prompt(
"Modifica l'ID gestionale del contratto:",
contratto.gestionale_id?.toString() || "",
);
if (newValue !== null) {
setContratto((prev) =>
prev
? { ...prev, gestionale_id: parseInt(newValue) }
: undefined,
);
}
}}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Decorrenza: </span>
{contratto.contratto_decorrenza &&
new Date(contratto.contratto_decorrenza).toLocaleDateString(
"it-IT",
)}{" "}
<Button
aria-label="Edit Decorrenza"
onClick={() => {
const newValue = prompt(
"Modifica la decorrenza del contratto (DD/MM/YYYY):",
contratto.contratto_decorrenza
? new Date(
contratto.contratto_decorrenza,
).toLocaleDateString("it-IT")
: "",
);
if (newValue !== null) {
setContratto((prev) =>
prev
? {
...prev,
contratto_decorrenza: new Date(newValue),
}
: undefined,
);
}
}}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Scadenza: </span>
{contratto.contratto_scadenza &&
new Date(contratto.contratto_scadenza).toLocaleDateString(
"it-IT",
)}{" "}
<Button
aria-label="Edit Scadenza"
onClick={() => {
const newValue = prompt(
"Modifica la scadenza del contratto (DD/MM/YYYY):",
contratto.contratto_scadenza
? new Date(
contratto.contratto_scadenza,
).toLocaleDateString("it-IT")
: "",
);
if (newValue !== null) {
setContratto((prev) =>
prev
? {
...prev,
contratto_scadenza: new Date(newValue),
}
: undefined,
);
}
}}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Tipo di contratto: </span>
{contratto.contratto_tipo}{" "}
<Button
aria-label="Edit Tipo di contratto"
onClick={() => {
const newValue = prompt(
"Modifica il tipo di contratto:",
contratto.contratto_tipo || "",
);
if (
newValue !== null &&
ContrattoImportSchema.shape.contratto_tipo.safeParse(
newValue,
).success
) {
setContratto((prev) =>
prev
? {
...prev,
contratto_tipo:
newValue as ContrattoImportObj["contratto_tipo"],
}
: undefined,
);
}
}}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm"
>
<Pen />
</Button>
</p>
</div>
) : (
<p>Inserisci la stringa dal gestionale e elabora</p>
)}
</CardContent>
<CardFooter>
<Button
aria-label="Save Contratto"
onClick={() => {
handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
2025-08-04 17:45:44 +02:00
};
const AdminContratto = () => {
2025-08-28 18:27:07 +02:00
const { userId, servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const [open, setOpen] = useState(false);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const utils = api.useUtils();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { mutate: updateServizio } =
api.servizio.updateServizioAnnunci.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getServizio.invalidate({
servizioId,
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
toast.success("Dati salvati con successo");
},
onError: (error) => {
toast.error(error.message);
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const handleUpdate = (upd: ServizioAnnunciUpdate) => {
updateServizio({
servizioId,
annuncioId,
data: {
...upd,
},
});
};
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<Credenza open={open} onOpenChange={setOpen}>
<CredenzaTrigger asChild>
<Button
variant={data.doc_contratto_added ? "success" : "secondary"}
className="flex w-fit items-center gap-2"
aria-label="Open Contratto Section"
>
{data.doc_contratto_added ? (
<CircleCheck className="size-6" />
) : (
<Wrench className="size-6" />
)}
Contratto
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] max-w-4xl px-3 sm:max-w-5xl">
<CredenzaHeader>
<CredenzaTitle>Sezione Contratto</CredenzaTitle>
<CredenzaDescription className="sr-only">
Contratto modal
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<div className="flex flex-col gap-8">
<ContrattoSection handleUpdate={handleUpdate} />
<Separator />
<ContrattoImport handleUpdate={handleUpdate} />
<Separator />
<RicevutaSection handleUpdate={handleUpdate} />
</div>
</CredenzaBody>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline" aria-label="Close Credenza">
Chiudi
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
2025-08-04 17:45:44 +02:00
};
const ContrattoSection = ({
2025-08-28 18:27:07 +02:00
handleUpdate,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
handleUpdate: (upd: ServizioAnnunciUpdate) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { userId } = useServizio();
const { data } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<StorageindexId | null>(
data.doc_contratto_ref,
);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const utils = api.useUtils();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { data: files, isLoading } = api.storage.getAll.useQuery();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onSuccess: async () => {
await utils.storage.getUserStorage.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const handleSave = () => {
handleUpdate({
doc_contratto_ref: selectedDoc,
doc_contratto_added: selectedDoc != null,
});
if (selectedDoc) {
setUserStorage({
userId,
fileId: selectedDoc,
});
}
};
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
if (isLoading) return <LoadingPage />;
if (files === undefined) {
return <p>Errore nel caricamento dei documenti</p>;
}
return (
<Card>
<CardHeader>
<CardTitle>Documento del Contratto</CardTitle>
<CardDescription>contratto di locazione</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{selectedDoc && (
<div className="flex flex-wrap items-center gap-3">
<DocInfo storageId={selectedDoc}>
<Button
aria-label="Reset Contratto"
variant="destructive"
className="flex items-center gap-3"
onClick={() => {
handleUpdate({
doc_contratto_ref: null,
doc_contratto_added: false,
});
setSelectedDoc(null);
}}
>
<Trash2 className="size-6" />
</Button>
</DocInfo>
</div>
)}
<DocCombo
files={files}
onSelect={(f) => {
setSelectedDoc(f.id);
}}
current={selectedDoc}
/>
<UploadModal isAdmin userId={userId} />
</CardContent>
<CardFooter>
<Button
aria-label="Save Contratto"
onClick={() => {
handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
2025-08-04 17:45:44 +02:00
};
const RicevutaSection = ({
2025-08-28 18:27:07 +02:00
handleUpdate,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
handleUpdate: (upd: ServizioAnnunciUpdate) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { userId } = useServizio();
const { data } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<StorageindexId | null>(
data.doc_registrazione_ref,
);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const utils = api.useUtils();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { data: files, isLoading } = api.storage.getAll.useQuery();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onSuccess: async () => {
await utils.storage.getUserStorage.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const handleSave = () => {
handleUpdate({
doc_registrazione_ref: selectedDoc,
doc_registrazione_added: selectedDoc != null,
});
if (selectedDoc) {
setUserStorage({
userId,
fileId: selectedDoc,
});
}
};
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
if (isLoading) return <LoadingPage />;
if (files === undefined) {
return <p>Errore nel caricamento dei documenti</p>;
}
return (
<Card>
<CardHeader>
<CardTitle>Ricevuta di Registrazione</CardTitle>
<CardDescription className="sr-only">desc</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{selectedDoc && (
<div className="flex flex-wrap items-center gap-3">
<DocInfo storageId={selectedDoc}>
<Button
aria-label="Reset Ricevuta"
variant="destructive"
className="flex items-center gap-3"
onClick={() => {
handleUpdate({
doc_registrazione_ref: null,
doc_registrazione_added: false,
});
setSelectedDoc(null);
}}
>
<Trash2 className="size-6" />
</Button>
</DocInfo>
</div>
)}
<DocCombo
files={files}
onSelect={(f) => {
setSelectedDoc(f.id);
}}
current={selectedDoc}
/>
<UploadModal isAdmin userId={userId} />
</CardContent>
<CardFooter>
<Button
aria-label="Save Ricevuta"
onClick={() => {
handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
2025-08-04 17:45:44 +02:00
};
const AdminLavoraConferma = () => {
2025-08-28 18:27:07 +02:00
const { userId, servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const [open, setOpen] = useState(false);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const utils = api.useUtils();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { mutate: updateServizio } =
api.servizio.updateServizioAnnunci.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getServizio.invalidate({
servizioId,
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
toast.success("Dati salvati con successo");
},
onError: (error) => {
toast.error(error.message);
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const handleUpdate = (upd: ServizioAnnunciUpdate) => {
updateServizio({
servizioId,
annuncioId,
data: {
...upd,
},
});
};
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<Credenza open={open} onOpenChange={setOpen}>
<CredenzaTrigger asChild>
<Button
variant={data.hasConfermaAdmin ? "success" : "secondary"}
className="flex w-fit items-center gap-2"
aria-label="Open Lavora Conferma Section"
>
{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 handleUpdate={handleUpdate} />
<Separator />
<Caparra handleUpdate={handleUpdate} />
<Separator />
<SendConferma handleUpdate={handleUpdate} />
</div>
</CredenzaBody>
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline" aria-label="Close Credenza">
Chiudi
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
2025-08-04 17:45:44 +02:00
};
const DocSection = ({
2025-08-28 18:27:07 +02:00
handleUpdate,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
handleUpdate: (upd: ServizioAnnunciUpdate) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { userId } = useServizio();
const { data } = useServizioAnnuncio();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const utils = api.useUtils();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { data: files, isLoading } = api.storage.getAll.useQuery();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const { mutate: setUserStorage } = api.storage.addUserStorage.useMutation({
onSuccess: async () => {
await utils.storage.getUserStorage.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
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"
variant="destructive"
className="flex items-center gap-3"
onClick={() => {
handleUpdate({
doc_conferma_ref: null,
doc_conferma_added: false,
});
}}
>
<Trash2 />
</Button>
</DocInfo>
</div>
)}
<div className="flex flex-wrap items-center gap-3">
<DocCombo
files={files}
onSelect={(f) => {
handleUpdate({
doc_conferma_ref: f.id,
doc_conferma_added: true,
});
2025-08-28 18:27:07 +02:00
setUserStorage({
userId,
fileId: f.id,
});
}}
current={data.doc_conferma_ref}
/>
<UploadModal isAdmin userId={userId} />
</div>
</CardContent>
</Card>
);
2025-08-04 17:45:44 +02:00
};
const DocInfo = ({
2025-08-28 18:27:07 +02:00
storageId,
children,
}: {
2025-08-28 18:27:07 +02:00
storageId: StorageindexId;
children: React.ReactNode;
}) => {
2025-08-28 18:27:07 +02:00
const { data: file_info } = api.storage.getStorageFromId.useQuery({
storageId,
});
if (!file_info) return null;
return (
<div className="flex w-full flex-col gap-2">
<Label htmlFor="selected-file">File Selezionato: </Label>
<div className="flex w-full items-center gap-2 sm:max-w-[30rem]">
<div className="flex w-full flex-1">
<Link
aria-label="Open Selected File"
href={`/area-riservata/allegato-view/${storageId}`}
target="_blank"
className={cn(
buttonVariants({
variant: "outline",
}),
"flex w-fit grow items-center gap-2",
)}
id="selected-file"
>
{file_info.ext && <ExtIcon ext={file_info.ext} />}
<span className="max-w-40 truncate sm:max-w-80">
{file_info.filename}
</span>
<ExternalLink className="size-4" />
</Link>
</div>
{children}
</div>
</div>
);
2025-08-04 17:45:44 +02:00
};
const DocCombo = ({
2025-08-28 18:27:07 +02:00
files,
onSelect,
current,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
files: Storageindex[];
onSelect: (file: Storageindex) => void;
current: StorageindexId | null;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const [open, setOpen] = useState(false);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
aria-label="Select Existing File"
variant="outline"
role="combobox"
aria-expanded={open}
className="w-auto justify-between"
>
Seleziona file esistente...
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Command>
<CommandInput placeholder="Cerca Documento..." />
<CommandList>
<CommandEmpty>Nessun file.</CommandEmpty>
<CommandGroup>
{files.map((file) => (
<CommandItem
key={file.id}
value={file.id}
onSelect={(currentValue) => {
if (currentValue !== current) {
// biome-ignore lint/style/noNonNullAssertion: <exists>
onSelect(files.find((f) => f.id === currentValue)!);
}
setOpen(false);
}}
>
<span className="flex items-center gap-2">
{file.ext && <ExtIcon ext={file.ext} />}
<span className="block font-medium">{file.filename}</span>
{current === file.id && (
<Check className="text-accent-foreground size-4" />
)}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
2025-08-04 17:45:44 +02:00
};
const CaparraSchema = z.object({
2025-08-28 18:27:07 +02:00
caparra_causale: z.string().nullable(),
caparra_iban: z.string().nullable(),
caparra_importo: z.string().nullable(),
caparra_intestazione: z.string().nullable(),
2025-08-04 17:45:44 +02:00
});
type CaparraObj = z.infer<typeof CaparraSchema>;
const parseCaparra = (ingest: string) => {
2025-08-28 18:27:07 +02:00
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;
} else {
console.error("Invalid caparra format:", parsed.error);
return undefined;
}
2025-08-04 17:45:44 +02:00
};
const Caparra = ({
2025-08-28 18:27:07 +02:00
handleUpdate,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
handleUpdate: (upd: ServizioAnnunciUpdate) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { data } = 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 = () => {
handleUpdate({
...caparra,
});
};
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();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
setCaparra(parseCaparra(clipboardText));
}}
className="flex items-center gap-2"
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,
});
}}
className="flex items-center gap-2"
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"
onClick={() => {
const newValue = prompt(
"Modifica l'intestazione della caparra:",
caparra.caparra_intestazione || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev
? { ...prev, caparra_intestazione: newValue }
: undefined,
);
}
}}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">IBAN: </span>
{caparra.caparra_iban}{" "}
<Button
aria-label="Edit IBAN"
onClick={() => {
const newValue = prompt(
"Modifica l'IBAN della caparra:",
caparra.caparra_iban || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev ? { ...prev, caparra_iban: newValue } : undefined,
);
}
}}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Importo: </span>
{caparra.caparra_importo}{" "}
<Button
aria-label="Edit Importo"
onClick={() => {
const newValue = prompt(
"Modifica l'importo della caparra:",
caparra.caparra_importo || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev ? { ...prev, caparra_importo: newValue } : undefined,
);
}
}}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Causale: </span>
{caparra.caparra_causale}{" "}
<Button
aria-label="Edit Causale"
onClick={() => {
const newValue = prompt(
"Modifica la causale della caparra:",
caparra.caparra_causale || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev ? { ...prev, caparra_causale: newValue } : undefined,
);
}
}}
className="h-6 px-1.5 [&_svg]:size-4"
size="sm"
>
<Pen />
</Button>
</p>
</div>
) : (
<p>Inserisci la stringa dal gestionale e elabora</p>
)}
</CardContent>
<CardFooter>
<Button
aria-label="Save Caparra"
onClick={() => {
handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
2025-08-04 17:45:44 +02:00
};
const SendConferma = ({
2025-08-28 18:27:07 +02:00
handleUpdate,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
handleUpdate: (upd: ServizioAnnunciUpdate) => void;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { servizio } = useServizio();
const { data } = useServizioAnnuncio();
const { t } = useTranslation();
const [check, setCheck] = useState(data.hasConfermaAdmin);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
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 (da preferenze):{" "}
{servizio.permanenza
? t.preferenze.permanenza[servizio.permanenza]
: "non inserita"}
</p>
<p>
Permanenza (da descrizione): &quot;
{(() => {
const match = data.desc_it?.match(/Permanenza:.*$/m);
return match ? match[0] : "";
})()}
&quot;
</p>
</>
);
} else {
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"
id="conferma"
checked={check}
onCheckedChange={(c) => {
setCheck(c);
}}
/>
<Label htmlFor="conferma">Conferma lavorata</Label>
</div>
</CardContent>
<CardFooter>
<Button
aria-label="Save Conferma Status"
onClick={() => {
handleUpdate({
hasConfermaAdmin: check,
});
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
2025-08-04 17:45:44 +02:00
};
export const AddButton = ({ annuncioId }: { annuncioId: AnnunciId }) => {
2025-08-28 18:27:07 +02:00
const { servizioId, userId } = useServizio();
const utils = api.useUtils();
const { mutate } = api.servizio.addServizioAnnunci.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
await utils.servizio.getAnnunciAvailableToAdd.invalidate({
servizioId,
});
await utils.servizio.AnnuncioServizioTipologiaMatch.invalidate({
userId,
});
await utils.servizio.getCompatibileAnnunci.invalidate({
servizioId,
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
toast.success("Annuncio aggiunto alla ricerca");
},
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiunta dell'annuncio");
},
});
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const buttonRef = useRef<AnimatedButtonRef>(null);
return (
<AnimatedButton
ref={buttonRef}
sparklesIcon="thumbs-up"
sparklesNumber={15}
sparklesRadius={60}
sparklesClassName="fill-yellow-500 stroke-transparent"
sparklesScale={[1, 1.5]}
onClick={async () => {
buttonRef.current?.sparkle();
//wait for the animation to finish
await new Promise((resolve) => setTimeout(resolve, 1000));
mutate({ servizioId, annunci: [annuncioId] });
}}
variant="info"
className="group flex w-full gap-2"
>
<Plus className="size-5" />
<span>Aggiungi agli annunci da contattare</span>
</AnimatedButton>
);
2025-08-04 17:45:44 +02:00
};