feat: add AdminLavoraConferma and AdminContratto components for managing service confirmations and contracts

- Implemented AdminLavoraConferma component to handle confirmation processes with document management.
- Added AdminContratto component for managing rental contracts and related documents.
- Introduced shared components for document selection and display (DocInfo and DocCombo).
- Enhanced storage controller with a new function to retrieve storage ID from user storage ID.
- Updated database interactions to improve error handling and data retrieval.
This commit is contained in:
Marco Pedone 2025-11-06 17:03:42 +01:00
parent 17ecc9604f
commit 23525d8f14
19 changed files with 1900 additions and 1482 deletions

View file

@ -88,7 +88,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e
disponibile_da = $47, disponibile_da = $47,
permanenza = $48, permanenza = $48,
persone = $49, persone = $49,
updated_at = NOW() media_updated_at = NOW()
WHERE codice = $46 WHERE codice = $46
`, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore, `, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore,
annuncio.Email, annuncio.Creato_il, annuncio.Modificato_il, annuncio.Email, annuncio.Creato_il, annuncio.Modificato_il,
@ -155,7 +155,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e
disponibile_da, disponibile_da,
permanenza, permanenza,
persone, persone,
updated_at media_updated_at
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, NOW()) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, NOW())
`, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore, `, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore,

View file

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

View file

@ -0,0 +1,512 @@
import { CircleCheck, ClipboardPaste, Pen, Trash2, Wrench } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import z from "zod";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import { api } from "~/utils/api";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza";
import { LoadingPage } from "../loading";
import { Button } from "../ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../ui/card";
import { Separator } from "../ui/separator";
import { UploadModal } from "../upload_modal";
import { DocCombo, DocInfo } from "./shared";
export const AdminContratto = () => {
const { data } = useServizioAnnuncio();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button
aria-label="Open Contratto Section"
className="w-fit"
variant={data.doc_contratto_added ? "success" : "secondary"}
>
{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 />
<Separator />
<ContrattoImport />
<Separator />
<RicevutaSection />
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Close Credenza" variant="outline">
Chiudi
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};
const ContrattoSection = () => {
const { userId } = useServizio();
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<string | null>(
data.doc_contratto_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_contratto_added: selectedDoc != null,
doc_contratto_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 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"
className="flex items-center gap-3"
onClick={async () => {
await updateServizioAnnunci({
doc_contratto_added: false,
doc_contratto_ref: null,
});
setSelectedDoc(null);
}}
variant="destructive"
>
<Trash2 className="size-6" />
</Button>
</DocInfo>
</div>
)}
<DocCombo
current={selectedDoc}
files={files}
onSelect={(f) => {
setSelectedDoc(f.id);
}}
/>
<UploadModal isAdmin userId={userId} />
</CardContent>
<CardFooter>
<Button
aria-label="Save Contratto"
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};
const ContrattoImportSchema = z.object({
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(),
gestionale_id: z.number().nullable(),
});
interface ContrattoImportObj
extends Omit<
z.infer<typeof ContrattoImportSchema>,
"contratto_decorrenza" | "contratto_scadenza"
> {
contratto_decorrenza: Date | null;
contratto_scadenza: Date | null;
}
const parseContrattoImport = (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 = 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,
};
}
console.error("Invalid format:", parsed.error);
return undefined;
};
const ContrattoImport = () => {
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const [contratto, setContratto] = useState<ContrattoImportObj | undefined>({
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"],
gestionale_id: data.gestionale_id,
});
const handleSave = async () => {
await updateServizioAnnunci({
contratto_decorrenza: contratto?.contratto_decorrenza || null,
contratto_scadenza: contratto?.contratto_scadenza || null,
contratto_tipo: contratto?.contratto_tipo || null,
gestionale_id: contratto?.gestionale_id || null,
});
};
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();
setContratto(parseContrattoImport(clipboardText));
}}
variant="info"
>
<ClipboardPaste className="size-4" />
Clicca qui per importare la stringa dal gestionale
</Button>
<Button
aria-label="Reset Contratto"
onClick={() => {
setContratto({
contratto_decorrenza: null,
contratto_scadenza: null,
contratto_tipo: null,
gestionale_id: null,
});
}}
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"
className="h-6 px-1.5 [&_svg]:size-4"
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,
);
}
}}
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"
className="h-6 px-1.5 [&_svg]:size-4"
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,
);
}
}}
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"
className="h-6 px-1.5 [&_svg]:size-4"
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,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Tipo di contratto: </span>
{contratto.contratto_tipo}{" "}
<Button
aria-label="Edit Tipo di contratto"
className="h-6 px-1.5 [&_svg]:size-4"
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,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
</div>
) : (
<p>Inserisci la stringa dal gestionale e elabora</p>
)}
</CardContent>
<CardFooter>
<Button
aria-label="Save Contratto"
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};
const RicevutaSection = () => {
const { userId } = useServizio();
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<string | null>(
data.doc_registrazione_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_registrazione_added: selectedDoc != null,
doc_registrazione_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>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"
className="flex items-center gap-3"
onClick={async () => {
await updateServizioAnnunci({
doc_registrazione_added: false,
doc_registrazione_ref: null,
});
setSelectedDoc(null);
}}
variant="destructive"
>
<Trash2 className="size-6" />
</Button>
</DocInfo>
</div>
)}
<DocCombo
current={selectedDoc}
files={files}
onSelect={(f) => {
setSelectedDoc(f.id);
}}
/>
<UploadModal isAdmin userId={userId} />
</CardContent>
<CardFooter>
<Button
aria-label="Save Ricevuta"
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,28 @@
import { CircleCheck, Handshake } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react";
import toast from "react-hot-toast";
import { AllegatoIframe } from "~/components/allegato-iframe"; import { AllegatoIframe } from "~/components/allegato-iframe";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { useTranslation } from "~/providers/I18nProvider"; 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 { api } from "~/utils/api";
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";
import { type UploadCallback, UploadModal } from "../upload_modal";
export const FileSection = ({ storageId }: { storageId: string }) => { export const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({ const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
@ -27,3 +47,236 @@ export const FileSection = ({ storageId }: { storageId: string }) => {
</> </>
); );
}; };
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-fit">
<Handshake className="size-6" /> Sono interessato a confermare
</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 setOpen={setOpen} />
)}
</CredenzaContent>
</Credenza>
);
};
const InvioConferma = ({ setOpen }: { setOpen: (value: boolean) => void }) => {
const { userId, 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.getAllServizioAnnunci.invalidate({ userId });
setOpen(false);
},
},
);
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<p>{t.richieste.conferma_txt}</p>
</CredenzaBody>
<CredenzaFooter>
<LoadingButton
aria-label="Conferma immobile"
loading={isPending}
onClick={() => {
mutate({
annuncioId,
servizioId,
});
}}
variant="success"
>
{t.conferma}
</LoadingButton>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza">Chiudi</Button>
</CredenzaClose>
</CredenzaFooter>
</>
);
};
const DocComponent = ({
docInfo,
cb,
}: {
docInfo: {
storageId: string;
ref: UsersStorageUserStorageId | null;
} | null;
cb: UploadCallback;
}) => {
const { isAdmin, userId } = useServizio();
if (docInfo?.storageId) {
return (
<div className="flex items-center gap-2 text-green-500">
<CircleCheck className="size-5" />
<span>Documento caricato</span>
</div>
);
}
return (
<UploadModal
cb_onUpload={cb}
isAdmin={isAdmin}
maxFiles={1}
userId={userId}
/>
);
};
const DocCheckpoint = () => {
const { t } = useTranslation();
const { userId, servizio } = useServizio();
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.updateServizio.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({
userId,
});
await utils.storage.retrieveUserFileData.invalidate({ userId });
await utils.servizio.getServizio.invalidate({
servizioId: servizio.servizio_id,
});
},
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>
Devi ancora caricare un tuo documento legato alla permanenza e
motivazione di transitorietà
</p>
<div className="flex flex-col gap-2">
<span>Documento:</span>
<DocComponent
cb={({ userStorageIds }) =>
userStorageIds.length > 0 &&
userStorageIds[0] &&
handleUpload(userStorageIds[0])
}
docInfo={servizio.doc_motivazione}
/>
</div>
<br />
<br />
<p className="text-muted-foreground text-sm">
I tuoi dati saranno conservati solamente per la durata del servizio,
in conformità con la nostra{" "}
<Link className="underline" href="/privacy-policy" target="_blank">
Informativa sulla Privacy
</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 } = useTranslation();
const { data } = useServizioAnnuncio();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button aria-label="Conferma in lavorazione" className="w-fit">
<CircleCheck className="size-7" />
Conferma in lavorazione
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle>In attesa di conferma</CredenzaTitle>
<CredenzaDescription className="sr-only">
Consegna status modal
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="prose max-h-[80vh] overflow-auto pb-5">
<p>
Richiesta di conferma inviata il:{" "}
{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>
La conferma è ancora in lavorazione. Stiamo verificando con i
proprietari le condizioni e dettagli della conferma per riferirteli.
Attendi una comunicazione.
</p>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza" variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};

View file

@ -1,27 +1,7 @@
import { import { FileBadge, FileText, PackageCheck, Pointer } from "lucide-react";
CircleCheck,
FileBadge,
FileText,
Handshake,
PackageCheck,
Pointer,
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "~/components/custom_ui/credenza";
import LoadingButton from "~/components/custom_ui/loading-button";
import { Button, type ButtonConfigs } from "~/components/ui/button"; import { Button, type ButtonConfigs } from "~/components/ui/button";
import { import {
Tooltip, Tooltip,
@ -30,30 +10,44 @@ import {
TooltipTrigger, TooltipTrigger,
} from "~/components/ui/tooltip"; } from "~/components/ui/tooltip";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
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 { api } from "~/utils/api"; import { api } from "~/utils/api";
import { ConfermaAnnuncioModal, ConfermaInLavorazioneModal } from "./conferma";
import { ContattiProprietarioModal } from "./modale_contatti"; import { ContattiProprietarioModal } from "./modale_contatti";
export const Interactions = () => { const SezioneConferma = () => {
const { servizio, servizioId } = useServizio(); const { servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio(); const { data, annuncioId } = useServizioAnnuncio();
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;
if (ContattiSbloccati && !ConfermaUtente) {
return <ConfermaAnnuncioModal />;
}
if (ConfermaUtente && !data.hasConfermaAdmin) {
return <ConfermaInLavorazioneModal />;
}
if (AccettatoConferma) {
return (
<Link
aria-label="Conferma immobile"
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
>
<Button className="w-fit" variant="success">
<Pointer className="size-6" />
Rivedi la conferma
</Button>
</Link>
);
}
if (ConfermaLavorata) {
if (data.doc_conferma_ref) {
return ( return (
<div className="flex flex-col gap-3">
<ContattiProprietarioModal />
<div className="flex flex-wrap gap-3">
{data.open_contatti_at !== null && data.user_confirmed_at == null && (
<AnnuncioConfirm />
)}
{data.user_confirmed_at && !data.hasConfermaAdmin && (
<ConfermaLavorazioneModal />
)}
{data.doc_conferma_added &&
data.accettato_conferma_at == null &&
data.hasConfermaAdmin &&
(data.doc_conferma_ref ? (
<Link <Link
aria-label="Conferma immobile" aria-label="Conferma immobile"
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`} href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
@ -63,7 +57,9 @@ export const Interactions = () => {
Confermato - Procedi ora Confermato - Procedi ora
</Button> </Button>
</Link> </Link>
) : ( );
}
return (
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -77,37 +73,35 @@ export const Interactions = () => {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Il documento non è più disponibile</p> <p>Il documento non è più disponibile</p>
<p> <p>Se hai bisogno di una copia, contattaci via email o chat.</p>
Se hai bisogno di una copia, contattaci via email o chat.
</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
))} );
{data.accettato_conferma_at != null && ( }
<Link
aria-label="Conferma immobile" return null;
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`} };
>
<Button className="w-fit" variant="success"> const PostConfermaSection = () => {
<Pointer className="size-6" /> const { servizio, servizioId } = useServizio();
Rivedi la conferma const { data, annuncioId } = useServizioAnnuncio();
</Button>
</Link> const saldoPaid = servizio.isOkSaldo;
)} const consulenzaPaid = servizio.isOkConsulenza;
</div>
{!servizio.isOkSaldo && data.accettato_conferma_at != null && ( if (!saldoPaid) {
return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h3 className="font-semibold"> <h3 className="font-semibold">
Questo annuncio è stato confermato, procedi al saldo: Questo annuncio è stato confermato, procedi al saldo:
</h3> </h3>
<SaldoButton annuncioId={annuncioId} /> <SaldoButton annuncioId={annuncioId} />
</div> </div>
)} );
}
{servizio.isOkSaldo && if (!data.doc_contratto_added) {
data.accettato_conferma_at != null && return (
data.doc_contratto_added == null && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h3 className="font-semibold text-green-500"> <h3 className="font-semibold text-green-500">
Ci siamo quasi! Stiamo preparando il contratto per te. Ci siamo quasi! Stiamo preparando il contratto per te.
@ -116,13 +110,13 @@ export const Interactions = () => {
Una volta pronto, riceverai una notifica via email. Una volta pronto, riceverai una notifica via email.
</h3> </h3>
</div> </div>
)} );
{servizio.isOkSaldo && }
data.accettato_conferma_at != null &&
data.doc_contratto_added && ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h3 className="font-semibold"> <h3 className="font-semibold">
{servizio.isOkConsulenza {consulenzaPaid
? "Il contratto è pronto, puoi visualizzarlo:" ? "Il contratto è pronto, puoi visualizzarlo:"
: "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"} : "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"}
</h3> </h3>
@ -149,18 +143,13 @@ export const Interactions = () => {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Il documento non è più disponibile</p> <p>Il documento non è più disponibile</p>
<p> <p>Se hai bisogno di una copia, contattaci via email o chat.</p>
Se hai bisogno di una copia, contattaci via email o
chat.
</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
{!servizio.isOkConsulenza && data.contratto_tipo && ( {!consulenzaPaid && data.contratto_tipo && <SaldoConsulenzaButton />}
<SaldoConsulenzaButton />
)}
{data.doc_registrazione_added && {data.doc_registrazione_added &&
(data.doc_registrazione_ref ? ( (data.doc_registrazione_ref ? (
@ -186,8 +175,7 @@ export const Interactions = () => {
<TooltipContent> <TooltipContent>
<p>Il documento non è più disponibile</p> <p>Il documento non è più disponibile</p>
<p> <p>
Se hai bisogno di una copia, contattaci via email o Se hai bisogno di una copia, contattaci via email o chat.
chat.
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -195,7 +183,19 @@ export const Interactions = () => {
))} ))}
</div> </div>
</div> </div>
)} );
};
export const Interactions = () => {
const { data } = useServizioAnnuncio();
return (
<div className="flex flex-col gap-3">
<ContattiProprietarioModal />
<SezioneConferma />
{data.accettato_conferma_at != null && <PostConfermaSection />}
</div> </div>
); );
}; };
@ -282,113 +282,3 @@ const SaldoConsulenzaButton = ({
</Button> </Button>
); );
}; };
const AnnuncioConfirm = () => {
const { userId, servizioId } = useServizio();
const { annuncioId } = useServizioAnnuncio();
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.userConfirmImmobile.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.getAllServizioAnnunci.invalidate({ userId });
setOpen(false);
},
});
return (
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild>
<Button aria-label="Conferma immobile" className="w-fit">
<Handshake className="size-6" /> Conferma immobile
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle> {t.richieste.conf_title}</CredenzaTitle>
<CredenzaDescription className="sr-only">
{t.richieste.conf_desc}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<p>{t.richieste.conferma_txt}</p>
</CredenzaBody>
<CredenzaFooter>
<LoadingButton
aria-label="Conferma immobile"
loading={isPending}
onClick={() => {
mutate({
annuncioId,
servizioId,
});
}}
variant="success"
>
{t.conferma}
</LoadingButton>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza">Chiudi</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};
const ConfermaLavorazioneModal = () => {
const { t } = useTranslation();
const { data } = useServizioAnnuncio();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button aria-label="Conferma in lavorazione" className="w-fit">
<CircleCheck className="size-7" />
Conferma in lavorazione
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle>In attesa di conferma</CredenzaTitle>
<CredenzaDescription className="sr-only">
Consegna status modal
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="prose max-h-[80vh] overflow-auto pb-5">
<p>
Richiesta di conferma inviata il:{" "}
{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>
La conferma è ancora in lavorazione. Stiamo verificando con i
proprietari le condizioni e dettagli della conferma per riferirteli.
Attendi una comunicazione.
</p>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza" variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};

View file

@ -171,14 +171,19 @@ export const ServizioContent = () => {
(a) => a.accettato_conferma_at !== null, (a) => a.accettato_conferma_at !== null,
); );
if (servizio.isOkSaldo) { if (servizio.isOkSaldo && annuncioConfermato.length > 0) {
return ( return (
<ServizioCard <ServizioCard
content={ content={
<> <>
{annuncioConfermato.map((data) => { {annuncioConfermato.map((data) => {
return ( return (
<ServizioAnnuncioProvider data={data} key={data.id}> <ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizio.servizio_id}
userId={userId}
>
<AnnuncioCard /> <AnnuncioCard />
</ServizioAnnuncioProvider> </ServizioAnnuncioProvider>
); );
@ -194,6 +199,10 @@ export const ServizioContent = () => {
); );
} }
const annunciPreContatti = servizio.annunci.filter(
(a) => a.open_contatti_at === null && a.user_confirmed_at === null,
);
const openContattiAnnunci = servizio.annunci.filter( const openContattiAnnunci = servizio.annunci.filter(
(a) => a.open_contatti_at !== null && a.user_confirmed_at === null, (a) => a.open_contatti_at !== null && a.user_confirmed_at === null,
); );
@ -275,14 +284,19 @@ export const ServizioContent = () => {
<div className="flex flex-col gap-2 overflow-y-auto"> <div className="flex flex-col gap-2 overflow-y-auto">
{annunciInConferma.map((data) => { {annunciInConferma.map((data) => {
return ( return (
<ServizioAnnuncioProvider data={data} key={data.id}> <ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizio.servizio_id}
userId={userId}
>
<AnnuncioCard className="border-2 border-green-500" /> <AnnuncioCard className="border-2 border-green-500" />
</ServizioAnnuncioProvider> </ServizioAnnuncioProvider>
); );
})} })}
</div> </div>
<AnnunciSelezionatiAccordion annunci={annunciNotInConferma} /> <AnnunciSelezionatiAccordion annunci={annunciPreContatti} />
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<AnnunciCompatibili /> <AnnunciCompatibili />
</div> </div>

View file

@ -131,7 +131,7 @@ const DocCheckpoint = () => {
}); });
const handleUpload = ( const handleUpload = (
field: "doc_personale_fronte_ref" | "doc_personale_retro_ref", field: "doc_personale_fronte_ref" | "doc_personale_retro_ref",
id: string, id: UsersStorageUserStorageId,
) => { ) => {
mutate({ mutate({
servizioId: servizio.servizio_id, servizioId: servizio.servizio_id,

View file

@ -196,7 +196,7 @@ export const AddAnnuncio = ({
}; };
const InterruzioneServizio = () => { const InterruzioneServizio = () => {
const { servizio, userId } = useServizio(); const { servizio, userId, isAdmin } = useServizio();
const beforeInterruzioneLimit = const beforeInterruzioneLimit =
servizio.decorrenza != null && servizio.decorrenza != null &&
@ -223,9 +223,14 @@ const InterruzioneServizio = () => {
<Button <Button
aria-label="Request Interruzione" aria-label="Request Interruzione"
className="" className=""
disabled={!beforeInterruzioneLimit} disabled={
!isAdmin &&
(!beforeInterruzioneLimit ||
servizio.isInterrotto ||
servizio.annunci.some((a) => a.user_confirmed_at !== null))
}
> >
<ClockAlert /> <span> Richiedi Interruzione Servizio</span> <ClockAlert /> <span>Interruzione Servizio</span>
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
@ -255,7 +260,7 @@ const InterruzioneServizio = () => {
}; };
const EditPreferenze = () => { const EditPreferenze = () => {
const { servizioId, userId, isAdmin } = useServizio(); const { servizioId, userId, isAdmin, servizio } = useServizio();
const [openEditPreferenze, setOpenEditPreferenze] = useState(false); const [openEditPreferenze, setOpenEditPreferenze] = useState(false);
const { data, isLoading } = api.servizio.getServizio.useQuery({ servizioId }); const { data, isLoading } = api.servizio.getServizio.useQuery({ servizioId });
const utils = api.useUtils(); const utils = api.useUtils();
@ -287,7 +292,15 @@ const EditPreferenze = () => {
return ( return (
<Credenza onOpenChange={setOpenEditPreferenze} open={openEditPreferenze}> <Credenza onOpenChange={setOpenEditPreferenze} open={openEditPreferenze}>
<CredenzaTrigger asChild> <CredenzaTrigger asChild>
<Button aria-label="Preferenze di ricerca" className=""> <Button
aria-label="Preferenze di ricerca"
className=""
disabled={
!isAdmin &&
(servizio.isInterrotto ||
servizio.annunci.some((a) => a.hasConfermaAdmin))
}
>
<Calculator /> <Calculator />
Modifica Preferenze Modifica Preferenze
</Button> </Button>

View file

@ -61,6 +61,7 @@ export const AnnunciInConfermaDialog = ({
}: { }: {
annunci: ServizioData["annunci"]; annunci: ServizioData["annunci"];
}) => { }) => {
const { servizioId, userId } = useServizio();
return ( return (
<DialogServizio <DialogServizio
title="Annunci in conferma" title="Annunci in conferma"
@ -84,7 +85,12 @@ export const AnnunciInConfermaDialog = ({
<div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto"> <div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto">
{annunci.map((data) => { {annunci.map((data) => {
return ( return (
<ServizioAnnuncioProvider data={data} key={data.id}> <ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizioId}
userId={userId}
>
<AnnuncioCard className="border-2 border-green-500" /> <AnnuncioCard className="border-2 border-green-500" />
</ServizioAnnuncioProvider> </ServizioAnnuncioProvider>
); );
@ -99,6 +105,7 @@ export const AnnunciSelezionatiDialog = ({
}: { }: {
annunci: ServizioData["annunci"]; annunci: ServizioData["annunci"];
}) => { }) => {
const { servizioId, userId } = useServizio();
return ( return (
<DialogServizio <DialogServizio
title="Annunci selezionati" title="Annunci selezionati"
@ -122,7 +129,12 @@ export const AnnunciSelezionatiDialog = ({
{annunci.length > 0 ? ( {annunci.length > 0 ? (
annunci.map((data) => { annunci.map((data) => {
return ( return (
<ServizioAnnuncioProvider data={data} key={data.id}> <ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizioId}
userId={userId}
>
<AnnuncioCard className="border-2 border-sky-500" /> <AnnuncioCard className="border-2 border-sky-500" />
</ServizioAnnuncioProvider> </ServizioAnnuncioProvider>
); );
@ -152,6 +164,7 @@ export const AnnunciInConfermaAccordion = ({
}: { }: {
annunci: ServizioData["annunci"]; annunci: ServizioData["annunci"];
}) => { }) => {
const { servizioId, userId } = useServizio();
if (annunci.length === 0) { if (annunci.length === 0) {
return null; // If there are no announcements in confirmation, do not render this section return null; // If there are no announcements in confirmation, do not render this section
} }
@ -166,7 +179,12 @@ export const AnnunciInConfermaAccordion = ({
<div className="flex flex-col flex-wrap gap-5"> <div className="flex flex-col flex-wrap gap-5">
{annunci.map((data) => { {annunci.map((data) => {
return ( return (
<ServizioAnnuncioProvider data={data} key={data.id}> <ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizioId}
userId={userId}
>
<AnnuncioCard className="border-2 border-green-500" /> <AnnuncioCard className="border-2 border-green-500" />
</ServizioAnnuncioProvider> </ServizioAnnuncioProvider>
); );
@ -183,6 +201,7 @@ export const AnnunciSelezionatiAccordion = ({
}: { }: {
annunci: ServizioData["annunci"]; annunci: ServizioData["annunci"];
}) => { }) => {
const { servizioId, userId } = useServizio();
return ( return (
<Accordion collapsible defaultValue="open" type="single"> <Accordion collapsible defaultValue="open" type="single">
<AccordionItem className="space-y-4" value="open"> <AccordionItem className="space-y-4" value="open">
@ -194,7 +213,12 @@ export const AnnunciSelezionatiAccordion = ({
{annunci.length > 0 ? ( {annunci.length > 0 ? (
annunci.map((data) => { annunci.map((data) => {
return ( return (
<ServizioAnnuncioProvider data={data} key={data.id}> <ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizioId}
userId={userId}
>
<AnnuncioCard className="border-2 border-sky-500" /> <AnnuncioCard className="border-2 border-sky-500" />
</ServizioAnnuncioProvider> </ServizioAnnuncioProvider>
); );
@ -210,7 +234,8 @@ export const AnnunciSelezionatiAccordion = ({
> >
Annunci Annunci
<ExternalLink className="size-4" /> <ExternalLink className="size-4" />
</Link> </Link>{" "}
o tra gli annunci compatibili qui sotto.
</span> </span>
</div> </div>
)} )}

View file

@ -0,0 +1,116 @@
import { Check, ChevronsUpDown, ExternalLink } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
import { api } from "~/utils/api";
import { ExtIcon } from "../area-riservata/allegati";
import { Button } from "../ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "../ui/command";
import { Label } from "../ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
export const DocInfo = ({
storageId,
children,
}: {
storageId: string;
children: React.ReactNode;
}) => {
const { data: file_info } = api.storage.getFileInfos.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}`}
id="selected-file"
target="_blank"
>
<Button className="w-fit grow" variant="outline">
<ExtIcon mimeType={file_info.mimeType} />
<span className="max-w-40 truncate sm:max-w-80">
{file_info.originalName}
</span>
<ExternalLink className="size-4" />
</Button>
</Link>
</div>
{children}
</div>
</div>
);
};
export const DocCombo = ({
files,
onSelect,
current,
}: {
files: FileMetadataWithAdmin[];
onSelect: (file: FileMetadataWithAdmin) => void;
current: string | null;
}) => {
const [open, setOpen] = useState(false);
return (
<Popover onOpenChange={setOpen} open={open}>
<PopoverTrigger asChild>
<Button
aria-expanded={open}
aria-label="Select Existing File"
className="w-auto justify-between"
role="combobox"
variant="outline"
>
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}
onSelect={(currentValue) => {
if (currentValue !== current) {
// biome-ignore lint/style/noNonNullAssertion: <exists>
onSelect(files.find((f) => f.id === currentValue)!);
}
setOpen(false);
}}
value={file.id}
>
<span className="flex items-center gap-2">
<ExtIcon mimeType={file.mimeType} />
<span className="block font-medium">
{file.originalName}
</span>
{current === file.id && (
<Check className="size-4 text-accent-foreground" />
)}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};

View file

@ -182,6 +182,7 @@ export const UploadComponent = ({
userId, userId,
from_admin, from_admin,
}); });
if (!res) continue;
userStorageIds.push(res.user_storage_id); userStorageIds.push(res.user_storage_id);
} }
} }

View file

@ -108,11 +108,11 @@ const FormNewServizioSchema = z.object({
reddito: z.string().nullable(), reddito: z.string().nullable(),
sede_legale: z.string().nullable(), sede_legale: z.string().nullable(),
sesso: z.string(), sesso: z.string(),
skipOrdine: z.boolean(),
telefono: z.string(), telefono: z.string(),
terrazzo: z.boolean(), terrazzo: z.boolean(),
tipologia: z.custom<TipologiaPosizioneEnum>(), tipologia: z.custom<TipologiaPosizioneEnum>(),
scadenza_motivazione_transitoria: z.date().nullable(), scadenza_motivazione_transitoria: z.date().nullable(),
skipPayment: z.boolean(),
}); });
type FormNewServizioValues = z.infer<typeof FormNewServizioSchema>; type FormNewServizioValues = z.infer<typeof FormNewServizioSchema>;
@ -308,7 +308,6 @@ export const FormNewServizioAcquisto = ({
sede_legale: initialData.anagrafica?.sede_legale || "", sede_legale: initialData.anagrafica?.sede_legale || "",
sesso: initialData.anagrafica?.sesso || "M", sesso: initialData.anagrafica?.sesso || "M",
...initialData.servizio, ...initialData.servizio,
skipOrdine: false,
}; };
const { locale, t } = useTranslation(); const { locale, t } = useTranslation();
@ -337,7 +336,7 @@ export const FormNewServizioAcquisto = ({
}); });
if (isAdmin) { if (isAdmin) {
if (data) { if (data.ordineIdForPayment) {
await router.push(`/area-riservata/admin/user-view/ordini/${userId}`); await router.push(`/area-riservata/admin/user-view/ordini/${userId}`);
} else { } else {
await router.push( await router.push(
@ -345,7 +344,11 @@ export const FormNewServizioAcquisto = ({
); );
} }
} else { } else {
await router.push(`/servizio/pagamento/${data}`); if (data.ordineIdForPayment) {
await router.push(`/servizio/pagamento/${data.ordineIdForPayment}`);
} else {
await router.push("/area-riservata/dashboard");
}
} }
}, },
}); });
@ -391,9 +394,9 @@ export const FormNewServizioAcquisto = ({
reddito: fields.reddito, reddito: fields.reddito,
terrazzo: fields.terrazzo, terrazzo: fields.terrazzo,
tipologia: fields.tipologia, tipologia: fields.tipologia,
skipPayment: fields.skipPayment,
}, },
servizioId: initialData.servizio.servizio_id, servizioId: initialData.servizio.servizio_id,
skipOrdine: isAdmin ? fields.skipOrdine : false,
user: { user: {
cognome: fields.cognome, cognome: fields.cognome,
email: fields.email, email: fields.email,
@ -1427,11 +1430,11 @@ export const FormNewServizioAcquisto = ({
{isAdmin && ( {isAdmin && (
<FormField <FormField
control={form.control} control={form.control}
name="skipOrdine" name="skipPayment"
render={({ field }) => ( render={({ field }) => (
<FormItem className="py-2"> <FormItem className="py-2">
<div className="flex items-center justify-start gap-x-4"> <div className="flex items-center justify-start gap-x-4">
<FormLabel htmlFor="skipOrdine"> <FormLabel htmlFor="skipPayment">
Vuoi saltare la creazione dell&apos;ordine e impostare il Vuoi saltare la creazione dell&apos;ordine e impostare il
servizio come attivo? servizio come attivo?
</FormLabel> </FormLabel>
@ -1439,7 +1442,7 @@ export const FormNewServizioAcquisto = ({
<Switch <Switch
checked={field.value} checked={field.value}
className="data-[state=checked]:bg-neutral-700" className="data-[state=checked]:bg-neutral-700"
id="skipOrdine" id="skipPayment"
onCheckedChange={(v) => { onCheckedChange={(v) => {
field.onChange(v); field.onChange(v);
}} }}

View file

@ -83,14 +83,15 @@ const ConfirmSection = ({
const [letto, setLetto] = useState(false); const [letto, setLetto] = useState(false);
const [accettato, setAccettato] = useState(false); const [accettato, setAccettato] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate: conferma } = api.servizio.updateServizioAnnunci.useMutation({ const { mutate: conferma } =
api.servizio.userAcceptConfermaImmobile.useMutation({
onMutate: () => { onMutate: () => {
const toastId = toast.loading(t.caricamento, { const toastId = toast.loading(t.caricamento, {
icon: "🔄", icon: "🔄",
}); });
return { toastId }; return { toastId };
}, },
onSuccess: async (_error, _variables, context) => { onSuccess: async (_data, _variables, context) => {
await utils.storage.retrieveUserFileData.invalidate(); await utils.storage.retrieveUserFileData.invalidate();
toast.success("Confermato", { toast.success("Confermato", {
@ -147,9 +148,6 @@ const ConfirmSection = ({
// Handle confirmation logic here // Handle confirmation logic here
conferma({ conferma({
annuncioId, annuncioId,
data: {
accettato_conferma_at: new Date(),
},
servizioId, servizioId,
}); });
}} }}

View file

@ -1,9 +1,19 @@
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import toast from "react-hot-toast";
import type { AnnunciId } from "~/schemas/public/Annunci"; import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio"; import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { ServizioData } from "~/server/controllers/servizio.controller"; import type { ServizioData } from "~/server/controllers/servizio.controller";
import { api } from "~/utils/api";
type ServizioProviderProps = {
isAdmin: boolean;
userId: UsersId;
servizioId: ServizioServizioId;
servizio: ServizioData;
children: React.ReactNode;
};
type ServizioContextProps = { type ServizioContextProps = {
isAdmin: boolean; isAdmin: boolean;
userId: UsersId; userId: UsersId;
@ -14,20 +24,11 @@ type ServizioContextProps = {
const ServizioContext = createContext<ServizioContextProps | null>(null); const ServizioContext = createContext<ServizioContextProps | null>(null);
export const ServizioProvider = ({ export const ServizioProvider = ({
isAdmin,
userId,
servizioId,
servizio,
children, children,
}: { ...props
isAdmin: boolean; }: ServizioProviderProps) => {
userId: UsersId;
servizioId: ServizioServizioId;
servizio: ServizioData;
children: React.ReactNode;
}) => {
return ( return (
<ServizioContext.Provider value={{ isAdmin, servizio, servizioId, userId }}> <ServizioContext.Provider value={{ ...props }}>
{children} {children}
</ServizioContext.Provider> </ServizioContext.Provider>
); );
@ -41,23 +42,54 @@ export const useServizio = () => {
return servizioContext; return servizioContext;
}; };
type ServizioAnnuncioProviderProps = {
data: ServizioData["annunci"][number];
servizioId: ServizioServizioId;
userId: UsersId;
children: React.ReactNode;
};
type ServizioAnnuncioContextProps = { type ServizioAnnuncioContextProps = {
data: ServizioData["annunci"][number]; data: ServizioData["annunci"][number];
annuncioId: AnnunciId; annuncioId: AnnunciId;
updateServizioAnnunci: (data: ServizioAnnunciUpdate) => Promise<void>;
}; };
const ServizioAnnuncioContext = const ServizioAnnuncioContext =
createContext<ServizioAnnuncioContextProps | null>(null); createContext<ServizioAnnuncioContextProps | null>(null);
export const ServizioAnnuncioProvider = ({ export const ServizioAnnuncioProvider = ({
data, data,
servizioId,
userId,
children, children,
}: { }: ServizioAnnuncioProviderProps) => {
data: ServizioData["annunci"][number]; const utils = api.useUtils();
children: React.ReactNode; const { mutateAsync } = api.servizio.updateServizioAnnunci.useMutation({
}) => { onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({
userId: userId,
});
await utils.servizio.getServizio.invalidate({
servizioId,
});
toast.success("Dati salvati con successo");
},
});
const updateServizioAnnunci = async (upd: ServizioAnnunciUpdate) => {
await mutateAsync({
annuncioId: data.id,
servizioId,
data: upd,
});
};
return ( return (
<ServizioAnnuncioContext.Provider <ServizioAnnuncioContext.Provider
value={{ annuncioId: data.annunci_id, data }} value={{ annuncioId: data.annunci_id, data, updateServizioAnnunci }}
> >
{children} {children}
</ServizioAnnuncioContext.Provider> </ServizioAnnuncioContext.Provider>

View file

@ -20,6 +20,7 @@ import {
import { import {
addServizio, addServizio,
addServizioAnnunci, addServizioAnnunci,
adminUpdateConfermaStatus,
deleteServizio, deleteServizio,
deleteServizioAnnuncio, deleteServizioAnnuncio,
getAllPagamenti, getAllPagamenti,
@ -47,7 +48,8 @@ import {
updatePagamento, updatePagamento,
updateServizio, updateServizio,
updateServizioAnnuncio, updateServizioAnnuncio,
userConfirmImmobile, userAccettaConferma,
userSendConfermaIntent,
} from "~/server/controllers/servizio.controller"; } from "~/server/controllers/servizio.controller";
import { import {
zAnnuncioId, zAnnuncioId,
@ -256,7 +258,6 @@ export const servizioRouter = createTRPCRouter({
anagrafica: z.custom<UsersAnagraficaUpdate>(), anagrafica: z.custom<UsersAnagraficaUpdate>(),
servizio: z.custom<ServizioUpdate>(), servizio: z.custom<ServizioUpdate>(),
servizioId: zServizioId, servizioId: zServizioId,
skipOrdine: z.boolean().optional(),
user: z.custom< user: z.custom<
Pick<Users, "nome" | "cognome" | "email" | "telefono" | "username"> Pick<Users, "nome" | "cognome" | "email" | "telefono" | "username">
>(), >(),
@ -420,7 +421,22 @@ export const servizioRouter = createTRPCRouter({
servizioId: input.servizioId, servizioId: input.servizioId,
}); });
}), }),
userConfirmImmobile: protectedProcedure 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( .input(
z.object({ z.object({
annuncioId: zAnnuncioId, annuncioId: zAnnuncioId,
@ -428,7 +444,20 @@ export const servizioRouter = createTRPCRouter({
}), }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await userConfirmImmobile({ 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, annuncioId: input.annuncioId,
servizioId: input.servizioId, servizioId: input.servizioId,
}); });

View file

@ -1,6 +1,9 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import z from "zod"; import z from "zod";
import type { NewUsersStorage } from "~/schemas/public/UsersStorage"; import type {
NewUsersStorage,
UsersStorageUserStorageId,
} from "~/schemas/public/UsersStorage";
import { import {
adminProcedure, adminProcedure,
createTRPCRouter, createTRPCRouter,
@ -11,6 +14,7 @@ import {
deleteUserStorageEntry, deleteUserStorageEntry,
getAllWithFromAdmin, getAllWithFromAdmin,
getMultipleWithRelations, getMultipleWithRelations,
getStorageIdFromUserStorageId,
purgeFileFromUserStorages, purgeFileFromUserStorages,
retrieveUserFiles, retrieveUserFiles,
} from "~/server/controllers/storage.controller"; } from "~/server/controllers/storage.controller";
@ -83,4 +87,11 @@ 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

@ -237,15 +237,13 @@ export const postAcquistoDataHandler = async ({
user, user,
anagrafica, anagrafica,
servizio, servizio,
skipOrdine = false,
}: { }: {
servizioId: ServizioServizioId; servizioId: ServizioServizioId;
userId: UsersId; userId: UsersId;
user: Pick<Users, "nome" | "cognome" | "email" | "telefono">; user: Pick<Users, "nome" | "cognome" | "email" | "telefono">;
anagrafica: UsersAnagraficaUpdate; anagrafica: UsersAnagraficaUpdate;
servizio: ServizioUpdate; servizio: ServizioUpdate;
skipOrdine?: boolean; }): Promise<{ ordineIdForPayment: OrdiniOrdineId | null }> => {
}) => {
try { try {
const servizioData = await db.transaction().execute(async (tx) => { const servizioData = await db.transaction().execute(async (tx) => {
// Update user and anagrafica data // Update user and anagrafica data
@ -285,16 +283,18 @@ export const postAcquistoDataHandler = async ({
.executeTakeFirstOrThrow(); .executeTakeFirstOrThrow();
}); });
if (skipOrdine) { if (servizioData.skipPayment) {
// If skipping order, set servizio as active with decorrenza today // If skipping order, set servizio as active with decorrenza today
await updateServizio({ await updateServizio({
data: { data: {
decorrenza: new Date(), decorrenza: new Date(),
isOkAcconto: true, isOkAcconto: true,
isOkSaldo: true,
isOkConsulenza: true,
}, },
servizioId, servizioId,
}); });
return null; return { ordineIdForPayment: null };
} }
//cleanup unused payments //cleanup unused payments
@ -350,7 +350,7 @@ export const postAcquistoDataHandler = async ({
.where("ordini.isActive", "=", false) .where("ordini.isActive", "=", false)
.where("ordine_id", "!=", existingOrder.ordine_id) .where("ordine_id", "!=", existingOrder.ordine_id)
.execute(); .execute();
return existingOrder.ordine_id; return { ordineIdForPayment: existingOrder.ordine_id };
} }
const order = await db const order = await db
@ -364,7 +364,7 @@ export const postAcquistoDataHandler = async ({
.returning("ordine_id") .returning("ordine_id")
.executeTakeFirstOrThrow(); .executeTakeFirstOrThrow();
return order.ordine_id; return { ordineIdForPayment: order.ordine_id };
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
@ -790,44 +790,6 @@ export const updateServizioAnnuncio = async ({
.where("annunci_id", "=", annuncioId) .where("annunci_id", "=", annuncioId)
.execute(); .execute();
const servizio = await getServizioById(servizioId);
const utente = await getUser({ db, userId: servizio.user_id });
//ADMIN CONFERMA LAVORATA EMAIL
if (data.hasConfermaAdmin) {
await NewMail({
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`,
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",
},
subject: "Conferma lavorata per il servizio di ricerca affitto",
to: utente.email,
userId: utente.id,
});
}
// USER ACCETTA CONFERMA EMAIL
if (data.accettato_conferma_at) {
await NewMail({
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}-${annuncioId}`,
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`,
},
subject: `Utente ${utente.username} ha accettato le condizioni di conferma`,
to: "web@infoalloggi.it",
});
}
return true; return true;
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({
@ -1241,7 +1203,7 @@ export const sendServizioEmail = async ({
} }
}; };
export const userConfirmImmobile = async ({ export const userSendConfermaIntent = async ({
servizioId, servizioId,
annuncioId, annuncioId,
}: { }: {
@ -1314,6 +1276,101 @@ export const userConfirmImmobile = async ({
}); });
} }
}; };
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 });
//ADMIN CONFERMA LAVORATA EMAIL
if (status) {
await NewMail({
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`,
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",
},
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({
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}-${annuncioId}`,
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`,
},
subject: `Utente ${utente.username} ha accettato le condizioni di conferma`,
to: "web@infoalloggi.it",
});
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error accepting conferma from annuncio: ${(e as Error).message}`,
});
}
};
export const isOrdineAwaitingPayment = async (ordineId: OrdiniOrdineId) => { export const isOrdineAwaitingPayment = async (ordineId: OrdiniOrdineId) => {
try { try {
const ordine = await db const ordine = await db
@ -1529,6 +1586,7 @@ export const getCompatibileAnnunci = async (
let qry = db let qry = db
.selectFrom("annunci") .selectFrom("annunci")
.distinctOn("annunci.id")
.select((eb) => [ .select((eb) => [
"id", "id",
"codice", "codice",

View file

@ -1,6 +1,9 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { NewUsersStorage } from "~/schemas/public/UsersStorage"; import type {
NewUsersStorage,
UsersStorageUserStorageId,
} from "~/schemas/public/UsersStorage";
import { db, type Querier } from "~/server/db"; import { db, type Querier } from "~/server/db";
import { fetchFiles, retriveFilesSafely } from "../services/storage.service"; import { fetchFiles, retriveFilesSafely } from "../services/storage.service";
@ -83,6 +86,26 @@ export const retrieveUserFiles = async ({ userId }: { userId: UsersId }) => {
} }
}; };
export const getStorageIdFromUserStorageId = async ({
userStorageId,
}: {
userStorageId: UsersStorageUserStorageId;
}) => {
try {
const storage = await db
.selectFrom("users_storage")
.select("storageId")
.where("user_storage_id", "=", userStorageId)
.executeTakeFirstOrThrow();
return storage.storageId;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while getting storage ID from user storage ID: ${(e as Error).message}`,
});
}
};
export const getAllWithFromAdmin = async () => { export const getAllWithFromAdmin = async () => {
try { try {
const files = await db const files = await db
@ -115,7 +138,7 @@ export const addUserStorage = async (data: NewUsersStorage) => {
.values(data) .values(data)
.onConflict((oc) => oc.columns(["userId", "storageId"]).doNothing()) .onConflict((oc) => oc.columns(["userId", "storageId"]).doNothing())
.returning("user_storage_id") .returning("user_storage_id")
.executeTakeFirstOrThrow(); .executeTakeFirst();
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",