From bd5a35a17261c4f98bd89a8d04f65f3a754b1acb Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Wed, 29 Oct 2025 09:39:06 +0100 Subject: [PATCH] Refactor service document handling and contact modal - Removed temp_tokens data from SQL initialization. - Updated AllegatiComp to invalidate additional service announcements on file removal. - Simplified imports in interactions.tsx and removed unused components. - Enhanced ServizioContent layout for better display of announcements. - Adjusted button styles in servizio_annunci_accordions for responsiveness. - Streamlined ricerca-table by removing unnecessary ServizioProvider wrapping. - Modified upload modal to support new upload callback structure. - Added new document reference fields to Servizio schema. - Introduced UsersStorageUserStorageId type for better type safety. - Updated servizio.controller to include document references in service announcements. - Changed storage.controller to return user_storage_id on conflict. - Updated docker-compose to streamline storage service configuration. - Created migration script for new foreign key constraints on document references. - Implemented ContattiProprietarioModal to manage document uploads and contact unlocking. --- apps/db/initial/1_init.sql | 176 ++++---- apps/db/initial/2_data.sql | 11 - apps/infoalloggi/migration.sql | 20 + .../components/area-riservata/allegati.tsx | 2 + .../src/components/servizio/interactions.tsx | 231 +---------- .../src/components/servizio/main.tsx | 19 +- .../components/servizio/modale_contatti.tsx | 378 ++++++++++++++++++ .../servizio/servizio_annunci_accordions.tsx | 12 +- .../src/components/tables/ricerca-table.tsx | 9 +- .../src/components/upload_modal.tsx | 41 +- .../src/schemas/public/Servizio.ts | 7 + .../src/schemas/public/UsersStorage.ts | 5 + .../server/controllers/servizio.controller.ts | 39 ++ .../server/controllers/storage.controller.ts | 4 +- ref-docker-compose.yml | 31 +- 15 files changed, 582 insertions(+), 403 deletions(-) create mode 100644 apps/infoalloggi/migration.sql create mode 100644 apps/infoalloggi/src/components/servizio/modale_contatti.tsx diff --git a/apps/db/initial/1_init.sql b/apps/db/initial/1_init.sql index 6876881..a3314ff 100644 --- a/apps/db/initial/1_init.sql +++ b/apps/db/initial/1_init.sql @@ -146,17 +146,16 @@ CREATE TABLE public.annunci ( stato text, web boolean, caratteristiche json, - url_immagini text[], homepage boolean, url_video text[], email text, creato_il timestamp without time zone, modificato_il timestamp without time zone, consegna integer, - og_url text, disponibile_da date, permanenza integer[], - persone text[] + persone text[], + updated_at timestamp without time zone ); @@ -394,6 +393,21 @@ CREATE TABLE public.flags ( ALTER TABLE public.flags OWNER TO postgres; +-- +-- Name: images_refs; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.images_refs ( + codice text NOT NULL, + ordine integer NOT NULL, + img text NOT NULL, + thumb text NOT NULL, + og_url text NOT NULL +); + + +ALTER TABLE public.images_refs OWNER TO postgres; + -- -- Name: messages; Type: TABLE; Schema: public; Owner: postgres -- @@ -410,18 +424,6 @@ CREATE TABLE public.messages ( ALTER TABLE public.messages OWNER TO postgres; --- --- Name: miogest_images_ref; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.miogest_images_ref ( - codice text NOT NULL, - foto text[] -); - - -ALTER TABLE public.miogest_images_ref OWNER TO postgres; - -- -- Name: ordini; Type: TABLE; Schema: public; Owner: postgres -- @@ -526,7 +528,10 @@ CREATE TABLE public.servizio ( permanenza integer, "isOkAcconto" boolean DEFAULT false NOT NULL, "isOkSaldo" boolean DEFAULT false NOT NULL, - "isOkConsulenza" boolean DEFAULT false NOT NULL + "isOkConsulenza" boolean DEFAULT false NOT NULL, + doc_personale_fronte_ref uuid, + doc_personale_retro_ref uuid, + doc_motivazione_ref uuid ); @@ -541,7 +546,7 @@ CREATE TABLE public.servizio_annunci ( annunci_id integer NOT NULL, created_at timestamp without time zone DEFAULT now() NOT NULL, open_contatti_at timestamp without time zone, - doc_conferma_ref uuid, + doc_conferma_ref text, user_confirmed_at timestamp without time zone, accettato_conferma_at timestamp without time zone, caparra_causale text, @@ -549,12 +554,12 @@ CREATE TABLE public.servizio_annunci ( caparra_importo text, caparra_intestazione text, "hasConfermaAdmin" boolean DEFAULT false NOT NULL, - doc_contratto_ref uuid, + doc_contratto_ref text, gestionale_id integer, contratto_decorrenza date, contratto_scadenza date, contratto_tipo text, - doc_registrazione_ref uuid, + doc_registrazione_ref text, doc_conferma_added boolean DEFAULT false NOT NULL, doc_contratto_added boolean DEFAULT false NOT NULL, doc_registrazione_added boolean DEFAULT false NOT NULL @@ -575,34 +580,6 @@ CREATE TABLE public.servizio_interessi ( ALTER TABLE public.servizio_interessi OWNER TO postgres; --- --- Name: storageindex; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.storageindex ( - id uuid DEFAULT gen_random_uuid() NOT NULL, - created_at timestamp without time zone DEFAULT now() NOT NULL, - filename text, - ext text, - from_admin boolean DEFAULT false NOT NULL, - expires_at timestamp without time zone -); - - -ALTER TABLE public.storageindex OWNER TO postgres; - --- --- Name: temp_tokens; Type: TABLE; Schema: public; Owner: postgres --- - -CREATE TABLE public.temp_tokens ( - token uuid DEFAULT gen_random_uuid() NOT NULL, - elapse timestamp without time zone DEFAULT (now() + '00:00:30'::interval) NOT NULL -); - - -ALTER TABLE public.temp_tokens OWNER TO postgres; - -- -- Name: testi_e_stringhe; Type: TABLE; Schema: public; Owner: postgres -- @@ -677,8 +654,10 @@ ALTER TABLE public.users_anagrafica OWNER TO postgres; -- CREATE TABLE public.users_storage ( - userid uuid NOT NULL, - storageid uuid NOT NULL + "userId" uuid NOT NULL, + "storageId" text NOT NULL, + from_admin boolean DEFAULT false NOT NULL, + user_storage_id uuid DEFAULT gen_random_uuid() NOT NULL ); @@ -791,6 +770,14 @@ ALTER TABLE ONLY public.event_queue ADD CONSTRAINT event_queue_pkey PRIMARY KEY (event_id); +-- +-- Name: images_refs img_cod_order; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.images_refs + ADD CONSTRAINT img_cod_order UNIQUE (codice, ordine); + + -- -- Name: messages messages_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -799,14 +786,6 @@ ALTER TABLE ONLY public.messages ADD CONSTRAINT messages_pkey PRIMARY KEY (messageid); --- --- Name: miogest_images_ref miogest_images_ref_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.miogest_images_ref - ADD CONSTRAINT miogest_images_ref_pkey PRIMARY KEY (codice); - - -- -- Name: ordini ordini2_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -847,30 +826,6 @@ ALTER TABLE ONLY public.flags ADD CONSTRAINT sitesettings_pkey PRIMARY KEY (id); --- --- Name: users_storage sotrageuser; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.users_storage - ADD CONSTRAINT sotrageuser UNIQUE (userid, storageid); - - --- --- Name: storageindex storageindex_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.storageindex - ADD CONSTRAINT storageindex_pkey PRIMARY KEY (id); - - --- --- Name: temp_tokens temp_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.temp_tokens - ADD CONSTRAINT temp_tokens_pkey PRIMARY KEY (token); - - -- -- Name: testi_e_stringhe testi_e_stringhe_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -895,6 +850,14 @@ ALTER TABLE ONLY public.servizio_annunci ADD CONSTRAINT uniq_annuncio_servizio UNIQUE (annunci_id, servizio_id); +-- +-- Name: users_storage unique_user_storage; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.users_storage + ADD CONSTRAINT unique_user_storage UNIQUE ("userId", "storageId"); + + -- -- Name: servizio_interessi uniqui_interest; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -919,6 +882,14 @@ ALTER TABLE ONLY public.users ADD CONSTRAINT users_pkey PRIMARY KEY (id); +-- +-- Name: users_storage users_storage_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.users_storage + ADD CONSTRAINT users_storage_pkey PRIMARY KEY (user_storage_id); + + -- -- Name: users_anagrafica usersanagrafica2_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -935,13 +906,6 @@ ALTER TABLE ONLY public.users_anagrafica ADD CONSTRAINT usserunique2 UNIQUE (userid); --- --- Name: annunci_immagini_index; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE INDEX annunci_immagini_index ON public.annunci USING gin (url_immagini); - - -- -- Name: ratelimiter_key_index; Type: INDEX; Schema: public; Owner: postgres -- @@ -957,6 +921,14 @@ ALTER TABLE ONLY public.chats_etichette ADD CONSTRAINT chat_etichetta_fkey FOREIGN KEY (chatid) REFERENCES public.chats(chatid) ON UPDATE CASCADE ON DELETE CASCADE; +-- +-- Name: images_refs cod_image; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.images_refs + ADD CONSTRAINT cod_image FOREIGN KEY (codice) REFERENCES public.annunci(codice); + + -- -- Name: emails email_user; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- @@ -1070,27 +1042,27 @@ ALTER TABLE ONLY public.servizio_annunci -- --- Name: servizio_annunci servizio_annuncio_conferma_doc; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- Name: servizio servizio_doc_fronte; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- -ALTER TABLE ONLY public.servizio_annunci - ADD CONSTRAINT servizio_annuncio_conferma_doc FOREIGN KEY (doc_conferma_ref) REFERENCES public.storageindex(id) ON UPDATE CASCADE ON DELETE SET NULL NOT VALID; +ALTER TABLE ONLY public.servizio + ADD CONSTRAINT servizio_doc_fronte FOREIGN KEY (doc_personale_fronte_ref) REFERENCES public.users_storage(user_storage_id) ON UPDATE CASCADE ON DELETE SET NULL NOT VALID; -- --- Name: servizio_annunci servizio_annuncio_contratto_doc; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- Name: servizio servizio_doc_motivazione; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- -ALTER TABLE ONLY public.servizio_annunci - ADD CONSTRAINT servizio_annuncio_contratto_doc FOREIGN KEY (doc_contratto_ref) REFERENCES public.storageindex(id) ON UPDATE CASCADE ON DELETE SET NULL NOT VALID; +ALTER TABLE ONLY public.servizio + ADD CONSTRAINT servizio_doc_motivazione FOREIGN KEY (doc_motivazione_ref) REFERENCES public.users_storage(user_storage_id) ON UPDATE CASCADE ON DELETE SET NULL NOT VALID; -- --- Name: servizio_annunci servizio_annuncio_registraz_doc; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- Name: servizio servizio_doc_retro; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- -ALTER TABLE ONLY public.servizio_annunci - ADD CONSTRAINT servizio_annuncio_registraz_doc FOREIGN KEY (doc_registrazione_ref) REFERENCES public.storageindex(id) ON UPDATE CASCADE ON DELETE SET NULL NOT VALID; +ALTER TABLE ONLY public.servizio + ADD CONSTRAINT servizio_doc_retro FOREIGN KEY (doc_personale_retro_ref) REFERENCES public.users_storage(user_storage_id) ON UPDATE CASCADE ON DELETE SET NULL NOT VALID; -- @@ -1117,20 +1089,12 @@ ALTER TABLE ONLY public.users_anagrafica ADD CONSTRAINT useridfkeyanagrafica2 FOREIGN KEY (userid) REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID; --- --- Name: users_storage users_storage_storageid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.users_storage - ADD CONSTRAINT users_storage_storageid_fkey FOREIGN KEY (storageid) REFERENCES public.storageindex(id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID; - - -- -- Name: users_storage userstorage; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- ALTER TABLE ONLY public.users_storage - ADD CONSTRAINT userstorage FOREIGN KEY (userid) REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE; + ADD CONSTRAINT userstorage FOREIGN KEY ("userId") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE CASCADE; -- diff --git a/apps/db/initial/2_data.sql b/apps/db/initial/2_data.sql index 2f9885a..cdb4047 100644 --- a/apps/db/initial/2_data.sql +++ b/apps/db/initial/2_data.sql @@ -87,17 +87,6 @@ SALDOCA500+ 38700 CONDIZIONI_CERCHI t Saldo Cerco Stabile (500+) Stable 500+ Sal \. --- --- Data for Name: temp_tokens; Type: TABLE DATA; Schema: public; Owner: postgres --- - -COPY public.temp_tokens (token, elapse) FROM stdin; -c256c17d-3299-47a4-b3ef-640aa9688b38 2025-08-22 13:01:41.20164 -4bd62a5f-aa6d-403b-a1bb-c0e11e382adc 2025-08-22 13:01:48.897271 -5d069230-9572-4b26-aa06-2964f3ca79a2 2030-02-19 10:25:33.046587 -\. - - -- -- Data for Name: testi_e_stringhe; Type: TABLE DATA; Schema: public; Owner: postgres -- diff --git a/apps/infoalloggi/migration.sql b/apps/infoalloggi/migration.sql new file mode 100644 index 0000000..6a953f6 --- /dev/null +++ b/apps/infoalloggi/migration.sql @@ -0,0 +1,20 @@ +ALTER TABLE + IF EXISTS public.servizio +ADD + CONSTRAINT servizio_doc_fronte FOREIGN KEY (doc_personale_fronte_ref) REFERENCES public.users_storage ("storageId") MATCH SIMPLE ON UPDATE CASCADE ON DELETE +SET + NULL NOT VALID; + +ALTER TABLE + IF EXISTS public.servizio +ADD + CONSTRAINT servizio_doc_retro FOREIGN KEY (doc_personale_retro_ref) REFERENCES public.users_storage ("storageId") MATCH SIMPLE ON UPDATE CASCADE ON DELETE +SET + NULL NOT VALID; + +ALTER TABLE + IF EXISTS public.servizio +ADD + CONSTRAINT servizio_doc_motivazione FOREIGN KEY (doc_motivazione_ref) REFERENCES public.users_storage ("storageId") MATCH SIMPLE ON UPDATE CASCADE ON DELETE +SET + NULL NOT VALID; \ No newline at end of file diff --git a/apps/infoalloggi/src/components/area-riservata/allegati.tsx b/apps/infoalloggi/src/components/area-riservata/allegati.tsx index ce81201..b313be2 100644 --- a/apps/infoalloggi/src/components/area-riservata/allegati.tsx +++ b/apps/infoalloggi/src/components/area-riservata/allegati.tsx @@ -66,6 +66,8 @@ export const AllegatiComp = ({ await utils.storage.retrieveUserFileData.invalidate({ userId: userId, }); + await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); + toast.success(t.allegati.allegato_rimosso, { icon: "🗑️", id: context?.toastId, diff --git a/apps/infoalloggi/src/components/servizio/interactions.tsx b/apps/infoalloggi/src/components/servizio/interactions.tsx index 987ee97..6df60a8 100644 --- a/apps/infoalloggi/src/components/servizio/interactions.tsx +++ b/apps/infoalloggi/src/components/servizio/interactions.tsx @@ -1,18 +1,14 @@ import { CircleCheck, - CircleUser, FileBadge, FileText, Handshake, - Navigation, PackageCheck, - Phone, Pointer, - TriangleAlert, } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import toast from "react-hot-toast"; import { Credenza, @@ -26,7 +22,6 @@ import { CredenzaTrigger, } from "~/components/custom_ui/credenza"; import LoadingButton from "~/components/custom_ui/loading-button"; -import { LoadingPage } from "~/components/loading"; import { Button, type ButtonConfigs } from "~/components/ui/button"; import { Tooltip, @@ -39,7 +34,7 @@ import { useTranslation } from "~/providers/I18nProvider"; import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider"; import type { AnnunciId } from "~/schemas/public/Annunci"; import { api } from "~/utils/api"; -import { WhatsAppIcon2 } from "../svgs"; +import { ContattiProprietarioModal } from "./modale_contatti"; export const Interactions = () => { const { servizio, servizioId } = useServizio(); @@ -406,225 +401,3 @@ const ConfermaLavorazioneModal = () => { ); }; - -const ContattiProprietarioModal = () => { - const { servizioId, userId } = useServizio(); - const { data, annuncioId } = useServizioAnnuncio(); - const { t } = useTranslation(); - const [showContacts, setShowContacts] = useState( - data.open_contatti_at != null, - ); - const utils = api.useUtils(); - - const { mutate } = api.servizio.unlockServizioContatti.useMutation({ - onError: (err) => { - toast.error(err.message, { - id: "unlock-contacts", - }); - }, - onMutate: () => { - toast.loading("Caricamento ...", { - id: "unlock-contacts", - }); - }, - onSuccess: async (data) => { - if (!data) { - toast.error("Limite di sblocchi raggiunto", { - id: "unlock-contacts", - }); - return; - } - toast.success("Successo", { - id: "unlock-contacts", - }); - await utils.servizio.getAllServizioAnnunci.invalidate({ - userId, - }); - setShowContacts(true); - }, - }); - - const d = new Date(); - const tooEarly = d.getHours() < 9; - const tooLate = d.getHours() >= 20; - - return ( - - - {showContacts ? ( - - ) : ( - - )} - - - - {t.contatto.title} - - {t.contatto.descrizione} - - - - {showContacts ? ( - - ) : ( -
-

{t.contatto.disclaimer}

-

{t.contatto.disclaimer2}

-

- {t.contatto.disclaimer3} -

- {(tooEarly || tooLate) && ( -
- - {/*TODO testare */} -

- Il proprietario riceverĂ  la notifica alle ore 9.00{" "} - {tooLate && "di domani"} -

-
- )} -
- )} -
- - {!showContacts && ( - - )} - - - - -
-
- ); -}; - -const ContattiProprietarioData = () => { - const { servizioId } = useServizio(); - const { annuncioId } = useServizioAnnuncio(); - const { data, isLoading } = api.annunci.getProprietarioData.useQuery({ - annuncioId, - servizioId, - }); - const [navigationUrl, setNavigationUrl] = useState( - undefined, - ); - const { t } = useTranslation(); - useEffect(() => { - if (!data || !data.propData) return; - if (typeof window === "undefined") return; - if ( - navigator.userAgent.toUpperCase().includes("MAC") || - navigator.userAgent.toUpperCase().includes("IPAD") || - navigator.userAgent.toUpperCase().includes("IPHONE") || - navigator.userAgent.toUpperCase().includes("IOS") || - navigator.userAgent.toUpperCase().includes("IPOD") - ) { - setNavigationUrl( - `maps://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${data.propData.lat},${data.propData.lon}`, - ); - } else { - setNavigationUrl( - `https://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${data.propData.lat},${data.propData.lon}`, - ); - } - }, [data]); - - if (isLoading) { - return ( -
- -
- ); - } - if (!data || !data.propData) { - return

{t.contatto.error}

; - } - - return ( -
-

{t.contatto.propietario}:

-

- {t.contatto.nome}: {data.propData.locatore} -

- - : - {data.propData.numero} - - - - -
-

{t.contatto.immobile}:

-

- {t.contatto.indirizzo}: {data.propData.indirizzo},{" "} - {data.propData.civico} {data.propData.comune} ({data.propData.provincia} - ) -

- - {t.contatto.apri_nav} - -
-

- {t.contatto.sbloccato_il}:{" "} - {data.opened_at.toLocaleString("it", { - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - month: "2-digit", - year: "numeric", - })} -

-
- -
-

{t.contatto.contatta}

-

{t.contatto.orari}

-
-
-
- ); -}; diff --git a/apps/infoalloggi/src/components/servizio/main.tsx b/apps/infoalloggi/src/components/servizio/main.tsx index 10a72db..06ba4d9 100644 --- a/apps/infoalloggi/src/components/servizio/main.tsx +++ b/apps/infoalloggi/src/components/servizio/main.tsx @@ -24,7 +24,6 @@ import { import { api } from "~/utils/api"; import { AnnunciCompatibili, - AnnunciInConferma, AnnunciSelezionati, } from "./servizio_annunci_accordions"; @@ -255,11 +254,21 @@ 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 new file mode 100644 index 0000000..89dec7a --- /dev/null +++ b/apps/infoalloggi/src/components/servizio/modale_contatti.tsx @@ -0,0 +1,378 @@ +import { + CircleCheck, + CircleUser, + Navigation, + Phone, + TriangleAlert, +} from "lucide-react"; +import Link from "next/link"; +import { useMemo } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "~/providers/I18nProvider"; +import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider"; +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 { WhatsAppIcon2 } from "../svgs"; +import { Button } from "../ui/button"; +import { type UploadCallback, UploadModal } from "../upload_modal"; + +export const ContattiProprietarioModal = () => { + const { servizio } = useServizio(); + const { data } = useServizioAnnuncio(); + const { t } = useTranslation(); + return ( + + + {data.open_contatti_at != null ? ( + + ) : ( + + )} + + + + {t.contatto.title} + + {t.contatto.descrizione} + + + {servizio.doc_personale_fronte == null || + servizio.doc_personale_retro == null ? ( + + // biome-ignore lint/style/noNestedTernary: + ) : data.open_contatti_at == null ? ( + + ) : ( + + )} + + + ); +}; + +const DocComponent = ({ + docInfo, + cb, +}: { + docInfo: { + storageId: string; + ref: UsersStorageUserStorageId | null; + } | null; + cb: UploadCallback; +}) => { + const { isAdmin, userId } = useServizio(); + + if (!docInfo?.storageId) { + return ; + } + return ( +
+ + Documento caricato +
+ ); +}; + +const DocCheckpoint = () => { + const { t } = useTranslation(); + const { userId, servizio } = useServizio(); + const utils = api.useUtils(); + const { mutate } = 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}`); + }, + }); + return ( + <> + +
+

Devi ancora caricare un tuo documento

+
+ Fronte documento:{" "} + + userStorageIds.length > 0 && + mutate({ + servizioId: servizio.servizio_id, + data: { + doc_personale_fronte_ref: userStorageIds[0], + }, + }) + } + docInfo={servizio.doc_personale_fronte} + /> +
+
+ Retro documento:{" "} + + userStorageIds.length > 0 && + mutate({ + servizioId: servizio.servizio_id, + data: { + doc_personale_retro_ref: userStorageIds[0], + }, + }) + } + docInfo={servizio.doc_personale_retro} + /> +
+ +
+
+

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

+
+
+ + + + + + + ); +}; + +const PreUnlock = () => { + const { userId, servizioId } = useServizio(); + const { annuncioId } = useServizioAnnuncio(); + const { t } = useTranslation(); + + const utils = api.useUtils(); + + const { mutate } = api.servizio.unlockServizioContatti.useMutation({ + onError: (err) => { + toast.error(err.message, { + id: "unlock-contacts", + }); + }, + onMutate: () => { + toast.loading("Caricamento ...", { + id: "unlock-contacts", + }); + }, + onSuccess: async (data) => { + if (!data) { + toast.error("Limite di sblocchi raggiunto", { + id: "unlock-contacts", + }); + return; + } + toast.success("Successo", { + id: "unlock-contacts", + }); + await utils.servizio.getAllServizioAnnunci.invalidate({ + userId, + }); + }, + }); + + const d = new Date(); + const tooEarly = d.getHours() < 9; + const tooLate = d.getHours() >= 20; + return ( + <> + +
+

{t.contatto.disclaimer}

+

{t.contatto.disclaimer2}

+

+ {t.contatto.disclaimer3} +

+ {(tooEarly || tooLate) && ( +
+ + {/*TODO testare */} +

+ Il proprietario riceverĂ  la notifica alle ore 9.00{" "} + {tooLate && "di domani"} +

+
+ )} +
+
+ + + + + + + + + ); +}; + +function getNavigationUrl(lat: string, lon: string): string { + if (typeof window !== "undefined") { + const isIOS = + navigator.userAgent.toUpperCase().includes("MAC") || + navigator.userAgent.toUpperCase().includes("IPAD") || + navigator.userAgent.toUpperCase().includes("IPHONE") || + navigator.userAgent.toUpperCase().includes("IOS") || + navigator.userAgent.toUpperCase().includes("IPOD"); + + return `${isIOS ? "maps" : "https"}://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${lat},${lon}`; + } + return `https://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${lat},${lon}`; +} + +const PostUnlock = () => { + const { servizioId } = useServizio(); + const { annuncioId } = useServizioAnnuncio(); + const { data, isLoading } = api.annunci.getProprietarioData.useQuery({ + annuncioId, + servizioId, + }); + + const { t } = useTranslation(); + + // Memoize navigation URL + const navigationUrl = useMemo(() => { + if (!data?.propData || !data.propData.lat || !data.propData.lon) + return undefined; + return getNavigationUrl(data.propData.lat, data.propData.lon); + }, [data?.propData]); + + if (isLoading) { + return ( +
+ +
+ ); + } + if (!data || !data.propData) { + return

{t.contatto.error}

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

{t.contatto.propietario}:

+

+ {t.contatto.nome}: {data.propData.locatore} +

+
+ + : + + {data.propData.numero} + + + + + +
+ +
+

{t.contatto.immobile}:

+

+ {t.contatto.indirizzo}: {data.propData.indirizzo},{" "} + {data.propData.civico} {data.propData.comune} ( + {data.propData.provincia}) +

+ + {t.contatto.apri_nav} + +
+

+ {t.contatto.sbloccato_il}:{" "} + {data.opened_at.toLocaleString("it", { + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + month: "2-digit", + year: "numeric", + })} +

+
+ +
+

{t.contatto.contatta}

+

{t.contatto.orari}

+
+
+
+
+ + + + + + + ); +}; diff --git a/apps/infoalloggi/src/components/servizio/servizio_annunci_accordions.tsx b/apps/infoalloggi/src/components/servizio/servizio_annunci_accordions.tsx index b286377..dd9c808 100644 --- a/apps/infoalloggi/src/components/servizio/servizio_annunci_accordions.tsx +++ b/apps/infoalloggi/src/components/servizio/servizio_annunci_accordions.tsx @@ -104,8 +104,12 @@ export const AnnunciSelezionati = ({ - {annunci.length > 0 && ( @@ -159,8 +163,8 @@ export const AnnunciCompatibili = () => { - {data && data.length > 0 && ( diff --git a/apps/infoalloggi/src/components/tables/ricerca-table.tsx b/apps/infoalloggi/src/components/tables/ricerca-table.tsx index 2567c47..b150fad 100644 --- a/apps/infoalloggi/src/components/tables/ricerca-table.tsx +++ b/apps/infoalloggi/src/components/tables/ricerca-table.tsx @@ -108,14 +108,7 @@ const Row = ({
- - - + diff --git a/apps/infoalloggi/src/components/upload_modal.tsx b/apps/infoalloggi/src/components/upload_modal.tsx index 97e4062..0aec761 100644 --- a/apps/infoalloggi/src/components/upload_modal.tsx +++ b/apps/infoalloggi/src/components/upload_modal.tsx @@ -25,10 +25,16 @@ import { Button } from "~/components/ui/button"; import { uploadHandler } from "~/hooks/storageClienSideHooks"; import { useTranslation } from "~/providers/I18nProvider"; import type { UsersId } from "~/schemas/public/Users"; +import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage"; import { api } from "~/utils/api"; +export type UploadCallback = (args: { + storageIds: string[]; + userStorageIds: UsersStorageUserStorageId[]; +}) => void; + type UploadModalProps = { - cb_onUpload?: () => void; + cb_onUpload?: UploadCallback; isAdmin?: boolean; userId?: UsersId; }; @@ -69,7 +75,7 @@ export const UploadModal = ({ }; type UploadComponentProps = { - cb_onUpload?: () => void; + cb_onUpload?: UploadCallback; isAdmin?: boolean; userId?: UsersId; maxMB?: number; @@ -90,7 +96,10 @@ export const UploadComponent = ({ const { mutateAsync: setUserStorage } = api.storage.addUserStorageEntry.useMutation({ onMutate: () => { - toast(t.allegati.allegato_caricato, { icon: "📤" }); + toast(t.allegati.allegato_caricato, { + icon: "📤", + toasterId: "uploading-toast", + }); }, onSettled: async () => { await utils.storage.getAll.invalidate(); @@ -160,6 +169,8 @@ export const UploadComponent = ({ if (files.length === 0) { return; } + let storageIds: string[] = []; + const userStorageIds: UsersStorageUserStorageId[] = []; try { const from_admin = isAdmin ?? true; setIsUploading(true); @@ -168,14 +179,24 @@ export const UploadComponent = ({ files, from_admin, }); + if (!status) { + toast.error(t.allegati.errore_caricamento, { + icon: "❌", + toasterId: "uploading-toast", + }); + setIsUploading(false); + return; + } + storageIds = attachments; if (userId) { for (const storageId of attachments) { - await setUserStorage({ + const res = await setUserStorage({ storageId, userId, from_admin, }); + userStorageIds.push(res.user_storage_id); } } @@ -193,16 +214,9 @@ export const UploadComponent = ({ }, randomDelay / 1); // Adjust interval based on random delay }); - if (!status) { - toast.error(t.allegati.errore_caricamento, { - icon: "❌", - }); - setIsUploading(false); - return; - } - toast.success(t.allegati.success_caricamento, { icon: "✅", + toasterId: "uploading-toast", }); } catch (error) { setIsUploading(false); @@ -211,11 +225,12 @@ export const UploadComponent = ({ error instanceof Error ? error.message : t.allegati.errore_sconosciuto, + { icon: "❌", toasterId: "uploading-toast" }, ); } finally { setIsUploading(false); setFiles([]); - cb_onUpload?.(); + cb_onUpload?.({ storageIds, userStorageIds }); await utils.storage.getAll.invalidate(); } }, diff --git a/apps/infoalloggi/src/schemas/public/Servizio.ts b/apps/infoalloggi/src/schemas/public/Servizio.ts index bdbdd80..ac9dfd7 100644 --- a/apps/infoalloggi/src/schemas/public/Servizio.ts +++ b/apps/infoalloggi/src/schemas/public/Servizio.ts @@ -3,6 +3,7 @@ import type { UsersId } from './Users'; import type { default as TipologiaPosizioneEnum } from './TipologiaPosizioneEnum'; +import type { UsersStorageUserStorageId } from './UsersStorage'; import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; /** Identifier type for public.servizio */ @@ -59,6 +60,12 @@ export default interface ServizioTable { isOkSaldo: ColumnType; isOkConsulenza: ColumnType; + + doc_personale_fronte_ref: ColumnType; + + doc_personale_retro_ref: ColumnType; + + doc_motivazione_ref: ColumnType; } export type Servizio = Selectable; diff --git a/apps/infoalloggi/src/schemas/public/UsersStorage.ts b/apps/infoalloggi/src/schemas/public/UsersStorage.ts index 1d7e29f..5c91806 100644 --- a/apps/infoalloggi/src/schemas/public/UsersStorage.ts +++ b/apps/infoalloggi/src/schemas/public/UsersStorage.ts @@ -4,8 +4,13 @@ import type { UsersId } from './Users'; import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; +/** Identifier type for public.users_storage */ +export type UsersStorageUserStorageId = string & { __brand: 'public.users_storage' }; + /** Represents the table public.users_storage */ export default interface UsersStorageTable { + user_storage_id: ColumnType; + userId: ColumnType; storageId: ColumnType; diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts index 2c86402..053c78c 100644 --- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts +++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts @@ -627,6 +627,45 @@ export const getAllServizioAnnunci = async (userId: UsersId) => { "servizio.isOkAcconto", "servizio.isOkSaldo", "servizio.isOkConsulenza", + jsonObjectFrom( + eb + .selectFrom("users_storage") + .select([ + "users_storage.storageId as storageId", + "servizio.doc_personale_fronte_ref as ref", + ]) + .whereRef( + "users_storage.user_storage_id", + "=", + "servizio.doc_personale_fronte_ref", + ), + ).as("doc_personale_fronte"), + jsonObjectFrom( + eb + .selectFrom("users_storage") + .select([ + "users_storage.storageId as storageId", + "servizio.doc_personale_retro_ref as ref", + ]) + .whereRef( + "users_storage.user_storage_id", + "=", + "servizio.doc_personale_retro_ref", + ), + ).as("doc_personale_retro"), + jsonObjectFrom( + eb + .selectFrom("users_storage") + .select([ + "users_storage.storageId as storageId", + "servizio.doc_motivazione_ref as ref", + ]) + .whereRef( + "users_storage.user_storage_id", + "=", + "servizio.doc_motivazione_ref", + ), + ).as("doc_motivazione"), jsonArrayFrom( eb .selectFrom("servizio_annunci") diff --git a/apps/infoalloggi/src/server/controllers/storage.controller.ts b/apps/infoalloggi/src/server/controllers/storage.controller.ts index e4ac238..d0a7419 100644 --- a/apps/infoalloggi/src/server/controllers/storage.controller.ts +++ b/apps/infoalloggi/src/server/controllers/storage.controller.ts @@ -114,8 +114,8 @@ export const addUserStorage = async (data: NewUsersStorage) => { .insertInto("users_storage") .values(data) .onConflict((oc) => oc.columns(["userId", "storageId"]).doNothing()) - .returningAll() - .execute(); + .returning("user_storage_id") + .executeTakeFirstOrThrow(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", diff --git a/ref-docker-compose.yml b/ref-docker-compose.yml index ee09cc2..9a65ff4 100644 --- a/ref-docker-compose.yml +++ b/ref-docker-compose.yml @@ -18,29 +18,12 @@ services: interval: 1s timeout: 5s retries: 10 - - # minio: - # image: minio/minio - # expose: - # - "9000" - # - "9001" - # volumes: - # - minio_storage:/data - # networks: - # - dokploy-network - # environment: - # MINIO_ROOT_USER: ${MINIO_ROOT_USER} - # MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} - # command: server --console-address ":9001" /data - # healthcheck: - # test: [ "CMD", "curl", "-f", "http://minio:9000/minio/health/live" ] - # interval: 10s - # timeout: 10s - # retries: 5 storage: image: "ghcr.io/marcopedone/go_fs:latest" - ports: - - "8080:8080" + expose: + - "8080" + restart: unless-stopped + pull_policy: "always" environment: PORT: 8080 AUTH_TOKEN: ${STORAGE_TOKEN} @@ -106,10 +89,8 @@ services: - images_volume:/app/images - storage_volume:/app/storage environment: - STORAGE_STRATEGY: "s3" - MINIO_URL: minio:9000 - MINIO_ROOT_USER: ${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + STORAGE_URL: http://storage:8080 + STORAGE_TOKEN: ${STORAGE_TOKEN} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB}