From 23525d8f1484b5258470a5957e72f65274b37db7 Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Thu, 6 Nov 2025 17:03:42 +0100 Subject: [PATCH] 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. --- apps/backend/queries/annunci.go | 4 +- .../components/servizio/admin_conferma.tsx | 540 ++++++++ .../components/servizio/admin_contratto.tsx | 512 ++++++++ .../components/servizio/annuncio_actions.tsx | 1108 +---------------- .../src/components/servizio/conferma.tsx | 253 ++++ .../src/components/servizio/interactions.tsx | 418 +++---- .../src/components/servizio/main.tsx | 22 +- .../components/servizio/modale_contatti.tsx | 2 +- .../components/servizio/servizio_actions.tsx | 23 +- .../servizio/servizio_annunci_accordions.tsx | 35 +- .../src/components/servizio/shared.tsx | 116 ++ .../src/components/upload_modal.tsx | 1 + .../src/forms/FormNewServizioAcquisto.tsx | 19 +- .../servizio/conferma-immobile/[...slug].tsx | 36 +- .../src/providers/ServizioProvider.tsx | 66 +- .../src/server/api/routers/servizio.ts | 37 +- .../src/server/api/routers/storage.ts | 13 +- .../server/controllers/servizio.controller.ts | 150 ++- .../server/controllers/storage.controller.ts | 27 +- 19 files changed, 1900 insertions(+), 1482 deletions(-) create mode 100644 apps/infoalloggi/src/components/servizio/admin_conferma.tsx create mode 100644 apps/infoalloggi/src/components/servizio/admin_contratto.tsx create mode 100644 apps/infoalloggi/src/components/servizio/shared.tsx diff --git a/apps/backend/queries/annunci.go b/apps/backend/queries/annunci.go index 878bb22..69f9b8c 100644 --- a/apps/backend/queries/annunci.go +++ b/apps/backend/queries/annunci.go @@ -88,7 +88,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e disponibile_da = $47, permanenza = $48, persone = $49, - updated_at = NOW() + media_updated_at = NOW() WHERE codice = $46 `, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore, annuncio.Email, annuncio.Creato_il, annuncio.Modificato_il, @@ -155,7 +155,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e disponibile_da, permanenza, 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()) `, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore, diff --git a/apps/infoalloggi/src/components/servizio/admin_conferma.tsx b/apps/infoalloggi/src/components/servizio/admin_conferma.tsx new file mode 100644 index 0000000..372a251 --- /dev/null +++ b/apps/infoalloggi/src/components/servizio/admin_conferma.tsx @@ -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 ( + + + + + + + Lavorazione conferma + + Consegna status modal + + + +
+ + + {servizio.tipologia === TipologiaPosizioneEnum.Transitorio && ( + + )} + +
+
+ + + + + + +
+
+ ); +}; + +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 ; + if (files === undefined) { + return

Errore nel caricamento dei documenti

; + } + return ( + + + Documento di Conferma + + documento che l'utente vedra alla conferma + + + + {data.doc_conferma_ref && ( +
+ + + +
+ )} +
+ { + await updateServizioAnnunci({ + doc_conferma_added: true, + doc_conferma_ref: f.id, + }); + + setUserStorage({ + storageId: f.id, + userId, + }); + }} + /> + +
+
+
+ ); +}; + +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; + +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({ + 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 ( + + + Dati Pagamento Caparra + + dati per il pagamento della caparra, da mostrare all'utente + + + +
+ + +
+ {caparra ? ( +
+

+ Intestazione: + {caparra.caparra_intestazione}{" "} + +

+

+ IBAN: + {caparra.caparra_iban}{" "} + +

+

+ Importo: + {caparra.caparra_importo}{" "} + +

+

+ Causale: + {caparra.caparra_causale}{" "} + +

+
+ ) : ( +

Inserisci la stringa dal gestionale e elabora

+ )} +
+ + + +
+ ); +}; + +const MotivazioneSection = () => { + const { servizioId } = useServizio(); + const { t } = useTranslation(); + + const { data: servizio } = api.servizio.getServizio.useQuery({ servizioId }); + if (!servizio) return null; + return ( + + + Motivazione di transitorietà + + Informazioni relative alla motivazione di transitorietà inserite + dall'utente + + + +
+

+ Motivazione specificata: + {servizio.motivazione_transitorio ? ( + + { + t.preferenze.motivazioni_mappings.find( + (m) => m.id === servizio.motivazione_transitorio, + )?.title + } + + ) : ( + Non inserita + )} +

+

+ + Data scadenza motivazione inserita:{" "} + + {servizio.scadenza_motivazione_transitoria ? ( + format(servizio.scadenza_motivazione_transitoria, "dd/MM/yyyy") + ) : ( + Non inserita + )} +

+

+ Documento caricato: + {servizio.doc_motivazione_ref ? ( + + ) : ( + Non caricato + )} +

+
+
+
+ ); +}; + +const FileLink = ({ + userStorageId, +}: { + userStorageId: UsersStorageUserStorageId; +}) => { + const { data: storageId } = + api.storage.getStorageIdFromUserStorageId.useQuery({ + userStorageId, + }); + if (!storageId) return null; + return ( + + Vedi documento + + ); +}; + +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 ( + + + Lavorazione Conferma + + controlla i parametri di ricerca prima! + + + +
+

Tipologia servizio: {servizio.tipologia}

+ {(() => { + if (servizio.tipologia === TipologiaPosizioneEnum.Transitorio) { + return ( + <> +

+ Permanenza specificata dall'utente:{" "} + {servizio.permanenza + ? t.preferenze.permanenza[servizio.permanenza] + : "non inserita"} +

+

+ Permanenza in descrizione: {(() => { + const match = data.desc_it?.match(/Permanenza:.*$/m); + return match + ? `"${match[0]}"` + : "errore: testo non trovato in descrizione"; + })()} +

+ + ); + } + return ( + <> +

Budget (da preferenze): {formatCurrency(servizio.budget)}

+

+ Canone Mensile (da descrizione):{" "} + {formatCurrency(data.prezzo / 100)} +

+ + ); + })()} +
+
+ { + setCheck(c); + }} + /> + +
+
+ + + +
+ ); +}; diff --git a/apps/infoalloggi/src/components/servizio/admin_contratto.tsx b/apps/infoalloggi/src/components/servizio/admin_contratto.tsx new file mode 100644 index 0000000..54bd315 --- /dev/null +++ b/apps/infoalloggi/src/components/servizio/admin_contratto.tsx @@ -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 ( + + + + + + + Sezione Contratto + + Contratto modal + + + +
+ + + + + +
+
+ + + + + + +
+
+ ); +}; + +const ContrattoSection = () => { + const { userId } = useServizio(); + const { data, updateServizioAnnunci } = useServizioAnnuncio(); + const [selectedDoc, setSelectedDoc] = useState( + 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 ; + if (files === undefined) { + return

Errore nel caricamento dei documenti

; + } + return ( + + + Documento del Contratto + contratto di locazione + + + {selectedDoc && ( +
+ + + +
+ )} + { + setSelectedDoc(f.id); + }} + /> + +
+ + + +
+ ); +}; + +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, + "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({ + 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 ( + + + Dati Contratto + desc + + +
+ + +
+ {contratto ? ( +
+

+ ID Gestionale: + {contratto.gestionale_id}{" "} + +

+

+ Decorrenza: + {contratto.contratto_decorrenza && + new Date(contratto.contratto_decorrenza).toLocaleDateString( + "it-IT", + )}{" "} + +

+

+ Scadenza: + {contratto.contratto_scadenza && + new Date(contratto.contratto_scadenza).toLocaleDateString( + "it-IT", + )}{" "} + +

+

+ Tipo di contratto: + {contratto.contratto_tipo}{" "} + +

+
+ ) : ( +

Inserisci la stringa dal gestionale e elabora

+ )} +
+ + + +
+ ); +}; + +const RicevutaSection = () => { + const { userId } = useServizio(); + const { data, updateServizioAnnunci } = useServizioAnnuncio(); + const [selectedDoc, setSelectedDoc] = useState( + 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 ; + if (files === undefined) { + return

Errore nel caricamento dei documenti

; + } + return ( + + + Ricevuta di Registrazione + desc + + + {selectedDoc && ( +
+ + + +
+ )} + { + setSelectedDoc(f.id); + }} + /> + +
+ + + +
+ ); +}; diff --git a/apps/infoalloggi/src/components/servizio/annuncio_actions.tsx b/apps/infoalloggi/src/components/servizio/annuncio_actions.tsx index ec1a7af..f1d2dcc 100644 --- a/apps/infoalloggi/src/components/servizio/annuncio_actions.tsx +++ b/apps/infoalloggi/src/components/servizio/annuncio_actions.tsx @@ -1,66 +1,17 @@ -import { - Check, - ChevronsUpDown, - CircleCheck, - ClipboardPaste, - ExternalLink, - FolderKanban, - Pen, - ShieldUser, - Trash2, - Wrench, -} from "lucide-react"; +import { FolderKanban, ShieldUser, Trash2, Wrench } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; import toast from "react-hot-toast"; -import { z } from "zod/v4"; -import { ExtIcon } from "~/components/area-riservata/allegati"; import { Confirm } from "~/components/confirm"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle, - CredenzaTrigger, -} from "~/components/custom_ui/credenza"; -import { LoadingPage } from "~/components/loading"; -import { Button, buttonVariants } from "~/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "~/components/ui/card"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "~/components/ui/command"; -import { Label } from "~/components/ui/label"; +import { Button } from "~/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger, } from "~/components/ui/popover"; -import { Separator } from "~/components/ui/separator"; -import { Switch } from "~/components/ui/switch"; -import { UploadModal } from "~/components/upload_modal"; -import { cn, formatCurrency } from "~/lib/utils"; -import { useTranslation } from "~/providers/I18nProvider"; import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider"; -import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci"; -import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; -import type { FileMetadataWithAdmin } from "~/server/services/storage.service"; import { api } from "~/utils/api"; +import { AdminLavoraConferma } from "./admin_conferma"; +import { AdminContratto } from "./admin_contratto"; export const AnnuncioActions = () => { const { servizioId, userId, isAdmin } = useServizio(); @@ -156,1054 +107,3 @@ export const AnnuncioActions = () => { } return null; }; - -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, - "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 = ({ - handleUpdate, -}: { - handleUpdate: (upd: ServizioAnnunciUpdate) => void; -}) => { - const { data } = useServizioAnnuncio(); - const [contratto, setContratto] = useState({ - 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 = () => { - handleUpdate({ - ...contratto, - }); - }; - return ( - - - Dati Contratto - desc - - -
- - -
- {contratto ? ( -
-

- ID Gestionale: - {contratto.gestionale_id}{" "} - -

-

- Decorrenza: - {contratto.contratto_decorrenza && - new Date(contratto.contratto_decorrenza).toLocaleDateString( - "it-IT", - )}{" "} - -

-

- Scadenza: - {contratto.contratto_scadenza && - new Date(contratto.contratto_scadenza).toLocaleDateString( - "it-IT", - )}{" "} - -

-

- Tipo di contratto: - {contratto.contratto_tipo}{" "} - -

-
- ) : ( -

Inserisci la stringa dal gestionale e elabora

- )} -
- - - -
- ); -}; - -const AdminContratto = () => { - const { userId, servizioId } = useServizio(); - const { data, annuncioId } = useServizioAnnuncio(); - - const [open, setOpen] = useState(false); - - const utils = api.useUtils(); - - const { mutate: updateServizio } = - api.servizio.updateServizioAnnunci.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 handleUpdate = (upd: ServizioAnnunciUpdate) => { - updateServizio({ - annuncioId, - data: { - ...upd, - }, - servizioId, - }); - }; - - return ( - - - - - - - Sezione Contratto - - Contratto modal - - - -
- - - - - -
-
- - - - - - -
-
- ); -}; - -const ContrattoSection = ({ - handleUpdate, -}: { - handleUpdate: (upd: ServizioAnnunciUpdate) => void; -}) => { - const { userId } = useServizio(); - const { data } = useServizioAnnuncio(); - const [selectedDoc, setSelectedDoc] = useState( - 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 = () => { - handleUpdate({ - doc_contratto_added: selectedDoc != null, - doc_contratto_ref: selectedDoc, - }); - if (selectedDoc) { - setUserStorage({ - storageId: selectedDoc, - userId, - }); - } - }; - - if (isLoading) return ; - if (files === undefined) { - return

Errore nel caricamento dei documenti

; - } - return ( - - - Documento del Contratto - contratto di locazione - - - {selectedDoc && ( -
- - - -
- )} - { - setSelectedDoc(f.id); - }} - /> - -
- - - -
- ); -}; - -const RicevutaSection = ({ - handleUpdate, -}: { - handleUpdate: (upd: ServizioAnnunciUpdate) => void; -}) => { - const { userId } = useServizio(); - const { data } = useServizioAnnuncio(); - const [selectedDoc, setSelectedDoc] = useState( - 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 = () => { - handleUpdate({ - doc_registrazione_added: selectedDoc != null, - doc_registrazione_ref: selectedDoc, - }); - if (selectedDoc) { - setUserStorage({ - storageId: selectedDoc, - userId, - }); - } - }; - - if (isLoading) return ; - if (files === undefined) { - return

Errore nel caricamento dei documenti

; - } - return ( - - - Ricevuta di Registrazione - desc - - - {selectedDoc && ( -
- - - -
- )} - { - setSelectedDoc(f.id); - }} - /> - -
- - - -
- ); -}; - -const AdminLavoraConferma = () => { - const { userId, servizioId } = useServizio(); - const { data, annuncioId } = useServizioAnnuncio(); - - const [open, setOpen] = useState(false); - - const utils = api.useUtils(); - - const { mutate: updateServizio } = - api.servizio.updateServizioAnnunci.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 handleUpdate = (upd: ServizioAnnunciUpdate) => { - updateServizio({ - annuncioId, - data: { - ...upd, - }, - servizioId, - }); - }; - - return ( - - - - - - - Lavorazione conferma - - Consegna status modal - - - -
- - - -
-
- - - - - - -
-
- ); -}; - -const DocSection = ({ - handleUpdate, -}: { - handleUpdate: (upd: ServizioAnnunciUpdate) => void; -}) => { - const { userId } = useServizio(); - const { data } = useServizioAnnuncio(); - - const utils = api.useUtils(); - - const { data: files, isLoading } = api.storage.getAll.useQuery(); - - const { mutate: setUserStorage } = - api.storage.addUserStorageEntry.useMutation({ - onSuccess: async () => { - await utils.storage.retrieveUserFileData.invalidate({ - userId, - }); - await utils.storage.getAll.invalidate(); - }, - }); - - if (isLoading) return ; - if (files === undefined) { - return

Errore nel caricamento dei documenti

; - } - return ( - - - Documento di Conferma - - documento che l'utente vedra alla conferma - - - - {data.doc_conferma_ref && ( -
- - - -
- )} -
- { - handleUpdate({ - doc_conferma_added: true, - doc_conferma_ref: f.id, - }); - - setUserStorage({ - storageId: f.id, - userId, - }); - }} - /> - -
-
-
- ); -}; - -const DocInfo = ({ - storageId, - children, -}: { - storageId: string; - children: React.ReactNode; -}) => { - const { data: file_info } = api.storage.getFileInfos.useQuery({ - storageId, - }); - if (!file_info) return null; - return ( -
- -
-
- - - - {file_info.originalName} - - - -
- {children} -
-
- ); -}; - -const DocCombo = ({ - files, - onSelect, - current, -}: { - files: FileMetadataWithAdmin[]; - onSelect: (file: FileMetadataWithAdmin) => void; - current: string | null; -}) => { - const [open, setOpen] = useState(false); - - return ( - - - - - - - - - Nessun file. - - {files.map((file) => ( - { - if (currentValue !== current) { - // biome-ignore lint/style/noNonNullAssertion: - onSelect(files.find((f) => f.id === currentValue)!); - } - setOpen(false); - }} - value={file.id} - > - - - - {file.originalName} - - {current === file.id && ( - - )} - - - ))} - - - - - - ); -}; - -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; - -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 = ({ - handleUpdate, -}: { - handleUpdate: (upd: ServizioAnnunciUpdate) => void; -}) => { - const { data } = useServizioAnnuncio(); - const [caparra, setCaparra] = useState({ - caparra_causale: data.caparra_causale, - caparra_iban: data.caparra_iban, - caparra_importo: data.caparra_importo, - caparra_intestazione: data.caparra_intestazione, - }); - const handleSave = () => { - handleUpdate({ - ...caparra, - }); - }; - return ( - - - Dati Pagamento Caparra - - dati per il pagamento della caparra, da mostrare all'utente - - - -
- - -
- {caparra ? ( -
-

- Intestazione: - {caparra.caparra_intestazione}{" "} - -

-

- IBAN: - {caparra.caparra_iban}{" "} - -

-

- Importo: - {caparra.caparra_importo}{" "} - -

-

- Causale: - {caparra.caparra_causale}{" "} - -

-
- ) : ( -

Inserisci la stringa dal gestionale e elabora

- )} -
- - - -
- ); -}; - -const SendConferma = ({ - handleUpdate, -}: { - handleUpdate: (upd: ServizioAnnunciUpdate) => void; -}) => { - const { servizio } = useServizio(); - const { data } = useServizioAnnuncio(); - const { t } = useTranslation(); - const [check, setCheck] = useState(data.hasConfermaAdmin); - - return ( - - - Lavorazione Conferma - - controlla i parametri di ricerca prima! - - - -
-

Tipologia servizio: {servizio.tipologia}

- {(() => { - if (servizio.tipologia === TipologiaPosizioneEnum.Transitorio) { - return ( - <> -

- Permanenza (da preferenze):{" "} - {servizio.permanenza - ? t.preferenze.permanenza[servizio.permanenza] - : "non inserita"} -

-

- Permanenza (da descrizione): " - {(() => { - const match = data.desc_it?.match(/Permanenza:.*$/m); - return match ? match[0] : ""; - })()} - " -

- - ); - } - return ( - <> -

Budget (da preferenze): {formatCurrency(servizio.budget)}

-

- Canone Mensile (da descrizione):{" "} - {formatCurrency(data.prezzo / 100)} -

- - ); - })()} -
-
- { - setCheck(c); - }} - /> - -
-
- - - -
- ); -}; diff --git a/apps/infoalloggi/src/components/servizio/conferma.tsx b/apps/infoalloggi/src/components/servizio/conferma.tsx index 6711c46..fe40dfd 100644 --- a/apps/infoalloggi/src/components/servizio/conferma.tsx +++ b/apps/infoalloggi/src/components/servizio/conferma.tsx @@ -1,8 +1,28 @@ +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 { 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 LoadingButton from "../custom_ui/loading-button"; +import { Button } from "../ui/button"; +import { type UploadCallback, UploadModal } from "../upload_modal"; export const FileSection = ({ storageId }: { storageId: string }) => { 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 ( + + + + + + + {t.richieste.conf_title} + + {t.richieste.conf_desc} + + + {needsDocuments ? ( + + ) : ( + + )} + + + ); +}; + +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 ( + <> + +

{t.richieste.conferma_txt}

+
+ + { + mutate({ + annuncioId, + servizioId, + }); + }} + variant="success" + > + {t.conferma} + + + + + + + + ); +}; + +const DocComponent = ({ + docInfo, + cb, +}: { + docInfo: { + storageId: string; + ref: UsersStorageUserStorageId | null; + } | null; + cb: UploadCallback; +}) => { + const { isAdmin, userId } = useServizio(); + + if (docInfo?.storageId) { + return ( +
+ + Documento caricato +
+ ); + } + + return ( + + ); +}; + +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 ( + <> + +
+

+ Devi ancora caricare un tuo documento legato alla permanenza e + motivazione di transitorietà +

+ +
+ Documento: + + userStorageIds.length > 0 && + userStorageIds[0] && + handleUpload(userStorageIds[0]) + } + docInfo={servizio.doc_motivazione} + /> +
+ +
+
+

+ I tuoi dati saranno conservati solamente per la durata del servizio, + in conformità con la nostra{" "} + + Informativa sulla Privacy + +

+
+
+ + + + + + + ); +}; + +export const ConfermaInLavorazioneModal = () => { + const { t } = useTranslation(); + const { data } = useServizioAnnuncio(); + return ( + + + + + + + In attesa di conferma + + Consegna status modal + + + +

+ 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", + })} +

+

+ La conferma è ancora in lavorazione. Stiamo verificando con i + proprietari le condizioni e dettagli della conferma per riferirteli. + Attendi una comunicazione. +

+
+ + + + + + +
+
+ ); +}; diff --git a/apps/infoalloggi/src/components/servizio/interactions.tsx b/apps/infoalloggi/src/components/servizio/interactions.tsx index 79f7b33..d15d34d 100644 --- a/apps/infoalloggi/src/components/servizio/interactions.tsx +++ b/apps/infoalloggi/src/components/servizio/interactions.tsx @@ -1,27 +1,7 @@ -import { - CircleCheck, - FileBadge, - FileText, - Handshake, - PackageCheck, - Pointer, -} from "lucide-react"; +import { FileBadge, FileText, PackageCheck, Pointer } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useState } from "react"; 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 { Tooltip, @@ -30,49 +10,166 @@ import { TooltipTrigger, } from "~/components/ui/tooltip"; import { cn } from "~/lib/utils"; -import { useTranslation } from "~/providers/I18nProvider"; import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider"; import type { AnnunciId } from "~/schemas/public/Annunci"; import { api } from "~/utils/api"; +import { ConfermaAnnuncioModal, ConfermaInLavorazioneModal } from "./conferma"; import { ContattiProprietarioModal } from "./modale_contatti"; -export const Interactions = () => { +const SezioneConferma = () => { + const { servizioId } = useServizio(); + 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 ; + } + + if (ConfermaUtente && !data.hasConfermaAdmin) { + return ; + } + if (AccettatoConferma) { + return ( + + + + ); + } + if (ConfermaLavorata) { + if (data.doc_conferma_ref) { + return ( + + + + ); + } + return ( + + + + + + +

Il documento non è più disponibile

+

Se hai bisogno di una copia, contattaci via email o chat.

+
+
+
+ ); + } + + return null; +}; + +const PostConfermaSection = () => { const { servizio, servizioId } = useServizio(); const { data, annuncioId } = useServizioAnnuncio(); + const saldoPaid = servizio.isOkSaldo; + const consulenzaPaid = servizio.isOkConsulenza; + + if (!saldoPaid) { + return ( +
+

+ Questo annuncio è stato confermato, procedi al saldo: +

+ +
+ ); + } + if (!data.doc_contratto_added) { + return ( +
+

+ Ci siamo quasi! Stiamo preparando il contratto per te. +

+

+ Una volta pronto, riceverai una notifica via email. +

+
+ ); + } + return ( -
- -
- {data.open_contatti_at !== null && data.user_confirmed_at == null && ( - +
+

+ {consulenzaPaid + ? "Il contratto è pronto, puoi visualizzarlo:" + : "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"} +

+
+ {data.doc_contratto_ref ? ( + + + + ) : ( + + + + + + +

Il documento non è più disponibile

+

Se hai bisogno di una copia, contattaci via email o chat.

+
+
+
)} - {data.user_confirmed_at && !data.hasConfermaAdmin && ( - - )} - {data.doc_conferma_added && - data.accettato_conferma_at == null && - data.hasConfermaAdmin && - (data.doc_conferma_ref ? ( + + {!consulenzaPaid && data.contratto_tipo && } + + {data.doc_registrazione_added && + (data.doc_registrazione_ref ? ( - ) : ( - @@ -84,118 +181,21 @@ export const Interactions = () => { ))} - {data.accettato_conferma_at != null && ( - - - - )}
- {!servizio.isOkSaldo && data.accettato_conferma_at != null && ( -
-

- Questo annuncio è stato confermato, procedi al saldo: -

- -
- )} +
+ ); +}; - {servizio.isOkSaldo && - data.accettato_conferma_at != null && - data.doc_contratto_added == null && ( -
-

- Ci siamo quasi! Stiamo preparando il contratto per te. -

-

- Una volta pronto, riceverai una notifica via email. -

-
- )} - {servizio.isOkSaldo && - data.accettato_conferma_at != null && - data.doc_contratto_added && ( -
-

- {servizio.isOkConsulenza - ? "Il contratto è pronto, puoi visualizzarlo:" - : "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"} -

-
- {data.doc_contratto_ref ? ( - - - - ) : ( - - - - - - -

Il documento non è più disponibile

-

- Se hai bisogno di una copia, contattaci via email o - chat. -

-
-
-
- )} +export const Interactions = () => { + const { data } = useServizioAnnuncio(); - {!servizio.isOkConsulenza && data.contratto_tipo && ( - - )} + return ( +
+ - {data.doc_registrazione_added && - (data.doc_registrazione_ref ? ( - - - - ) : ( - - - - - - -

Il documento non è più disponibile

-

- Se hai bisogno di una copia, contattaci via email o - chat. -

-
-
-
- ))} -
-
- )} + + + {data.accettato_conferma_at != null && }
); }; @@ -282,113 +282,3 @@ const SaldoConsulenzaButton = ({ ); }; - -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 ( - - - - - - - {t.richieste.conf_title} - - {t.richieste.conf_desc} - - - -

{t.richieste.conferma_txt}

-
- - { - mutate({ - annuncioId, - servizioId, - }); - }} - variant="success" - > - {t.conferma} - - - - - -
-
- ); -}; -const ConfermaLavorazioneModal = () => { - const { t } = useTranslation(); - const { data } = useServizioAnnuncio(); - return ( - - - - - - - In attesa di conferma - - Consegna status modal - - - -

- 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", - })} -

-

- La conferma è ancora in lavorazione. Stiamo verificando con i - proprietari le condizioni e dettagli della conferma per riferirteli. - Attendi una comunicazione. -

-
- - - - - - -
-
- ); -}; diff --git a/apps/infoalloggi/src/components/servizio/main.tsx b/apps/infoalloggi/src/components/servizio/main.tsx index 29c030b..950c44f 100644 --- a/apps/infoalloggi/src/components/servizio/main.tsx +++ b/apps/infoalloggi/src/components/servizio/main.tsx @@ -171,14 +171,19 @@ export const ServizioContent = () => { (a) => a.accettato_conferma_at !== null, ); - if (servizio.isOkSaldo) { + if (servizio.isOkSaldo && annuncioConfermato.length > 0) { return ( {annuncioConfermato.map((data) => { return ( - + ); @@ -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( (a) => a.open_contatti_at !== null && a.user_confirmed_at === null, ); @@ -275,14 +284,19 @@ export const ServizioContent = () => {
{annunciInConferma.map((data) => { return ( - + ); })}
- +
diff --git a/apps/infoalloggi/src/components/servizio/modale_contatti.tsx b/apps/infoalloggi/src/components/servizio/modale_contatti.tsx index 817eb2d..aa8d5cd 100644 --- a/apps/infoalloggi/src/components/servizio/modale_contatti.tsx +++ b/apps/infoalloggi/src/components/servizio/modale_contatti.tsx @@ -131,7 +131,7 @@ const DocCheckpoint = () => { }); const handleUpload = ( field: "doc_personale_fronte_ref" | "doc_personale_retro_ref", - id: string, + id: UsersStorageUserStorageId, ) => { mutate({ servizioId: servizio.servizio_id, diff --git a/apps/infoalloggi/src/components/servizio/servizio_actions.tsx b/apps/infoalloggi/src/components/servizio/servizio_actions.tsx index f0fb4bb..aededa6 100644 --- a/apps/infoalloggi/src/components/servizio/servizio_actions.tsx +++ b/apps/infoalloggi/src/components/servizio/servizio_actions.tsx @@ -196,7 +196,7 @@ export const AddAnnuncio = ({ }; const InterruzioneServizio = () => { - const { servizio, userId } = useServizio(); + const { servizio, userId, isAdmin } = useServizio(); const beforeInterruzioneLimit = servizio.decorrenza != null && @@ -223,9 +223,14 @@ const InterruzioneServizio = () => { @@ -255,7 +260,7 @@ const InterruzioneServizio = () => { }; const EditPreferenze = () => { - const { servizioId, userId, isAdmin } = useServizio(); + const { servizioId, userId, isAdmin, servizio } = useServizio(); const [openEditPreferenze, setOpenEditPreferenze] = useState(false); const { data, isLoading } = api.servizio.getServizio.useQuery({ servizioId }); const utils = api.useUtils(); @@ -287,7 +292,15 @@ const EditPreferenze = () => { return ( - diff --git a/apps/infoalloggi/src/components/servizio/servizio_annunci_accordions.tsx b/apps/infoalloggi/src/components/servizio/servizio_annunci_accordions.tsx index 894504b..fad45da 100644 --- a/apps/infoalloggi/src/components/servizio/servizio_annunci_accordions.tsx +++ b/apps/infoalloggi/src/components/servizio/servizio_annunci_accordions.tsx @@ -61,6 +61,7 @@ export const AnnunciInConfermaDialog = ({ }: { annunci: ServizioData["annunci"]; }) => { + const { servizioId, userId } = useServizio(); return ( {annunci.map((data) => { return ( - + ); @@ -99,6 +105,7 @@ export const AnnunciSelezionatiDialog = ({ }: { annunci: ServizioData["annunci"]; }) => { + const { servizioId, userId } = useServizio(); return ( 0 ? ( annunci.map((data) => { return ( - + ); @@ -152,6 +164,7 @@ export const AnnunciInConfermaAccordion = ({ }: { annunci: ServizioData["annunci"]; }) => { + const { servizioId, userId } = useServizio(); if (annunci.length === 0) { return null; // If there are no announcements in confirmation, do not render this section } @@ -166,7 +179,12 @@ export const AnnunciInConfermaAccordion = ({
{annunci.map((data) => { return ( - + ); @@ -183,6 +201,7 @@ export const AnnunciSelezionatiAccordion = ({ }: { annunci: ServizioData["annunci"]; }) => { + const { servizioId, userId } = useServizio(); return ( @@ -194,7 +213,12 @@ export const AnnunciSelezionatiAccordion = ({ {annunci.length > 0 ? ( annunci.map((data) => { return ( - + ); @@ -210,7 +234,8 @@ export const AnnunciSelezionatiAccordion = ({ > Annunci - + {" "} + o tra gli annunci compatibili qui sotto.
)} diff --git a/apps/infoalloggi/src/components/servizio/shared.tsx b/apps/infoalloggi/src/components/servizio/shared.tsx new file mode 100644 index 0000000..6049943 --- /dev/null +++ b/apps/infoalloggi/src/components/servizio/shared.tsx @@ -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 ( +
+ +
+
+ + + +
+ {children} +
+
+ ); +}; + +export const DocCombo = ({ + files, + onSelect, + current, +}: { + files: FileMetadataWithAdmin[]; + onSelect: (file: FileMetadataWithAdmin) => void; + current: string | null; +}) => { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + Nessun file. + + {files.map((file) => ( + { + if (currentValue !== current) { + // biome-ignore lint/style/noNonNullAssertion: + onSelect(files.find((f) => f.id === currentValue)!); + } + setOpen(false); + }} + value={file.id} + > + + + + {file.originalName} + + {current === file.id && ( + + )} + + + ))} + + + + + + ); +}; diff --git a/apps/infoalloggi/src/components/upload_modal.tsx b/apps/infoalloggi/src/components/upload_modal.tsx index 68fc81e..4a4242d 100644 --- a/apps/infoalloggi/src/components/upload_modal.tsx +++ b/apps/infoalloggi/src/components/upload_modal.tsx @@ -182,6 +182,7 @@ export const UploadComponent = ({ userId, from_admin, }); + if (!res) continue; userStorageIds.push(res.user_storage_id); } } diff --git a/apps/infoalloggi/src/forms/FormNewServizioAcquisto.tsx b/apps/infoalloggi/src/forms/FormNewServizioAcquisto.tsx index ab62864..a6c1584 100644 --- a/apps/infoalloggi/src/forms/FormNewServizioAcquisto.tsx +++ b/apps/infoalloggi/src/forms/FormNewServizioAcquisto.tsx @@ -108,11 +108,11 @@ const FormNewServizioSchema = z.object({ reddito: z.string().nullable(), sede_legale: z.string().nullable(), sesso: z.string(), - skipOrdine: z.boolean(), telefono: z.string(), terrazzo: z.boolean(), tipologia: z.custom(), scadenza_motivazione_transitoria: z.date().nullable(), + skipPayment: z.boolean(), }); type FormNewServizioValues = z.infer; @@ -308,7 +308,6 @@ export const FormNewServizioAcquisto = ({ sede_legale: initialData.anagrafica?.sede_legale || "", sesso: initialData.anagrafica?.sesso || "M", ...initialData.servizio, - skipOrdine: false, }; const { locale, t } = useTranslation(); @@ -337,7 +336,7 @@ export const FormNewServizioAcquisto = ({ }); if (isAdmin) { - if (data) { + if (data.ordineIdForPayment) { await router.push(`/area-riservata/admin/user-view/ordini/${userId}`); } else { await router.push( @@ -345,7 +344,11 @@ export const FormNewServizioAcquisto = ({ ); } } 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, terrazzo: fields.terrazzo, tipologia: fields.tipologia, + skipPayment: fields.skipPayment, }, servizioId: initialData.servizio.servizio_id, - skipOrdine: isAdmin ? fields.skipOrdine : false, user: { cognome: fields.cognome, email: fields.email, @@ -1427,11 +1430,11 @@ export const FormNewServizioAcquisto = ({ {isAdmin && ( (
- + Vuoi saltare la creazione dell'ordine e impostare il servizio come attivo? @@ -1439,7 +1442,7 @@ export const FormNewServizioAcquisto = ({ { field.onChange(v); }} diff --git a/apps/infoalloggi/src/pages/servizio/conferma-immobile/[...slug].tsx b/apps/infoalloggi/src/pages/servizio/conferma-immobile/[...slug].tsx index ce07825..947b9be 100644 --- a/apps/infoalloggi/src/pages/servizio/conferma-immobile/[...slug].tsx +++ b/apps/infoalloggi/src/pages/servizio/conferma-immobile/[...slug].tsx @@ -83,24 +83,25 @@ const ConfirmSection = ({ const [letto, setLetto] = useState(false); const [accettato, setAccettato] = useState(false); const utils = api.useUtils(); - const { mutate: conferma } = api.servizio.updateServizioAnnunci.useMutation({ - onMutate: () => { - const toastId = toast.loading(t.caricamento, { - icon: "🔄", - }); - return { toastId }; - }, - onSuccess: async (_error, _variables, context) => { - await utils.storage.retrieveUserFileData.invalidate(); + 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(); - toast.success("Confermato", { - icon: "👍", - id: context?.toastId, - }); + toast.success("Confermato", { + icon: "👍", + id: context?.toastId, + }); - await router.push("/area-riservata/dashboard"); - }, - }); + await router.push("/area-riservata/dashboard"); + }, + }); return ( <>
@@ -147,9 +148,6 @@ const ConfirmSection = ({ // Handle confirmation logic here conferma({ annuncioId, - data: { - accettato_conferma_at: new Date(), - }, servizioId, }); }} diff --git a/apps/infoalloggi/src/providers/ServizioProvider.tsx b/apps/infoalloggi/src/providers/ServizioProvider.tsx index d457c95..c65f3ba 100644 --- a/apps/infoalloggi/src/providers/ServizioProvider.tsx +++ b/apps/infoalloggi/src/providers/ServizioProvider.tsx @@ -1,9 +1,19 @@ import { createContext, useContext } from "react"; +import toast from "react-hot-toast"; import type { AnnunciId } from "~/schemas/public/Annunci"; import type { ServizioServizioId } from "~/schemas/public/Servizio"; +import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci"; import type { UsersId } from "~/schemas/public/Users"; 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 = { isAdmin: boolean; userId: UsersId; @@ -14,20 +24,11 @@ type ServizioContextProps = { const ServizioContext = createContext(null); export const ServizioProvider = ({ - isAdmin, - userId, - servizioId, - servizio, children, -}: { - isAdmin: boolean; - userId: UsersId; - servizioId: ServizioServizioId; - servizio: ServizioData; - children: React.ReactNode; -}) => { + ...props +}: ServizioProviderProps) => { return ( - + {children} ); @@ -41,23 +42,54 @@ export const useServizio = () => { return servizioContext; }; +type ServizioAnnuncioProviderProps = { + data: ServizioData["annunci"][number]; + servizioId: ServizioServizioId; + userId: UsersId; + children: React.ReactNode; +}; + type ServizioAnnuncioContextProps = { data: ServizioData["annunci"][number]; annuncioId: AnnunciId; + updateServizioAnnunci: (data: ServizioAnnunciUpdate) => Promise; }; const ServizioAnnuncioContext = createContext(null); export const ServizioAnnuncioProvider = ({ data, + servizioId, + userId, children, -}: { - data: ServizioData["annunci"][number]; - children: React.ReactNode; -}) => { +}: ServizioAnnuncioProviderProps) => { + const utils = api.useUtils(); + 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 ( {children} diff --git a/apps/infoalloggi/src/server/api/routers/servizio.ts b/apps/infoalloggi/src/server/api/routers/servizio.ts index 5e673b2..ae20c72 100644 --- a/apps/infoalloggi/src/server/api/routers/servizio.ts +++ b/apps/infoalloggi/src/server/api/routers/servizio.ts @@ -20,6 +20,7 @@ import { import { addServizio, addServizioAnnunci, + adminUpdateConfermaStatus, deleteServizio, deleteServizioAnnuncio, getAllPagamenti, @@ -47,7 +48,8 @@ import { updatePagamento, updateServizio, updateServizioAnnuncio, - userConfirmImmobile, + userAccettaConferma, + userSendConfermaIntent, } from "~/server/controllers/servizio.controller"; import { zAnnuncioId, @@ -256,7 +258,6 @@ export const servizioRouter = createTRPCRouter({ anagrafica: z.custom(), servizio: z.custom(), servizioId: zServizioId, - skipOrdine: z.boolean().optional(), user: z.custom< Pick >(), @@ -420,7 +421,22 @@ export const servizioRouter = createTRPCRouter({ 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( z.object({ annuncioId: zAnnuncioId, @@ -428,7 +444,20 @@ export const servizioRouter = createTRPCRouter({ }), ) .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, servizioId: input.servizioId, }); diff --git a/apps/infoalloggi/src/server/api/routers/storage.ts b/apps/infoalloggi/src/server/api/routers/storage.ts index 39292eb..c5eb687 100644 --- a/apps/infoalloggi/src/server/api/routers/storage.ts +++ b/apps/infoalloggi/src/server/api/routers/storage.ts @@ -1,6 +1,9 @@ import { TRPCError } from "@trpc/server"; import z from "zod"; -import type { NewUsersStorage } from "~/schemas/public/UsersStorage"; +import type { + NewUsersStorage, + UsersStorageUserStorageId, +} from "~/schemas/public/UsersStorage"; import { adminProcedure, createTRPCRouter, @@ -11,6 +14,7 @@ import { deleteUserStorageEntry, getAllWithFromAdmin, getMultipleWithRelations, + getStorageIdFromUserStorageId, purgeFileFromUserStorages, retrieveUserFiles, } from "~/server/controllers/storage.controller"; @@ -83,4 +87,11 @@ export const storageRouter = createTRPCRouter({ .mutation(async ({ input }) => { return await editFile(input.fileId, input.name); }), + getStorageIdFromUserStorageId: protectedProcedure + .input(z.object({ userStorageId: z.custom() })) + .query(async ({ input }) => { + return await getStorageIdFromUserStorageId({ + userStorageId: input.userStorageId, + }); + }), }); diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts index 79aaae2..6aed8aa 100644 --- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts +++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts @@ -237,15 +237,13 @@ export const postAcquistoDataHandler = async ({ user, anagrafica, servizio, - skipOrdine = false, }: { servizioId: ServizioServizioId; userId: UsersId; user: Pick; anagrafica: UsersAnagraficaUpdate; servizio: ServizioUpdate; - skipOrdine?: boolean; -}) => { +}): Promise<{ ordineIdForPayment: OrdiniOrdineId | null }> => { try { const servizioData = await db.transaction().execute(async (tx) => { // Update user and anagrafica data @@ -285,16 +283,18 @@ export const postAcquistoDataHandler = async ({ .executeTakeFirstOrThrow(); }); - if (skipOrdine) { + if (servizioData.skipPayment) { // If skipping order, set servizio as active with decorrenza today await updateServizio({ data: { decorrenza: new Date(), isOkAcconto: true, + isOkSaldo: true, + isOkConsulenza: true, }, servizioId, }); - return null; + return { ordineIdForPayment: null }; } //cleanup unused payments @@ -350,7 +350,7 @@ export const postAcquistoDataHandler = async ({ .where("ordini.isActive", "=", false) .where("ordine_id", "!=", existingOrder.ordine_id) .execute(); - return existingOrder.ordine_id; + return { ordineIdForPayment: existingOrder.ordine_id }; } const order = await db @@ -364,7 +364,7 @@ export const postAcquistoDataHandler = async ({ .returning("ordine_id") .executeTakeFirstOrThrow(); - return order.ordine_id; + return { ordineIdForPayment: order.ordine_id }; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -790,44 +790,6 @@ export const updateServizioAnnuncio = async ({ .where("annunci_id", "=", annuncioId) .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; } catch (e) { throw new TRPCError({ @@ -1241,7 +1203,7 @@ export const sendServizioEmail = async ({ } }; -export const userConfirmImmobile = async ({ +export const userSendConfermaIntent = async ({ servizioId, 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) => { try { const ordine = await db @@ -1529,6 +1586,7 @@ export const getCompatibileAnnunci = async ( let qry = db .selectFrom("annunci") + .distinctOn("annunci.id") .select((eb) => [ "id", "codice", diff --git a/apps/infoalloggi/src/server/controllers/storage.controller.ts b/apps/infoalloggi/src/server/controllers/storage.controller.ts index d0a7419..f8b9773 100644 --- a/apps/infoalloggi/src/server/controllers/storage.controller.ts +++ b/apps/infoalloggi/src/server/controllers/storage.controller.ts @@ -1,6 +1,9 @@ import { TRPCError } from "@trpc/server"; 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 { 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 () => { try { const files = await db @@ -115,7 +138,7 @@ export const addUserStorage = async (data: NewUsersStorage) => { .values(data) .onConflict((oc) => oc.columns(["userId", "storageId"]).doNothing()) .returning("user_storage_id") - .executeTakeFirstOrThrow(); + .executeTakeFirst(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR",