refactor: update confirmation handling in service announcements

- Changed the confirmation status checks from `accettato_conferma_at` to `status_conferma` in various components and controllers.
- Introduced a new `StatusConfermaEnum` to standardize confirmation statuses.
- Removed deprecated confirmation-related fields from the `ServizioAnnunci` schema.
- Deleted unused confirmation-related API methods and adjusted related logic in the service controller.
- Updated the search parameters schema to use the new confirmation status.
- Removed the `conferma-immobile` and `riapri-conferma` pages as they are no longer needed.
This commit is contained in:
Marco Pedone 2026-04-09 16:17:24 +02:00
parent dc28e42baa
commit ed7359aa16
17 changed files with 185 additions and 2010 deletions

View file

@ -0,0 +1,53 @@
DO $$ BEGIN
CREATE TYPE public."StatusConfermaEnum" AS ENUM (
'Inviato conferma',
'Conferma accettata',
'Caparra versata'
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
ALTER TABLE IF EXISTS public.servizio_annunci
ADD COLUMN IF NOT EXISTS status_conferma public."StatusConfermaEnum";
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'servizio_annunci'
AND column_name = 'hasConfermaAdmin'
) THEN
UPDATE public.servizio_annunci
SET status_conferma = 'Caparra versata'
WHERE "hasConfermaAdmin" = true;
END IF;
END $$;
ALTER TABLE IF EXISTS public.servizio_annunci
DROP COLUMN IF EXISTS doc_conferma_ref;
ALTER TABLE IF EXISTS public.servizio_annunci
DROP COLUMN IF EXISTS user_confirmed_at;
ALTER TABLE IF EXISTS public.servizio_annunci
DROP COLUMN IF EXISTS accettato_conferma_at;
ALTER TABLE IF EXISTS public.servizio_annunci
DROP COLUMN IF EXISTS caparra_causale;
ALTER TABLE IF EXISTS public.servizio_annunci
DROP COLUMN IF EXISTS caparra_iban;
ALTER TABLE IF EXISTS public.servizio_annunci
DROP COLUMN IF EXISTS caparra_importo;
ALTER TABLE IF EXISTS public.servizio_annunci
DROP COLUMN IF EXISTS caparra_intestazione;
ALTER TABLE IF EXISTS public.servizio_annunci
DROP COLUMN IF EXISTS "hasConfermaAdmin";
ALTER TABLE IF EXISTS public.servizio_annunci
DROP COLUMN IF EXISTS doc_conferma_added;

View file

@ -1,893 +0,0 @@
import { format } from "date-fns";
import {
CircleAlert,
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 { 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 } = 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.invalidate();
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 initialDoc = data.doc_conferma_ref;
const [selectedDoc, setSelectedDoc] = useState<string | null>(
data.doc_conferma_ref,
);
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();
},
});
const handleSave = async () => {
await updateServizioAnnunci({
doc_conferma_added: selectedDoc != null,
doc_conferma_ref: selectedDoc,
});
if (selectedDoc) {
setUserStorage({
storageId: selectedDoc,
userId,
});
}
};
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">
{selectedDoc && (
<div className="flex flex-wrap items-center gap-3">
<DocInfo storageId={selectedDoc}>
<Button
aria-label="Reset Contratto"
className="flex items-center gap-3"
onClick={async () => {
await updateServizioAnnunci({
doc_conferma_added: false,
doc_conferma_ref: null,
});
setSelectedDoc(null);
}}
variant="destructive"
>
<Trash2 className="size-6" />
</Button>
</DocInfo>
</div>
)}
<DocCombo
current={selectedDoc}
files={files}
onSelect={(f) => {
setSelectedDoc(f.id);
}}
/>
<UploadModal
cb_onUpload={({ storageIds }) => {
if (storageIds.length === 1 && storageIds[0]) {
setSelectedDoc(storageIds[0]);
}
}}
isAdmin
userId={userId}
/>
</CardContent>
<CardFooter className="gap-4">
<Button
aria-label="Save Contratto"
disabled={selectedDoc === initialDoc}
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
{selectedDoc !== initialDoc ? (
<div className="flex items-center gap-2">
<CircleAlert className="size-5 text-orange-500" />
<span className="text-orange-500">Salva modifiche</span>
</div>
) : (
initialDoc &&
selectedDoc === initialDoc && (
<div className="flex items-center gap-2">
<CircleCheck className="size-5 text-green-500" />
<span className="text-green-500">Dati salvati</span>
</div>
)
)}
</CardFooter>
</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 initialCaparra = {
caparra_causale: data.caparra_causale,
caparra_iban: data.caparra_iban,
caparra_importo: data.caparra_importo,
caparra_intestazione: data.caparra_intestazione,
};
const [caparra, setCaparra] = useState<CaparraObj | undefined>(
initialCaparra,
);
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 className="gap-4">
<Button
aria-label="Save Caparra"
disabled={JSON.stringify(caparra) === JSON.stringify(initialCaparra)}
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
{JSON.stringify(caparra) !== JSON.stringify(initialCaparra) ? (
<div className="flex items-center gap-2">
<CircleAlert className="size-5 text-orange-500" />
<span className="text-orange-500">Salva modifiche</span>
</div>
) : (
Object.values(initialCaparra).every((v) => v !== null) && (
<div className="flex items-center gap-2">
<CircleCheck className="size-5 text-green-500" />
<span className="text-green-500">Dati salvati</span>
</div>
)
)}
</CardFooter>
</Card>
);
};
const MotivazioneSection = () => {
const { servizio } = useServizio();
const { t } = useTranslation();
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 } = 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.invalidate();
},
});
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 !== null
? `${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 !== null
? {
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 } = 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.invalidate();
},
});
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 } = 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.invalidate();
},
});
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.invalidate();
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>
)}
<p className="font-semibold text-lg">
ATTENZIONE: Il saldo verrà aggiunto (se necessario), in base ai
parametri, all'invio della conferma
</p>
<div className="mt-4 flex items-center gap-3">
<Button
aria-label="Send Conferma"
disabled={!docOk || !caparraOk || data.hasConfermaAdmin}
onClick={() => {
handleUpdate({
servizioId: servizio.servizio_id,
annuncioId: data.id,
status: true,
});
}}
size="lg"
variant="orange"
>
<CircleAlert className="size-5" />
<span>Invia la conferma</span>
</Button>
</div>
</div>
</CardContent>
</Card>
);
};

View file

@ -1,12 +1,14 @@
import { import {
Circle,
CircleCheck,
FileBadge, FileBadge,
FileText, FileText,
FolderKanban, FolderKanban,
Info, Info,
PackageCheck, PackageCheck,
Pointer,
Printer, Printer,
Trash2, Trash2,
Workflow,
Wrench, Wrench,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@ -22,6 +24,14 @@ import {
AlertDialogTrigger, AlertDialogTrigger,
} from "~/components/ui/alert-dialog"; } from "~/components/ui/alert-dialog";
import { Button, type ButtonConfigs } from "~/components/ui/button"; import { Button, type ButtonConfigs } from "~/components/ui/button";
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "~/components/ui/popover";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -33,11 +43,10 @@ import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider"; import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci"; import type { AnnunciId } from "~/schemas/public/Annunci";
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import StatusConfermaEnum from "~/schemas/public/StatusConfermaEnum";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { Confirm } from "../confirm"; import { Confirm } from "../confirm";
import { AdminLavoraConferma } from "./admin_conferma";
import { AdminContratto } from "./admin_contratto"; import { AdminContratto } from "./admin_contratto";
import { ConfermaAnnuncioModal, ConfermaInLavorazioneModal } from "./conferma";
import { ContattiProprietarioModal } from "./modale_contatti"; import { ContattiProprietarioModal } from "./modale_contatti";
export const AdminActions = () => { export const AdminActions = () => {
@ -62,8 +71,9 @@ export const AdminActions = () => {
<h3 className="ml-1 font-semibold">Azioni Admin:</h3> <h3 className="ml-1 font-semibold">Azioni Admin:</h3>
<div className="flex w-full max-w-fit flex-wrap justify-start gap-3"> <div className="flex w-full max-w-fit flex-wrap justify-start gap-3">
<AdminAnnuncioContatti /> <AdminAnnuncioContatti />
{data.user_confirmed_at != null && <AdminLavoraConferma />} <AdminAnnuncioConfermaPopover />
{data.accettato_conferma_at != null && (
{data.status_conferma != null && (
<> <>
<AdminContratto /> <AdminContratto />
{data.gestionale_id && ( {data.gestionale_id && (
@ -134,86 +144,79 @@ export const AdminActions = () => {
}; };
export const UserActions = () => { export const UserActions = () => {
const { servizioId } = useServizio(); const { data } = useServizioAnnuncio();
const { data, annuncioId } = useServizioAnnuncio();
const { locale } = useTranslation();
const ContattiSbloccati = data.open_contatti_at !== null;
const ConfermaUtente = data.user_confirmed_at !== null;
const ConfermaLavorata = data.doc_conferma_added && data.hasConfermaAdmin;
const AccettatoConferma = data.accettato_conferma_at !== null;
return ( return (
<> <>
<div className="flex w-full flex-col flex-wrap gap-2 sm:flex-row"> <div className="flex w-full flex-col flex-wrap gap-2 sm:flex-row">
<ContattiProprietarioModal /> <ContattiProprietarioModal />
{ConfermaUtente
? !data.hasConfermaAdmin && <ConfermaInLavorazioneModal />
: ContattiSbloccati && <ConfermaAnnuncioModal />}
{AccettatoConferma ? (
<Link
aria-label={
locale === "it" ? "Rivedi la conferma" : "Review the confirmation"
}
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
>
<Button className="w-full sm:w-fit" variant="success">
<Pointer />
{locale === "it"
? "Rivedi la conferma"
: "Review the confirmation"}
</Button>
</Link>
) : (
ConfermaLavorata &&
(data.doc_conferma_ref ? (
<Link
aria-label={locale === "it" ? "Confermato" : "Confirmed"}
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
>
<Button className="w-full sm:w-fit" variant="success">
<Pointer />
{locale === "it"
? "Confermato - Procedi ora"
: "Confirmed - Proceed now"}
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="w-full cursor-not-allowed opacity-50 sm:w-fit"
variant="success"
>
<Pointer />
{locale === "it"
? "Confermato - Procedi ora"
: "Confirmed - Proceed now"}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{locale === "it"
? "Il documento non è più disponibile"
: "The document is no longer available"}
</p>
<p>
{locale === "it"
? "Se hai bisogno di una copia, contattaci via email o chat."
: "If you need a copy, contact us via email or chat."}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))
)}
</div> </div>
{data.status_conferma != null && <PostConfermaSection />}
{data.accettato_conferma_at != null && <PostConfermaSection />}
</> </>
); );
}; };
const AdminAnnuncioConfermaPopover = () => {
const { servizioId } = useServizio();
const { data } = useServizioAnnuncio();
const utils = api.useUtils();
const { mutate } = api.servizio.updateServizioAnnunci.useMutation({
onSuccess: async () => {
await utils.servizio.invalidate();
},
onError: (error) => {
console.error(error);
toast.error("Errore durante l'aggiornamento dell'annuncio");
},
});
const handleStatusChange = (status: StatusConfermaEnum | null) => {
mutate({
annuncioId: data.id,
servizioId,
data: { status_conferma: status },
});
};
return (
<Popover>
<PopoverTrigger asChild>
<Button size="sm" variant="orange">
<Workflow /> {data.status_conferma || "Non in conferma"}
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader>
<PopoverTitle>Status Conferma:</PopoverTitle>
<PopoverDescription className="sr-only">
Description
</PopoverDescription>
</PopoverHeader>
<div className="flex flex-col divide-y rounded-md border border-border">
<Button
className="justify-start rounded-b-none"
onClick={() => handleStatusChange(null)}
variant={data.status_conferma === null ? "default" : "outline"}
>
{data.status_conferma === null ? <CircleCheck /> : <Circle />}
Non in conferma
</Button>
{Object.values(StatusConfermaEnum).map((status) => (
<Button
className="justify-start rounded-none last:rounded-b-md"
key={status}
onClick={() => handleStatusChange(status)}
variant={data.status_conferma === status ? "default" : "outline"}
>
{data.status_conferma === status ? <CircleCheck /> : <Circle />}
{status}
</Button>
))}
</div>
</PopoverContent>
</Popover>
);
};
const AdminAnnuncioContatti = () => { const AdminAnnuncioContatti = () => {
const { data } = useServizioAnnuncio(); const { data } = useServizioAnnuncio();
const { data: annuncio, isLoading } = api.annunci.getAnnuncioFull.useQuery({ const { data: annuncio, isLoading } = api.annunci.getAnnuncioFull.useQuery({

View file

@ -1,268 +0,0 @@
import { CircleCheck, Handshake } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import toast from "react-hot-toast";
import { AllegatoIframe } from "~/components/allegato-iframe";
import { LoadingPage } from "~/components/loading";
import { getStorageUrl } from "~/lib/storage_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 { DocumentiUploadSection } from "../area-riservata/documenti_personali";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza";
import LoadingButton from "../custom_ui/loading-button";
import { Button } from "../ui/button";
export const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
storageId,
});
const { t } = useTranslation();
if (isLoading) return <LoadingPage />;
if (!fileInfos) return <p>{t.file_section.error}</p>;
return (
<>
<AllegatoIframe
allegato={fileInfos}
className="h-full max-h-[70vh] min-h-[60vh]"
/>
<Link
className="underline underline-offset-1 after:content-['_↗']"
href={getStorageUrl({
storageId: fileInfos.id,
params: { mode: "download" },
})}
>
{t.file_section.download}
</Link>
</>
);
};
export const ConfermaAnnuncioModal = () => {
const { servizio } = useServizio();
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const needsDocuments =
!servizio.skipDocMotivazione &&
!servizio.doc_motivazione &&
servizio.tipologia === TipologiaPosizioneEnum.Transitorio;
return (
<>
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild>
<Button aria-label="Conferma immobile" className="w-full sm:w-fit">
<Handshake /> {t.richieste.cta}
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle> {t.richieste.conf_title}</CredenzaTitle>
<CredenzaDescription className="sr-only">
{t.richieste.conf_desc}
</CredenzaDescription>
</CredenzaHeader>
{needsDocuments ? (
<DocCheckpoint />
) : (
<InvioConferma closeModal={() => setOpen(false)} />
)}
</CredenzaContent>
</Credenza>
</>
);
};
const InvioConferma = ({ closeModal }: { closeModal: () => void }) => {
const { servizioId } = useServizio();
const { annuncioId } = useServizioAnnuncio();
const { t } = useTranslation();
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.userSendConfermaIntent.useMutation(
{
onError: (err) => {
toast.error(err.message, { id: "updateRichiestaToast" });
},
onMutate: () => {
toast.loading("Conferma in corso", { id: "updateRichiestaToast" });
},
onSuccess: async () => {
toast.success("Richiesta confermata con successo", {
id: "updateRichiestaToast",
});
await utils.servizio.invalidate();
closeModal();
},
},
);
return (
<>
<CredenzaBody className="flex flex-col gap-4">
<p className="text-center font-semibold">
Hai fatto la visita? Se no contatta il proprietario!
</p>
<p>{t.richieste.conferma_txt}</p>
<p className="text-sm">{t.richieste.conf_alert}</p>
</CredenzaBody>
<CredenzaFooter>
<LoadingButton
aria-label="Conferma immobile"
loading={isPending}
onClick={() => {
mutate({
annuncioId,
servizioId,
});
}}
variant="info"
>
{t.procedi}
</LoadingButton>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza">{t.chiudi}</Button>
</CredenzaClose>
</CredenzaFooter>
</>
);
};
const DocCheckpoint = () => {
const { t, locale } = useTranslation();
const { userId, servizio, isAdmin } = useServizio();
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.updateServizio.useMutation({
onSuccess: async () => {
await utils.servizio.invalidate();
},
onError: (err) => {
toast.error(`Errore aggiornamento servizio: ${err.message}`);
},
});
const handleUpload = (id: UsersStorageUserStorageId) => {
mutate({
servizioId: servizio.servizio_id,
data: { doc_motivazione_ref: id },
});
};
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<div className="space-y-5">
<p>
{locale === "it"
? "Devi ancora caricare un tuo documento legato alla permanenza e motivazione di transitorietà"
: "You still need to upload a document related to your stay and reason for transience"}
</p>
<DocumentiUploadSection
caricatoNote
document={servizio.doc_motivazione}
isAdmin={isAdmin}
isPending={isPending}
onUpload={handleUpload}
title={locale === "it" ? "Documento:" : "Document:"}
userId={userId}
/>
<br />
<br />
<p className="text-muted-foreground text-sm">
{locale === "it"
? "I tuoi dati saranno conservati solamente per la durata del servizio, in conformità con la nostra "
: "Your data will be stored only for the duration of the service, in accordance with our "}
<Link className="underline" href="/privacy-policy" target="_blank">
{locale === "it" ? "Informativa sulla Privacy" : "Privacy Policy"}
</Link>
</p>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi" disabled={isPending} variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</>
);
};
export const ConfermaInLavorazioneModal = () => {
const { t, locale } = useTranslation();
const { data } = useServizioAnnuncio();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button
aria-label="Conferma in lavorazione"
className="w-full sm:w-fit"
>
<CircleCheck />
{locale === "it"
? "Conferma in lavorazione"
: "Confirmation in progress"}
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle>
{locale === "it"
? "In attesa di conferma"
: "Awaiting confirmation"}
</CredenzaTitle>
<CredenzaDescription className="sr-only">
Consegna status modal
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="prose dark:prose-invert max-h-[80vh] overflow-auto pb-5">
<p>
{locale === "it"
? "Richiesta di conferma inviata il:"
: "Confirmation request sent on:"}{" "}
{data.user_confirmed_at &&
new Date(data.user_confirmed_at).toLocaleString("it", {
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
month: "2-digit",
year: "numeric",
})}
</p>
<p>
{locale === "it"
? "La conferma è ancora in lavorazione. Stiamo verificando con i proprietari le condizioni e dettagli della conferma per riferirteli. Attendi una comunicazione."
: "The confirmation is still in progress. We are checking with the owners the conditions and details of the confirmation to report them to you. Please wait for a communication."}
</p>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza" variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};

View file

@ -195,7 +195,7 @@ export const InterruzioneServizio = () => {
!servizio.isInterrotto && !servizio.isInterrotto &&
(isAdmin || (isAdmin ||
(isBefore(new Date(), fineInterruzione) && (isBefore(new Date(), fineInterruzione) &&
servizio.annunci.every((a) => a.accettato_conferma_at === null))); servizio.annunci.every((a) => a.status_conferma === null)));
return ( return (
<AlertDialog> <AlertDialog>

View file

@ -71,7 +71,7 @@ const Schema = z
pianoterra: z.boolean(), pianoterra: z.boolean(),
reddito: z.string().nullable(), reddito: z.string().nullable(),
terrazzo: z.boolean(), terrazzo: z.boolean(),
tipologia: z.custom<TipologiaPosizioneEnum>(), tipologia: z.enum(TipologiaPosizioneEnum),
scadenza_motivazione_transitoria: z.date().nullable(), scadenza_motivazione_transitoria: z.date().nullable(),
skipPayment: z.boolean(), skipPayment: z.boolean(),
skipControlloDoc: z.boolean(), skipControlloDoc: z.boolean(),
@ -832,7 +832,7 @@ export const EditServizioModal = () => {
disabled={ disabled={
!isAdmin && !isAdmin &&
(servizio.isInterrotto || (servizio.isInterrotto ||
servizio.annunci.some((a) => a.hasConfermaAdmin)) servizio.annunci.some((a) => a.status_conferma !== null))
} }
variant="secondary" variant="secondary"
> >

View file

@ -567,15 +567,13 @@ const Main = () => {
); );
} }
const annuncioConfermato = servizio.annunci.filter( const InConferma = servizio.annunci.filter((a) => a.status_conferma !== null);
(a) => a.accettato_conferma_at !== null,
);
// in conferma, mostra solo annunci confermati // in conferma, mostra solo annunci confermati
if (servizio.isOkSaldo && annuncioConfermato.length > 0) { if (servizio.isOkSaldo && InConferma.length > 0) {
return ( return (
<> <>
{annuncioConfermato.map((data) => { {InConferma.map((data) => {
return ( return (
<ServizioAnnuncioProvider <ServizioAnnuncioProvider
data={data} data={data}
@ -591,7 +589,7 @@ const Main = () => {
<h4 className="text-lg">Altri annunci:</h4> <h4 className="text-lg">Altri annunci:</h4>
<div className="@container grid grid-flow-row @sm:grid-cols-2 grid-cols-1 gap-4"> <div className="@container grid grid-flow-row @sm:grid-cols-2 grid-cols-1 gap-4">
{servizio.annunci {servizio.annunci
.filter((a) => !a.accettato_conferma_at) .filter((a) => a.status_conferma === null)
.map((data) => { .map((data) => {
return ( return (
<ServizioAnnuncioProvider <ServizioAnnuncioProvider
@ -633,28 +631,18 @@ const Main = () => {
// normal case // normal case
return ( return (
<> <>
{annuncioConfermato.length > 0 && ( {InConferma.length > 0 && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<PackageCheck className="size-6 text-primary" /> <PackageCheck className="size-6 text-primary" />
<span className="font-medium">{t.servizio.saldi_in_attesa}</span> <span className="font-medium">{t.servizio.saldi_in_attesa}</span>
</div> </div>
{annuncioConfermato.map((a) => ( {InConferma.map((a) => (
<div <div
className="flex items-center gap-2 rounded-md bg-sky-400 p-2" className="flex items-center gap-2 rounded-md bg-sky-400 p-2"
key={a.annunci_id} key={a.annunci_id}
> >
<h3> <h3 className="font-semibold text-white">{a.codice} </h3>
{a.codice} -{" "}
{a.accettato_conferma_at &&
new Date(a.accettato_conferma_at).toLocaleDateString("it", {
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
month: "2-digit",
year: "numeric",
})}
</h3>
<SaldoButton <SaldoButton
annuncioId={a.annunci_id} annuncioId={a.annunci_id}
className="border-none" className="border-none"

View file

@ -1,7 +1,6 @@
import { import {
Bug, Bug,
Check, Check,
ChevronDown,
Cog, Cog,
Euro, Euro,
ExternalLink, ExternalLink,
@ -369,101 +368,6 @@ const AddOrderDialogContent = ({ swapBack }: { swapBack: () => void }) => {
); );
}; };
export const ServizioPacksInfos = () => {
const { servizio } = useServizio();
const { t } = useTranslation();
const { data: packs } = api.servizio.getPacksSolution.useQuery({
servizioId: servizio.servizio_id,
});
if (!packs) return null;
return (
<Collapsible className="flex flex-col gap-2">
<CollapsibleTrigger asChild>
<Button
className="h-fit w-full flex-wrap items-center justify-start py-1 sm:w-fit sm:justify-center"
size="sm"
variant="outline"
>
<Euro className="size-5" />
<h3 className="font-semibold text-lg">{t.servizio.prezzi.title}</h3>
<ChevronDown className="ml-auto size-4 sm:ml-0" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="flex flex-col gap-2">
<span className="text-muted-foreground text-sm">
{t.servizio.prezzi.sub}
</span>
<div className="flex w-full flex-col gap-4 md:flex-row">
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
{packs.acconto.success ? (
<>
<span className="font-medium">
{packs.acconto.pack.nome_it}
</span>
<span className="text-muted-foreground text-sm">
{packs.acconto.pack.desc_it}
</span>
<span className="font-semibold text-lg">
{formatCurrency(packs.acconto.pack.prezzo_cent / 100)}
</span>
<span className="text-muted-foreground text-sm">
COD: {packs.acconto.pack.idprezziario}
</span>
</>
) : (
<span className="font-medium">{packs.acconto.message}</span>
)}
</div>
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
{packs.saldo.success ? (
<>
<span className="font-medium">{packs.saldo.pack.nome_it}</span>
<span className="text-muted-foreground text-sm">
{packs.saldo.pack.desc_it}
</span>
<span className="font-semibold text-lg">
{formatCurrency(packs.saldo.pack.prezzo_cent / 100)}
</span>
<span className="text-muted-foreground text-sm">
COD: {packs.saldo.pack.idprezziario}
</span>
</>
) : (
<span className="font-medium">{packs.saldo.message}</span>
)}
</div>
{servizio.tipologia === TipologiaPosizioneEnum.Stabile && (
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
<>
<span className="font-medium">
{t.servizio.prezzi.consulenza}
</span>
<span className="text-muted-foreground text-sm">
{t.servizio.prezzi.consulenza_desc}
</span>
<span className="font-semibold">
<Link
className="flex items-center gap-2 underline underline-offset-2"
href={"/prezzi#contratti"}
target="_blank"
>
<span>{t.servizio.prezzi.gotoprezzi}</span>
<ExternalLink className="size-4 stroke-foreground" />
</Link>
</span>
</>
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
);
};
const AdminActivationUfficio = () => { const AdminActivationUfficio = () => {
const { servizio } = useServizio(); const { servizio } = useServizio();
const utils = api.useUtils(); const utils = api.useUtils();

View file

@ -56,6 +56,7 @@ import { UserAvatar } from "~/components/user_avatar";
import { MutationPageRestore } from "~/lib/tableUtils"; import { MutationPageRestore } from "~/lib/tableUtils";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import StatusConfermaEnum from "~/schemas/public/StatusConfermaEnum";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { RichiesteData } from "~/server/controllers/servizio.controller"; import type { RichiesteData } from "~/server/controllers/servizio.controller";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@ -383,10 +384,10 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
} }
} }
const annuncioConfermato = row.original.annunci_servizio.filter( const annuncioCaparraVersata = row.original.annunci_servizio.filter(
(a) => a.accettato_conferma_at !== null, (a) => a.status_conferma === StatusConfermaEnum["Caparra versata"],
); );
if (annuncioConfermato.length > 0) { if (annuncioCaparraVersata.length > 0) {
if (row.original.isOkSaldo) { if (row.original.isOkSaldo) {
badgeClass = "border-blue-500 bg-blue-500/10 text-blue-500"; badgeClass = "border-blue-500 bg-blue-500/10 text-blue-500";
contents = ( contents = (
@ -407,9 +408,24 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
return; return;
} }
const annuncioConfermato = row.original.annunci_servizio.filter(
(a) =>
a.status_conferma === StatusConfermaEnum["Conferma accettata"],
);
if (annuncioConfermato.length > 0) {
badgeClass = "border-yellow-500 bg-yellow-500/10 text-yellow-600";
contents = (
<>
<ClockFading className="stroke-yellow-500 dark:stroke-yellow-400" />
Attesa di caparra
</>
);
return;
}
const annuncioAttesaConferma = row.original.annunci_servizio.filter( const annuncioAttesaConferma = row.original.annunci_servizio.filter(
(a) => a.accettato_conferma_at == null && a.doc_conferma_added, (a) => a.status_conferma === StatusConfermaEnum["Inviato conferma"],
); );
if (annuncioAttesaConferma.length > 0) { if (annuncioAttesaConferma.length > 0) {
badgeClass = "border-blue-500 bg-blue-500/10 text-blue-500"; badgeClass = "border-blue-500 bg-blue-500/10 text-blue-500";

View file

@ -1,219 +0,0 @@
import type { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import toast from "react-hot-toast";
import { DocNotFoundPage } from "~/components/doc_not_found";
import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { FileSection } from "~/components/servizio/conferma";
import { Status500 } from "~/components/status-page";
import { Button } from "~/components/ui/button";
import { Checkbox } from "~/components/ui/checkbox";
import { redirectTo500 } from "~/lib/utils";
import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider";
import { useEnforcedSession } from "~/providers/SessionProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
import { api } from "~/utils/api";
type ConfermaImmobileProps = {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
};
/**
* Pagina di conferma immobile: /servizio/conferma-immobile/[servizioId]/[annuncioId]
*/
const ConfermaImmobile: NextPageWithLayout<ConfermaImmobileProps> = ({
servizioId,
annuncioId,
}: ConfermaImmobileProps) => {
const { data, isLoading } = api.servizio.getServizioAnnuncio.useQuery({
annuncioId,
servizioId,
});
const { t } = useTranslation();
if (isLoading) return <LoadingPage />;
if (!data) return <Status500 />;
if (!data.doc_conferma_ref) return <DocNotFoundPage />;
return (
<div className="mx-2 flex flex-1 overflow-auto">
<div className="mx-auto flex grow sm:grow-0">
<div className="flex flex-col items-center space-y-4 py-4">
<h1 className="font-bold text-xl">{t.servizio.conferma.titolo}</h1>
<p className="text-center">{t.servizio.conferma.descrizione}</p>
<FileSection storageId={data.doc_conferma_ref} />
<div className="flex max-w-3xl flex-col gap-2 rounded-md bg-green-400 p-4 text-left">
<p className="font-semibold text-xl">{t.servizio.conferma.cta}</p>
<p>{t.servizio.conferma.coordinate}</p>
<p>
{t.servizio.conferma.intestazione}{" "}
{data.caparra_intestazione || ""}
</p>
<p>
{t.servizio.conferma.iban} {data.caparra_iban || ""}
</p>
<p>
{t.servizio.conferma.importo} {data.caparra_importo || ""}
</p>
<p>
{t.servizio.conferma.causale} {data.caparra_causale || ""}
</p>
<p className="font-semibold text-xl">
{t.servizio.conferma.coord_footer}
</p>
</div>
<ConfirmSection annuncioId={annuncioId} servizioId={servizioId} />
</div>
</div>
</div>
);
};
const ConfirmSection = ({
servizioId,
annuncioId,
}: {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
}) => {
const { t } = useTranslation();
const router = useRouter();
const [letto, setLetto] = useState(false);
const [accettato, setAccettato] = useState(false);
const { user } = useEnforcedSession();
const utils = api.useUtils();
const { mutate: conferma } =
api.servizio.userAcceptConfermaImmobile.useMutation({
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🔄",
});
return { toastId };
},
onSuccess: async (data, _variables, context) => {
await utils.storage.retrieveUserFileData.invalidate();
await utils.servizio.invalidate();
toast.success("Confermato", {
icon: "👍",
id: context?.toastId,
});
if (user.isAdmin) {
await router.push(
`/area-riservata/admin/user-view/servizio/${data.userId}/${servizioId}`,
);
} else {
await router.push(`/area-riservata/servizio/${servizioId}`);
}
},
});
return (
<>
<div className="flex flex-col items-start space-y-3 px-2">
<div className="flex items-center space-x-2">
<Checkbox
checked={letto}
className="size-6"
id="letto"
onCheckedChange={(v) => v !== "indeterminate" && setLetto(v)}
/>
<div className="grid gap-1.5 leading-none">
<label
className="font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
htmlFor="letto"
>
{t.servizio.conferma.ho_letto}
</label>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox
checked={accettato}
className="size-6"
id="termini"
onCheckedChange={(v) => v !== "indeterminate" && setAccettato(v)}
/>
<div className="grid gap-1.5 leading-none">
<label
className="font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
htmlFor="termini"
>
{t.servizio.conferma.accetto}
</label>
<p className="text-muted-foreground text-sm">
{t.servizio.conferma.confermo_termini}
</p>
</div>
</div>
</div>
<Button
className="h-fit whitespace-normal text-pretty"
disabled={!letto || !accettato}
onClick={() => {
// Handle confirmation logic here
conferma({
annuncioId,
servizioId,
});
}}
>
{t.servizio.conferma.conferma}
</Button>
</>
);
};
ConfermaImmobile.getLayout = function getLayout(page) {
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
};
export default ConfermaImmobile;
export const getServerSideProps = (async (context) => {
const slug = context.params?.slug;
if (!slug || slug.length !== 2) {
return {
notFound: true,
};
}
const [servizioId, annuncioId] = slug;
if (!servizioId || !annuncioId) {
return {
notFound: true,
};
}
const parsedServizioId = zServizioId.safeParse(servizioId);
const parsedAnnuncioId = zAnnuncioId.safeParse(parseInt(annuncioId));
if (!parsedServizioId.success || !parsedAnnuncioId.success) {
return {
notFound: true,
};
}
const access_token = context.req.cookies.access_token;
const helper = await TrpcAuthedFetchingIstance({ access_token });
if (helper) {
await helper.trpc.servizio.getServizioAnnuncio.prefetch({
annuncioId: parsedAnnuncioId.data,
servizioId: parsedServizioId.data,
});
return {
props: {
annuncioId: parsedAnnuncioId.data,
servizioId: parsedServizioId.data,
trpcState: helper.trpc.dehydrate(),
},
};
}
return redirectTo500;
}) satisfies GetServerSideProps<ConfermaImmobileProps>;

View file

@ -1,131 +0,0 @@
import { ArrowLeft } from "lucide-react";
import type { GetServerSideProps } from "next";
import { useRouter } from "next/router";
import { DocNotFoundPage } from "~/components/doc_not_found";
import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { FileSection } from "~/components/servizio/conferma";
import { Status500 } from "~/components/status-page";
import { Button } from "~/components/ui/button";
import { redirectTo500 } from "~/lib/utils";
import type { NextPageWithLayout } from "~/pages/_app";
import { useTranslation } from "~/providers/I18nProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
import { api } from "~/utils/api";
type RiapriConfermaProps = {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
};
/**
* Pagina di conferma di riapertura servizio: /servizio/riapri-conferma/[servizioId]/[annuncioId]
*/
const RiapriConferma: NextPageWithLayout<RiapriConfermaProps> = ({
servizioId,
annuncioId,
}: RiapriConfermaProps) => {
const { data, isLoading } = api.servizio.getServizioAnnuncio.useQuery({
annuncioId,
servizioId,
});
const router = useRouter();
const { t } = useTranslation();
if (isLoading) return <LoadingPage />;
if (!data) return <Status500 />;
if (!data.doc_conferma_ref) return <DocNotFoundPage />;
return (
<div className="mx-2 flex flex-1 overflow-auto">
<div className="mx-auto flex grow sm:grow-0">
<div className="flex flex-col items-center space-y-4 py-4">
<h1 className="font-bold text-xl">{t.servizio.conferma.titolo}</h1>
<p className="text-center">{t.servizio.conferma.descrizione}</p>
<FileSection storageId={data.doc_conferma_ref} />
<div className="flex max-w-3xl flex-col gap-2 rounded-md bg-green-400 p-4 text-left">
<p className="font-semibold text-xl">{t.servizio.conferma.cta}</p>
<p>{t.servizio.conferma.coordinate}</p>
<p>
{t.servizio.conferma.intestazione}{" "}
{data.caparra_intestazione || ""}
</p>
<p>
{t.servizio.conferma.iban} {data.caparra_iban || ""}
</p>
<p>
{t.servizio.conferma.importo} {data.caparra_importo || ""}
</p>
<p>
{t.servizio.conferma.causale} {data.caparra_causale || ""}
</p>
<p className="font-semibold text-xl">
{t.servizio.conferma.coord_footer}
</p>
</div>
<Button
className="mx-auto mb-4"
onClick={() => router.back()}
variant="secondary"
>
<ArrowLeft />
{t.indietro}
</Button>
</div>
</div>
</div>
);
};
RiapriConferma.getLayout = function getLayout(page) {
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
};
export default RiapriConferma;
export const getServerSideProps = (async (context) => {
const slug = context.params?.slug;
if (!slug || slug.length !== 2) {
return {
notFound: true,
};
}
const [servizioId, annuncioId] = slug;
if (!servizioId || !annuncioId) {
return {
notFound: true,
};
}
const parsedServizioId = zServizioId.safeParse(servizioId);
const parsedAnnuncioId = zAnnuncioId.safeParse(parseInt(annuncioId));
if (!parsedServizioId.success || !parsedAnnuncioId.success) {
return {
notFound: true,
};
}
const access_token = context.req.cookies.access_token;
const helper = await TrpcAuthedFetchingIstance({ access_token });
if (helper) {
await helper.trpc.servizio.getServizioAnnuncio.prefetch({
annuncioId: parsedAnnuncioId.data,
servizioId: parsedServizioId.data,
});
return {
props: {
annuncioId: parsedAnnuncioId.data,
servizioId: parsedServizioId.data,
trpcState: helper.trpc.dehydrate(),
},
};
}
return redirectTo500;
}) satisfies GetServerSideProps<RiapriConfermaProps>;

View file

@ -10,18 +10,18 @@ import { LoadingPage } from "~/components/loading";
import type { Session, SessionStatus } from "~/server/api/trpc"; import type { Session, SessionStatus } from "~/server/api/trpc";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
export type ValidSession = { type ValidSession = {
user: Session; user: Session;
status: Extract<SessionStatus, "AUTHENTICATED">; status: Extract<SessionStatus, "AUTHENTICATED">;
}; };
type InvalidStatus = Exclude<SessionStatus, "AUTHENTICATED"> | "LOADING"; type InvalidStatus = Exclude<SessionStatus, "AUTHENTICATED"> | "LOADING";
export type InvalidSession = { type InvalidSession = {
status: InvalidStatus; status: InvalidStatus;
user: null; user: null;
}; };
export type SessionContextType = ValidSession | InvalidSession; type SessionContextType = ValidSession | InvalidSession;
const SessionContext = createContext<SessionContextType>({ const SessionContext = createContext<SessionContextType>({
status: "LOADING", status: "LOADING",

View file

@ -3,6 +3,7 @@
import type { ServizioServizioId } from './Servizio'; import type { ServizioServizioId } from './Servizio';
import type { AnnunciId } from './Annunci'; import type { AnnunciId } from './Annunci';
import type { default as StatusConfermaEnum } from './StatusConfermaEnum';
import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely';
/** Identifier type for public.servizio_annunci */ /** Identifier type for public.servizio_annunci */
@ -20,22 +21,6 @@ export default interface ServizioAnnunciTable {
open_contatti_at: ColumnType<Date | null, Date | string | null, Date | string | null>; open_contatti_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
doc_conferma_ref: ColumnType<string | null, string | null, string | null>;
user_confirmed_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
accettato_conferma_at: ColumnType<Date | null, Date | string | null, Date | string | null>;
caparra_causale: ColumnType<string | null, string | null, string | null>;
caparra_iban: ColumnType<string | null, string | null, string | null>;
caparra_importo: ColumnType<string | null, string | null, string | null>;
caparra_intestazione: ColumnType<string | null, string | null, string | null>;
hasConfermaAdmin: ColumnType<boolean, boolean | undefined, boolean>;
doc_contratto_ref: ColumnType<string | null, string | null, string | null>; doc_contratto_ref: ColumnType<string | null, string | null, string | null>;
gestionale_id: ColumnType<number | null, number | null, number | null>; gestionale_id: ColumnType<number | null, number | null, number | null>;
@ -48,13 +33,13 @@ export default interface ServizioAnnunciTable {
doc_registrazione_ref: ColumnType<string | null, string | null, string | null>; doc_registrazione_ref: ColumnType<string | null, string | null, string | null>;
doc_conferma_added: ColumnType<boolean, boolean | undefined, boolean>;
doc_contratto_added: ColumnType<boolean, boolean | undefined, boolean>; doc_contratto_added: ColumnType<boolean, boolean | undefined, boolean>;
doc_registrazione_added: ColumnType<boolean, boolean | undefined, boolean>; doc_registrazione_added: ColumnType<boolean, boolean | undefined, boolean>;
note: ColumnType<string | null, string | null, string | null>; note: ColumnType<string | null, string | null, string | null>;
status_conferma: ColumnType<StatusConfermaEnum | null, StatusConfermaEnum | null, StatusConfermaEnum | null>;
} }
export type ServizioAnnunci = Selectable<ServizioAnnunciTable>; export type ServizioAnnunci = Selectable<ServizioAnnunciTable>;

View file

@ -0,0 +1,11 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.
/** Represents the enum public.StatusConfermaEnum */
enum StatusConfermaEnum {
'Inviato conferma' = 'Inviato conferma',
'Conferma accettata' = 'Conferma accettata',
'Caparra versata' = 'Caparra versata',
};
export default StatusConfermaEnum;

View file

@ -18,22 +18,18 @@ import { getPacksPerServizio } from "~/server/controllers/pagamenti.controller";
import { import {
AdminAttivaServizio_Ufficio, AdminAttivaServizio_Ufficio,
addServizioAnnunci, addServizioAnnunci,
adminUpdateConfermaStatus,
attivaServizio_SaltaPagamentoAcconto, attivaServizio_SaltaPagamentoAcconto,
getAllServizioAnnunci, getAllServizioAnnunci,
getAnnunciAvailableToAdd, getAnnunciAvailableToAdd,
getCompatibileAnnunci, getCompatibileAnnunci,
getDataPerAcquisto, getDataPerAcquisto,
getRichieste, getRichieste,
getServiziByUserId,
getServizioDataById, getServizioDataById,
InteractionLogicTipologia, InteractionLogicTipologia,
interruzioneServizio, interruzioneServizio,
processServizioOnboard, processServizioOnboard,
SbloccaContatti, SbloccaContatti,
type ServizioData, type ServizioData,
userAccettaConferma,
userSendConfermaIntent,
} from "~/server/controllers/servizio.controller"; } from "~/server/controllers/servizio.controller";
import { import {
createOrdine, createOrdine,
@ -376,47 +372,7 @@ export const servizioRouter = createTRPCRouter({
servizioId: input.servizioId, servizioId: input.servizioId,
}); });
}), }),
adminUpdateConfermaStatus: adminProcedure
.input(
z.object({
annuncioId: zAnnuncioId,
servizioId: zServizioId,
status: z.boolean(),
}),
)
.mutation(async ({ input }) => {
return await adminUpdateConfermaStatus({
annuncioId: input.annuncioId,
servizioId: input.servizioId,
status: input.status,
});
}),
userSendConfermaIntent: protectedProcedure
.input(
z.object({
annuncioId: zAnnuncioId,
servizioId: zServizioId,
}),
)
.mutation(async ({ input }) => {
return await userSendConfermaIntent({
annuncioId: input.annuncioId,
servizioId: input.servizioId,
});
}),
userAcceptConfermaImmobile: protectedProcedure
.input(
z.object({
annuncioId: zAnnuncioId,
servizioId: zServizioId,
}),
)
.mutation(async ({ input }) => {
return await userAccettaConferma({
annuncioId: input.annuncioId,
servizioId: input.servizioId,
});
}),
getAllServiziData: protectedProcedure getAllServiziData: protectedProcedure
.input( .input(
z.object({ z.object({

View file

@ -1,9 +1,6 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import z from "zod"; import z from "zod";
import type { import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
NewUsersStorage,
UsersStorageUserStorageId,
} from "~/schemas/public/UsersStorage";
import { import {
adminProcedure, adminProcedure,
createTRPCRouter, createTRPCRouter,
@ -14,7 +11,6 @@ import {
deleteUserStorageEntry, deleteUserStorageEntry,
getAllWithFromAdmin, getAllWithFromAdmin,
getMultipleWithRelations, getMultipleWithRelations,
getStorageIdFromUserStorageId,
purgeFileFromUserStorages, purgeFileFromUserStorages,
retrieveUserFiles, retrieveUserFiles,
} from "~/server/controllers/storage.controller"; } from "~/server/controllers/storage.controller";
@ -87,11 +83,4 @@ export const storageRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await editFile(input.fileId, input.name); return await editFile(input.fileId, input.name);
}), }),
getStorageIdFromUserStorageId: protectedProcedure
.input(z.object({ userStorageId: z.custom<UsersStorageUserStorageId>() }))
.query(async ({ input }) => {
return await getStorageIdFromUserStorageId({
userStorageId: input.userStorageId,
});
}),
}); });

View file

@ -455,7 +455,7 @@ export const getAllServizioAnnunci = async (
//ignora annunci che non sono disponibili //ignora annunci che non sono disponibili
//.where("annunci.web", "=", true) //.where("annunci.web", "=", true)
//.where("annunci.stato", "!=", "Sospeso") //.where("annunci.stato", "!=", "Sospeso")
.orderBy("servizio_annunci.user_confirmed_at", (ob) => .orderBy("servizio_annunci.status_conferma", (ob) =>
ob.asc().nullsLast(), ob.asc().nullsLast(),
) )
.orderBy("servizio_annunci.open_contatti_at", (ob) => .orderBy("servizio_annunci.open_contatti_at", (ob) =>
@ -593,7 +593,7 @@ export const getServizioDataById = async (
//ignora annunci che non sono disponibili //ignora annunci che non sono disponibili
//.where("annunci.web", "=", true) //.where("annunci.web", "=", true)
//.where("annunci.stato", "!=", "Sospeso") //.where("annunci.stato", "!=", "Sospeso")
.orderBy("servizio_annunci.user_confirmed_at", (ob) => .orderBy("servizio_annunci.status_conferma", (ob) =>
ob.asc().nullsLast(), ob.asc().nullsLast(),
) )
.orderBy("servizio_annunci.open_contatti_at", (ob) => .orderBy("servizio_annunci.open_contatti_at", (ob) =>
@ -631,10 +631,7 @@ export const getServizioDataById = async (
...annuncio, ...annuncio,
created_at: new Date(annuncio.created_at), created_at: new Date(annuncio.created_at),
open_contatti_at: parseNullableDateString(annuncio.open_contatti_at), open_contatti_at: parseNullableDateString(annuncio.open_contatti_at),
user_confirmed_at: parseNullableDateString(annuncio.user_confirmed_at),
accettato_conferma_at: parseNullableDateString(
annuncio.accettato_conferma_at,
),
contratto_decorrenza: parseNullableDateString( contratto_decorrenza: parseNullableDateString(
annuncio.contratto_decorrenza, annuncio.contratto_decorrenza,
), ),
@ -1006,222 +1003,6 @@ export const InteractionLogicTipologia = async ({
}; };
}; };
export const userSendConfermaIntent = async ({
servizioId,
annuncioId,
}: {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
}) => {
try {
const servizio = await getServizioById(servizioId);
if (
!servizio ||
servizio.isInterrotto ||
!servizio.isOkAcconto ||
servizio.decorrenza == null
) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Servizio not found",
});
}
await db
.updateTable("servizio_annunci")
.set({
user_confirmed_at: new Date(),
})
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.execute();
// NOTIFICHE EMAIL CONFERMA IMMOBILE
const utente = await getUser({ db, userId: servizio.user_id });
if (!utente.comms_enabled) {
return true;
}
const annuncio_codice = await db
.selectFrom("annunci")
.select("codice")
.where("id", "=", annuncioId)
.executeTakeFirstOrThrow(
() =>
new Error(
`Annuncio not found - servizioId: ${servizioId}, annunciId: ${annuncioId}`,
),
);
await NewMail({
template: {
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}`,
label: "Visualizza Annuncio",
},
noreply: true,
testo: `L'utente ${utente.username} ha notificato interesse per l'immobile con codice ${annuncio_codice.codice}.`,
title: `Nuovo interesse conferma immobile ${annuncio_codice.codice}`,
},
},
mail: {
subject: `Nuovo interesse conferma immobile ${annuncio_codice.codice}`,
to: "web@infoalloggi.it",
},
});
await NewMail({
template: {
mailType: "generic",
props: {
noreply: true,
testo: `La tua notifica di interesse per l'immobile con codice ${annuncio_codice.codice} è stata inviata con successo. Contatteremo i proprietari per raccogliere le informazioni necessarie. Riceverai una notifica via email per procedere.`,
title: `Notifica interesse immobile ${annuncio_codice.codice} inviata con successo`,
},
},
mail: {
subject: `Notifica interesse immobile ${annuncio_codice.codice} inviata con successo`,
to: utente.email,
},
userId: utente.id,
});
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error confirming immobile: ${(e as Error).message}`,
});
}
};
export const adminUpdateConfermaStatus = async ({
servizioId,
annuncioId,
status,
}: {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
status: boolean;
}) => {
try {
await db
.updateTable("servizio_annunci")
.set({
hasConfermaAdmin: status,
})
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.execute();
const servizio = await getServizioById(servizioId);
const utente = await getUser({ db, userId: servizio.user_id });
const saldo = await SaldoSolver({
tipologia: servizio.tipologia,
permanenza: servizio.permanenza,
budget: servizio.budget,
});
if (saldo.success) {
await createOrdine({
servizio_id: servizioId,
type: OrderTypeEnum.Saldo,
amount_cent: saldo.pack.prezzo_cent,
packid: saldo.pack.idprezziario,
userid: servizio.user_id,
});
} else {
console.error(
`Errore nel calcolo del saldo per il servizio ${servizioId}: ${saldo.message}, procedura invio conferma`,
);
}
//ADMIN CONFERMA LAVORATA EMAIL
if (!utente.comms_enabled) {
return true;
}
if (status) {
await NewMail({
template: {
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/servizio/${servizioId}`,
label: "Vai al servizio",
},
noreply: true,
testo: `La conferma è stata lavorata con successo. Puoi procedere con le prossime azioni.`,
title: "Conferma lavorata per il servizio di ricerca affitto",
},
},
mail: {
subject: "Conferma lavorata per il servizio di ricerca affitto",
to: utente.email,
},
userId: utente.id,
});
}
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error updating conferma status from annuncio: ${(e as Error).message}`,
});
}
};
export const userAccettaConferma = async ({
servizioId,
annuncioId,
}: {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
}) => {
try {
await db
.updateTable("servizio_annunci")
.set({
accettato_conferma_at: new Date(),
})
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.execute();
const servizio = await getServizioById(servizioId);
const utente = await getUser({ db, userId: servizio.user_id });
// USER ACCETTA CONFERMA EMAIL
await NewMail({
template: {
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/admin/user-view/servizio/${servizio.user_id}/${servizioId}`,
label: "Vai al servizio",
},
noreply: true,
testo: `L'utente ha accettato le condizioni di conferma per la locazione. Puoi procedere con le prossime azioni.`,
title: `Utente ${utente.username} ha accettato le condizioni di conferma`,
},
},
mail: {
subject: `Utente ${utente.username} ha accettato le condizioni di conferma`,
to: "web@infoalloggi.it",
},
});
return { success: true, userId: utente.id };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error accepting conferma from annuncio: ${(e as Error).message}`,
});
}
};
export const UtenteInteressatoAdAnnuncioEmail = async ({ export const UtenteInteressatoAdAnnuncioEmail = async ({
codice, codice,
nome, nome,
@ -1414,7 +1195,7 @@ export const getRichieste = async (): Promise<RichiesteData[]> => {
withImages(), withImages(),
withVideos(), withVideos(),
]) ])
.orderBy("servizio_annunci.user_confirmed_at", (ob) => .orderBy("servizio_annunci.status_conferma", (ob) =>
ob.asc().nullsLast(), ob.asc().nullsLast(),
) )
.orderBy("servizio_annunci.open_contatti_at", (ob) => .orderBy("servizio_annunci.open_contatti_at", (ob) =>