From c31a68ef6d8319bea2589aeeb2f1eff6515c979c Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Mon, 2 Mar 2026 15:35:23 +0100 Subject: [PATCH] feat: implement image and video reconciliation, update pricing options, and enhance media removal functionality --- apps/backend/images.go | 48 +++++++- apps/backend/miogest_handler.go | 2 + apps/backend/typesdefs/models.go | 4 +- apps/backend/video.go | 59 +++++++--- apps/infoalloggi/src/components/prezzi.tsx | 5 +- .../src/forms/FormEditAnnuncio.tsx | 109 ++++++++++++++---- apps/infoalloggi/src/forms/FormPrezzo.tsx | 7 +- apps/infoalloggi/src/i18n/en.ts | 3 +- apps/infoalloggi/src/i18n/it.ts | 5 +- apps/infoalloggi/src/i18n/locales.ts | 1 + apps/infoalloggi/src/pages/annuncio/[cod].tsx | 38 +++--- apps/infoalloggi/src/pages/prezzi.tsx | 1 + .../src/server/api/routers/annunci.ts | 10 ++ .../server/controllers/annunci.controller.ts | 27 +++++ .../server/controllers/servizio.controller.ts | 14 ++- 15 files changed, 263 insertions(+), 70 deletions(-) diff --git a/apps/backend/images.go b/apps/backend/images.go index d41c109..faa6c0c 100644 --- a/apps/backend/images.go +++ b/apps/backend/images.go @@ -305,7 +305,7 @@ func CodiciToProcessImages(annunci *typesdefs.AnnunciXML) ([]string, error) { return nil, err } - dbImagesMap := make(map[string][]imgRef) // Convert slice to map for faster lookup + dbImagesMap := make(map[string][]imgRef) for _, ref := range db_images { dbImagesMap[ref.Codice] = append(dbImagesMap[ref.Codice], imgRef{ Og_url: ref.Og_url, @@ -318,7 +318,8 @@ func CodiciToProcessImages(annunci *typesdefs.AnnunciXML) ([]string, error) { for _, annuncio := range annunci.Annuncio { if annuncio.Codice != nil && annuncio.Foto != nil { - refFoto, existsInDb := dbImagesMap[*annuncio.Codice] + // Check if codice exists in DB + refFotos, existsInDb := dbImagesMap[*annuncio.Codice] if !existsInDb { codiciToProcess = append(codiciToProcess, *annuncio.Codice) continue @@ -326,8 +327,10 @@ func CodiciToProcessImages(annunci *typesdefs.AnnunciXML) ([]string, error) { needProcess := false for _, fotoUrl := range *annuncio.Foto { found := false - for _, ref := range refFoto { + // Check if the fotoUrl exists in the DB refs + for _, ref := range refFotos { if fotoUrl == ref.Og_url { + // check if the corresponding img and thumb are in the bucket if bucketIdsMap[ref.Img] && bucketIdsMap[ref.Thumb] { found = true break @@ -374,3 +377,42 @@ func ForceClearImages(codici []string) error { } return nil } + +// Removes stranded images (in storage but not in DB) +func ReconcileImages() error { + currentBucketState, err := ListBucket("images") + if err != nil { + return fmt.Errorf("failed to list bucket: %w", err) + } + //make a list of bucket ids + bucketIdsMap := make(map[string]bool) + for _, item := range currentBucketState { + bucketIdsMap[item.ID] = true + } + db_images, err := queries.GetImagesFromDB() + if err != nil { + return fmt.Errorf("failed to get images from DB: %w", err) + } + knownStorageIds := make(map[string]bool) + for _, ref := range db_images { + knownStorageIds[ref.Img] = true + knownStorageIds[ref.Thumb] = true + } + // reconcile: delete storage files not referenced in DB + var strandedIds []string + for _, item := range currentBucketState { + if !knownStorageIds[item.ID] { + strandedIds = append(strandedIds, item.ID) + } + } + if len(strandedIds) > 0 { + log.Printf("Reconciliation: found %d stranded files, deleting...", len(strandedIds)) + if err := DeleteIdsFromStorage(strandedIds); err != nil { + log.Printf("Warning: failed to delete stranded files: %v", err) // non-fatal + } + } else { + log.Println("Reconciliation: no stranded files found.") + } + return nil + +} diff --git a/apps/backend/miogest_handler.go b/apps/backend/miogest_handler.go index 1fdf19e..818bde0 100644 --- a/apps/backend/miogest_handler.go +++ b/apps/backend/miogest_handler.go @@ -204,6 +204,7 @@ func (m *MiogestHandler) ParseAnnunci(codFilter string, imgPool *[]typesdefs.Med image_codes_to_process = append(image_codes_to_process, codFilter) } } + err = ReconcileImages() err = ForceClearImages(image_codes_to_process) if err != nil { log.Fatalf("Failed to clear images for codice %s: %v", codFilter, err.Error()) @@ -237,6 +238,7 @@ func (m *MiogestHandler) ParseAnnunci(codFilter string, imgPool *[]typesdefs.Med video_codes_to_process = append(video_codes_to_process, codFilter) } } + err = ReconcileVideos() err = ForceClearVideos(video_codes_to_process) if err != nil { log.Fatalf("Failed to clear videos for codice %s: %v", codFilter, err.Error()) diff --git a/apps/backend/typesdefs/models.go b/apps/backend/typesdefs/models.go index 01b10fc..9df0d6e 100644 --- a/apps/backend/typesdefs/models.go +++ b/apps/backend/typesdefs/models.go @@ -201,8 +201,8 @@ type AnnuncioXML struct { } `xml:"Clienti"` Cap *string `xml:"Cap"` Video *[]string `xml:"Video"` - AMMedias *[]struct { - AMVideo *struct { + AMMedias *struct { + AMVideo []struct { Url *string `xml:"Url"` } `xml:"AMVideo"` } `xml:"AMMedias"` diff --git a/apps/backend/video.go b/apps/backend/video.go index 2a2f792..942a6da 100644 --- a/apps/backend/video.go +++ b/apps/backend/video.go @@ -27,10 +27,10 @@ func AddToVideoProcessing(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.Ann miogestVideos := []string{} if annuncio.AMMedias != nil { - for _, media := range *annuncio.AMMedias { - if media.AMVideo != nil && media.AMVideo.Url != nil { - if strings.Contains(*media.AMVideo.Url, "video.miogest") { - miogestVideos = append(miogestVideos, *media.AMVideo.Url) + for _, media := range annuncio.AMMedias.AMVideo { + if media.Url != nil { + if strings.Contains(*media.Url, "video.miogest") { + miogestVideos = append(miogestVideos, *media.Url) } } @@ -347,12 +347,12 @@ func CodiciToProcessVideos(annunci *typesdefs.AnnunciXML) ([]string, error) { var codiciToProcess []string for _, annuncio := range annunci.Annuncio { - if annuncio.Codice != nil && annuncio.AMMedias != nil && len(*annuncio.AMMedias) > 0 { + if annuncio.Codice != nil && annuncio.AMMedias != nil { mediaUrls := []string{} - for _, media := range *annuncio.AMMedias { - if media.AMVideo != nil && media.AMVideo.Url != nil { - if strings.Contains(*media.AMVideo.Url, "video.miogest") { - mediaUrls = append(mediaUrls, *media.AMVideo.Url) + for _, media := range annuncio.AMMedias.AMVideo { + if media.Url != nil { + if strings.Contains(*media.Url, "video.miogest") { + mediaUrls = append(mediaUrls, *media.Url) } } } @@ -367,17 +367,17 @@ func CodiciToProcessVideos(annunci *typesdefs.AnnunciXML) ([]string, error) { } needProcess := false - for _, media := range *annuncio.AMMedias { - if media.AMVideo == nil || media.AMVideo.Url == nil { + for _, media := range annuncio.AMMedias.AMVideo { + if media.Url == nil { continue } - if !strings.Contains(*media.AMVideo.Url, "video.miogest") { + if !strings.Contains(*media.Url, "video.miogest") { continue } found := false for _, ref := range refVideos { - if *media.AMVideo.Url == ref.Og_url { + if *media.Url == ref.Og_url { if bucketIdsMap[ref.Video] && bucketIdsMap[ref.Thumb] { found = true break @@ -425,3 +425,36 @@ func ForceClearVideos(codici []string) error { } return nil } + +// ReconcileVideos removes stranded videos (in storage but not in DB) +func ReconcileVideos() error { + currentBucketState, err := ListBucket("videos") + if err != nil { + return fmt.Errorf("failed to list bucket: %w", err) + } + //make a list of bucket ids + bucketIdsMap := make(map[string]bool) + for _, item := range currentBucketState { + bucketIdsMap[item.ID] = true + } + db_videos, err := queries.GetVideosFromDB() + if err != nil { + return fmt.Errorf("failed to get videos from DB: %w", err) + } + var strandedIds []string + for _, ref := range db_videos { + if !bucketIdsMap[ref.Video] { + strandedIds = append(strandedIds, ref.Video) + } + if !bucketIdsMap[ref.Thumb] { + strandedIds = append(strandedIds, ref.Thumb) + } + } + if len(strandedIds) > 0 { + err := DeleteIdsFromStorage(strandedIds) + if err != nil { + return fmt.Errorf("failed to delete stranded videos from storage: %w", err) + } + } + return nil +} diff --git a/apps/infoalloggi/src/components/prezzi.tsx b/apps/infoalloggi/src/components/prezzi.tsx index 6e83670..cd01d29 100644 --- a/apps/infoalloggi/src/components/prezzi.tsx +++ b/apps/infoalloggi/src/components/prezzi.tsx @@ -33,7 +33,6 @@ export const PricingChoice = () => { downPayment: prezziario.stabile.acconto.prezzo_cent / 100, secondPayment: s.prezzo_cent / 100, })); - return (

Scegli il tuo servizio

@@ -75,6 +74,7 @@ export const PricingChoice = () => { opzioni={[ t.pricing_cmp.stabile_option1, t.pricing_cmp.stabile_option2, + t.pricing_cmp.stabile_option3, ]} pricingData={pricingStabile} tipo={TipologiaPosizioneEnum.Stabile} @@ -103,6 +103,7 @@ export const PricingComponent = ({ className, title, tipo, + optionsBtnClassName, }: { pricingData: PricingData; opzioni: string[]; @@ -111,6 +112,7 @@ export const PricingComponent = ({ statico?: boolean; className?: string; tipo: TipologiaPosizioneEnum; + optionsBtnClassName?: string; }) => { const { t } = useTranslation(); const [Value, setValue] = useState(0); @@ -213,6 +215,7 @@ export const PricingComponent = ({ className={cn( "h-9 xs:h-10 rounded-md border-none px-3 xs:px-4 xs:py-2", idx === Value ? "fade-in-0 animate-in" : "", + optionsBtnClassName, )} key={opzione} onClick={() => { diff --git a/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx b/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx index acf4adb..429a616 100644 --- a/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx +++ b/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx @@ -8,11 +8,12 @@ import { RefreshCcw, RotateCw, TriangleAlert, + X, } from "lucide-react"; +import Image from "next/image"; import Link from "next/link"; import toast from "react-hot-toast"; import z from "zod"; -import { CarouselAnnuncio } from "~/components/annuncio_card"; import { Confirm } from "~/components/confirm"; import { Form, @@ -41,8 +42,10 @@ import { } from "~/components/ui/popover"; import { Switch } from "~/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; +import { VideoPlayer } from "~/components/videoPlayer"; import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils"; import { NullableStringOnChange } from "~/lib/form_utils"; +import { getStorageUrl } from "~/lib/storage_utils"; import { cn } from "~/lib/utils"; import { useZodForm } from "~/lib/zodForm"; import { StatusBadge } from "~/pages/area-riservata/admin/edit-annuncio/[id]"; @@ -109,14 +112,6 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => { titolo_it: z.string().nullable(), unita_condominio: z.number().nullable(), web: z.boolean().nullable(), - - // MEDIA - //url_immagini: z.string().array().nullable(), - //url_video: z.string().array().nullable(), - //og_url: z.string().nullable(), - - //creato_il: z.date().nullable(), - //modificato_il: z.date().nullable(), }); type FormValues = z.infer; @@ -160,6 +155,14 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => { }, }); + const { mutateAsync: removeMedia } = + api.annunci.removeAnnuncioMedia.useMutation({ + onSettled: async () => { + await revalidate({ cod: data.codice }); + toast.success("Media rimosso con successo"); + }, + }); + const defaultValues: FormValues = { ...data, mq: Number(data.mq) || null, @@ -535,19 +538,6 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => { /> - - - Imagini Annuncio - - - - -
@@ -1843,7 +1833,80 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => { Media -
+
+ {data.images.map((img) => ( +
+ {img.img} + + removeMedia({ type: "img", id: img.img }) + } + title="Rimuovi media" + > + + +
+ ))} + {data.videos.map((video) => ( +
+ + + removeMedia({ type: "video", id: video.video }) + } + title="Rimuovi media" + > + + +
+ ))} +
diff --git a/apps/infoalloggi/src/forms/FormPrezzo.tsx b/apps/infoalloggi/src/forms/FormPrezzo.tsx index 7a04105..9b99ee7 100644 --- a/apps/infoalloggi/src/forms/FormPrezzo.tsx +++ b/apps/infoalloggi/src/forms/FormPrezzo.tsx @@ -133,12 +133,7 @@ export const FormPrezzo = ({ - + )} diff --git a/apps/infoalloggi/src/i18n/en.ts b/apps/infoalloggi/src/i18n/en.ts index 9c4d14b..313275f 100644 --- a/apps/infoalloggi/src/i18n/en.ts +++ b/apps/infoalloggi/src/i18n/en.ts @@ -1187,7 +1187,8 @@ The Stable Rent service, ideal for those with demonstrable work references and f stabile_footer: "Types of stable rent: Libero (4+4 years), Subsidized (3+2 years) or commercial (6+6 years)", stabile_option1: "Up to 500 €/month", - stabile_option2: "Over 500 €/month", + stabile_option2: "Up to 700 €/month", + stabile_option3: "Over 700 €/month", stabile_select: "Select the rent:", titolo: "Pay based on your search", txt_tipo_acquisto: "For the purchase of any type of property", diff --git a/apps/infoalloggi/src/i18n/it.ts b/apps/infoalloggi/src/i18n/it.ts index 830b765..c74cbf8 100644 --- a/apps/infoalloggi/src/i18n/it.ts +++ b/apps/infoalloggi/src/i18n/it.ts @@ -1183,8 +1183,9 @@ export const it: LangDict = { saldi_stabile: ["Fino a 500 €/mese", "Superiore a 500 €/mese"], stabile_footer: "Tipologie di affitto stabile: libero (4+4 anni), agevolato (3+2 anni) o commerciale (6+6 anni)", - stabile_option1: "Fino a 500 €/mese", - stabile_option2: "Superiore a 500 €/mese", + stabile_option1: "Fino a 500€/mese", + stabile_option2: "Fino a 700€/mese", + stabile_option3: "Oltre 700€/mese", stabile_select: "Seleziona il canone:", titolo: "Paga in base alla tua ricerca", txt_tipo_acquisto: "Per l'acquisto di qualsiasi tipologia di immobile", diff --git a/apps/infoalloggi/src/i18n/locales.ts b/apps/infoalloggi/src/i18n/locales.ts index 25bacf9..191c3e9 100644 --- a/apps/infoalloggi/src/i18n/locales.ts +++ b/apps/infoalloggi/src/i18n/locales.ts @@ -185,6 +185,7 @@ export type LangDict = { stabile_select: string; stabile_option1: string; stabile_option2: string; + stabile_option3: string; stabile_footer: string; breve_select: string; breve_option1: string; diff --git a/apps/infoalloggi/src/pages/annuncio/[cod].tsx b/apps/infoalloggi/src/pages/annuncio/[cod].tsx index 3ca12ba..593a0d2 100644 --- a/apps/infoalloggi/src/pages/annuncio/[cod].tsx +++ b/apps/infoalloggi/src/pages/annuncio/[cod].tsx @@ -307,7 +307,11 @@ const Pricing = ({ t.pricing_cmp.breve_option4, t.pricing_cmp.breve_option5, ] - : [t.pricing_cmp.stabile_option1, t.pricing_cmp.stabile_option2]; + : [ + t.pricing_cmp.stabile_option1, + t.pricing_cmp.stabile_option2, + t.pricing_cmp.stabile_option3, + ]; const style = tipologia === TipologiaPosizioneEnum.Transitorio @@ -456,7 +460,7 @@ const CardInfos = ({ data }: { data: Annunci }) => {
-
+
{data.prezzo && (data.prezzo / 1e2).toFixed(2).replace(".", ",")}{" "} {t.annunci.euro_mese}
@@ -480,7 +484,7 @@ const CardInfos = ({ data }: { data: Annunci }) => {
{
- {(() => { - if (!data.numero_camere) return "N/A"; - if (data.numero_camere > 1) - return `${data.numero_camere} ${t.card.camere2}`; - if (data.numero_camere === 1) - return `${data.numero_camere} ${t.card.camere1}`; - })()} + + {(() => { + if (!data.numero_camere) return "N/A"; + if (data.numero_camere > 1) + return `${data.numero_camere} ${t.card.camere2}`; + if (data.numero_camere === 1) + return `${data.numero_camere} ${t.card.camere1}`; + })()} +
- + {data.numero_bagni || 0} {t.annunci.bagni}
- {data.caratteristiche?.Scheda_Arredi || ""} + + {data.caratteristiche?.Scheda_Arredi || ""} +
- + {data.numero_postiauto || 0} {t.annunci.posti_auto}
@@ -532,7 +540,7 @@ const CardInfos = ({ data }: { data: Annunci }) => {
- + {data.mq ? Number(data.mq).toLocaleString("it-IT") : "--"} m 2 @@ -542,7 +550,7 @@ const CardInfos = ({ data }: { data: Annunci }) => { {data.piano !== null && data.piano !== "" && ( <> - + {(() => { if (data.piano === "0") return t.annunci.p_terra; if (data.piano === "0.5") return t.annunci.p_rialzato; diff --git a/apps/infoalloggi/src/pages/prezzi.tsx b/apps/infoalloggi/src/pages/prezzi.tsx index e77e53b..a4e9e46 100644 --- a/apps/infoalloggi/src/pages/prezzi.tsx +++ b/apps/infoalloggi/src/pages/prezzi.tsx @@ -102,6 +102,7 @@ const Prezzi: NextPage = () => { opzioni={[ t.pricing_cmp.stabile_option1, t.pricing_cmp.stabile_option2, + t.pricing_cmp.stabile_option3, ]} pricingData={pricingStabile} statico diff --git a/apps/infoalloggi/src/server/api/routers/annunci.ts b/apps/infoalloggi/src/server/api/routers/annunci.ts index 7d0e3c3..d07e596 100644 --- a/apps/infoalloggi/src/server/api/routers/annunci.ts +++ b/apps/infoalloggi/src/server/api/routers/annunci.ts @@ -19,6 +19,9 @@ import { getOnline_AnnuncioIdCodice_AnnunciHandler, getOptions_AnnunciHandler, getProprietarioDataHandler, + type ImgToRemove, + removeAnnuncioMedia, + type VideoToRemove, } from "~/server/controllers/annunci.controller"; import { db } from "~/server/db"; import { NewMail } from "~/server/services/mailer"; @@ -142,4 +145,11 @@ export const annunciRouter = createTRPCRouter({ } return { status: "ok", timestamp: Date.now() }; }), + removeAnnuncioMedia: adminProcedure + .input(z.custom()) + .mutation(async ({ input }) => { + return await removeAnnuncioMedia({ + ...input, + }); + }), }); diff --git a/apps/infoalloggi/src/server/controllers/annunci.controller.ts b/apps/infoalloggi/src/server/controllers/annunci.controller.ts index 2037742..f06fd36 100644 --- a/apps/infoalloggi/src/server/controllers/annunci.controller.ts +++ b/apps/infoalloggi/src/server/controllers/annunci.controller.ts @@ -495,3 +495,30 @@ export const getProprietarioDataHandler = async ({ }); } }; + +export type ImgToRemove = { + type: "img"; + id: ImagesRefs["img"]; +}; +export type VideoToRemove = { + type: "video"; + id: VideosRefs["video"]; +}; +export const removeAnnuncioMedia = async ({ + id, + type, +}: ImgToRemove | VideoToRemove) => { + try { + if (type === "img") { + await db.deleteFrom("images_refs").where("img", "=", id).execute(); + } + if (type === "video") { + await db.deleteFrom("videos_refs").where("video", "=", id).execute(); + } + } catch (e) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Errore rimozione media: ${(e as Error).message}`, + }); + } +}; diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts index aca3328..ccadbb6 100644 --- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts +++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts @@ -461,7 +461,11 @@ export const setupSaldoServizio = async ({ } saldo = await qry.where("idprezziario", "=", mpp).executeTakeFirst(); } else if (servizio.tipologia === TipologiaPosizioneEnum.Stabile) { - if (servizio.budget >= 500) { + if (servizio.budget >= 700) { + saldo = await qry + .where("idprezziario", "=", "SALDOCA700+" as PrezziarioIdprezziario) + .executeTakeFirst(); + } else if (servizio.budget >= 500) { saldo = await qry .where("idprezziario", "=", "SALDOCA500+" as PrezziarioIdprezziario) @@ -588,9 +592,11 @@ export const getPacksPerServizio = async ( } case TipologiaPosizioneEnum.Stabile: { const pack = await getPrezziarioByIdHandler({ - id: (servizio.budget >= 500 - ? "SALDOCA500+" - : "SALDOCA500") as PrezziarioIdprezziario, + id: (servizio.budget >= 700 + ? "SALDOCA700+" + : servizio.budget >= 500 + ? "SALDOCA500+" + : "SALDOCA500") as PrezziarioIdprezziario, }); if (!pack) { return { success: false, message: "Pack non trovato" };