import { TRPCError } from "@trpc/server"; import { env } from "~/env"; import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils"; import type { Result } from "~/lib/result"; import { getStorageUrl } from "~/lib/storage_utils"; import type { Annunci, AnnunciId, AnnunciUpdate, } from "~/schemas/public/Annunci"; import type { ImagesRefs } from "~/schemas/public/ImagesRefs"; import type { ServizioServizioId } from "~/schemas/public/Servizio"; import type { VideosRefs } from "~/schemas/public/VideosRefs"; import { db } from "~/server/db"; import type { AnnuncioTemplateData } from "~/server/services/typst.service"; import { withImages, withVideos } from "~/utils/kysely-helper"; import { revalidate } from "../utils/revalidationHelper"; // const ratelimit = new RateLimiterHandler({ // windowSize: 10, // maxRequests: 10, // analytics: true, // }); export type AnnunciWithMedia = Annunci & { tipo: string; images: Pick[]; videos: Pick[]; }; export const getAnnunciListHandler = async (): Promise< Pick< Annunci, "id" | "codice" | "titolo_it" | "comune" | "tipo" | "locatore" | "stato" >[] > => { try { return await db .selectFrom("annunci") .select([ "id", "codice", "titolo_it", "comune", "tipo", "locatore", "stato", ]) .execute(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore query getAllRaw: ${(e as Error).message}`, }); } }; export const getCodici_AnnunciHandler = async (): Promise => { try { const codici = await db .selectFrom("annunci") .select("codice") .where("annunci.web", "=", true) .where("stato", "!=", "Sospeso") .execute(); if (!codici) { return []; } return codici.map((c) => c.codice); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore query getCodici: ${(e as Error).message}`, }); } }; export type AnnuncioData = Pick< Annunci, | "anno" | "accessori" | "caratteristiche" | "categorie" | "desc_en" | "desc_it" | "titolo_it" | "titolo_en" | "stato" | "id" | "codice" | "comune" | "consegna" | "external_videos" | "prezzo" | "numero_camere" | "numero_bagni" | "numero_balconi" | "numero_postiauto" | "numero_box" | "numero_vani" | "numero_terrazzi" | "mq" | "piano" | "piano_palazzo" | "lat_secondario" | "lon_secondario" > & { tipo: string; media_updated_at: string | null; images: Pick[]; videos: Pick[]; ogUrl: string; ogImage: string; }; export const getAnnuncioData = async ({ cod, }: { cod: string; }): Promise> => { try { const annuncio = await db .selectFrom("annunci") .select([ "anno", "accessori", "caratteristiche", "categorie", "desc_en", "desc_it", "tipo", "titolo_it", "titolo_en", "stato", "media_updated_at", "id", "codice", "comune", "consegna", "external_videos", "prezzo", "numero_camere", "numero_bagni", "numero_balconi", "numero_postiauto", "numero_box", "numero_vani", "numero_terrazzi", "mq", "piano", "piano_palazzo", "lat_secondario", "lon_secondario", ]) .select((_eb) => [withImages(), withVideos()]) .where("annunci.web", "=", true) .where("tipo", "is not", null) .where("codice", "=", cod) .executeTakeFirst(); if (!annuncio) { return { success: false, message: "Annuncio non trovato" }; } const { tipo, media_updated_at, ...rest } = annuncio; if (tipo === null) { return { success: false, message: "Annuncio non trovato" }; } const ogImage = annuncio.images && annuncio.images.length > 0 && annuncio.images[0] ? getStorageUrl({ storageId: annuncio.images[0].img, params: { cacheKey: annuncio.media_updated_at?.toISOString(), media: "image", }, host: env.BASE_URL, }) : `${env.BASE_URL}/og.jpg`; return { success: true, data: { ...rest, media_updated_at: media_updated_at?.toISOString() || null, tipo, ogUrl: `${env.BASE_URL}/annuncio/${cod}`, ogImage, }, }; } catch (e) { throw new TRPCError({ cause: (e as Error).cause, code: "INTERNAL_SERVER_ERROR", message: `Errore query getAnnunciByCod: ${(e as Error).message}`, }); } }; export const getAnnunciById_rawImgUrls = async ({ id, }: { id: AnnunciId; }): Promise => { try { const annuncio = await db .selectFrom("annunci") .selectAll() .select((_eb) => [withImages(), withVideos()]) .where("id", "=", id) .where("tipo", "is not", null) .executeTakeFirst(); if (!annuncio || annuncio.tipo === null) { throw new TRPCError({ code: "NOT_FOUND", message: "Annuncio non trovato", }); } return annuncio as AnnunciWithMedia; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore query getAnnuncioById : ${(e as Error).message}`, }); } }; export type AnnuncioRicercaWPosition = AnnuncioRicerca & { lat_secondario: string | null; lon_secondario: string | null; }; export const get_AnnunciPositionsHandler = async ({ tipologia, comune, consegna, minBudget, maxBudget, }: { tipologia?: string; comune?: string; consegna?: number[]; minBudget?: number; maxBudget?: number; }): Promise => { try { let query = db .selectFrom("annunci") .select((_eb) => [ "id", "codice", "comune", "provincia", "prezzo", "consegna", "numero_camere", "mq", "tipo", "titolo_it", "titolo_en", "desc_en", "desc_it", "web", "modificato_il", "stato", "external_videos", "lon_secondario", "lat_secondario", "media_updated_at", "homepage", withImages(), withVideos(), ]) .where("web", "=", true) .where("stato", "!=", "Sospeso") .orderBy("id", "asc"); if (tipologia && tipologia !== "") { query = query.where("tipo", "=", tipologia); } if (comune && comune !== "") { query = query.where("comune", "like", comune); } if (consegna) { query = query.where("consegna", "in", consegna); } if (minBudget) { query = query.where("prezzo", ">=", minBudget); } if (maxBudget) { query = query.where("prezzo", "<=", maxBudget); } const annunci = await query.execute(); if (!annunci) { return []; } return annunci; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore query getAnnunciPositions: ${(e as Error).message}`, }); } }; export type AnnuncioRicerca = Pick< Annunci, | "id" | "codice" | "comune" | "provincia" | "prezzo" | "consegna" | "numero_camere" | "mq" | "tipo" | "titolo_it" | "titolo_en" | "desc_en" | "desc_it" | "modificato_il" | "stato" | "external_videos" | "media_updated_at" | "homepage" | "web" > & { images: Pick[]; videos: Pick[]; }; export const getAnnunciRicerca = async ({ tipologia, comune, consegna, sort, minBudget, maxBudget, }: { tipologia?: string; comune?: string; consegna?: number[]; sort?: "Recenti" | "Prezzo" | "Consegna" | "Mq"; minBudget?: number; maxBudget?: number; }): Promise => { try { let query = db .selectFrom("annunci") .select((_eb) => [ "id", "codice", "comune", "provincia", "prezzo", "consegna", "numero_camere", "mq", "tipo", "titolo_it", "titolo_en", "desc_en", "desc_it", "web", "modificato_il", "stato", "external_videos", "media_updated_at", "homepage", withImages(), withVideos(), ]) .where("web", "=", true) .where("stato", "!=", "Sospeso"); if (tipologia && tipologia !== "") { query = query.where("tipo", "=", tipologia); } if (comune && comune !== "") { query = query.where("comune", "like", comune); } if (consegna) { query = query.where("consegna", "in", consegna); } if (minBudget) { query = query.where("prezzo", ">=", minBudget); } if (maxBudget) { query = query.where("prezzo", "<=", maxBudget); } switch (sort) { case "Recenti": query = query.orderBy("modificato_il", "desc"); break; case "Prezzo": query = query.orderBy("prezzo", "asc"); break; case "Consegna": query = query.orderBy("consegna", "asc"); break; case "Mq": query = query.orderBy("mq", "asc"); break; case undefined: break; } query = query.orderBy("id", "asc"); return await query.execute(); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore query ricercaAnnunci: ${(e as Error).message}`, }); } }; export const getOptions_AnnunciHandler = async () => { try { const comuni = await db .selectFrom("annunci") .select(["comune", "tipo", "consegna", "prezzo"]) .where("web", "=", true) .where("stato", "!=", "Sospeso") .distinct() .execute(); const results: { comune: string; tipo: string; consegna: number; prezzo: number; }[] = []; for (const comune of comuni) { if (comune.comune && comune.tipo && comune.consegna && comune.prezzo) { results.push({ comune: comune.comune, consegna: comune.consegna, tipo: comune.tipo, prezzo: comune.prezzo, }); } } return results; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore query getComuni: ${(e as Error).message}`, }); } }; export const editAnnuncioHandler = async ({ annuncioId, data, }: { annuncioId: AnnunciId; data: AnnunciUpdate; }) => { try { const annuncio = await db .updateTable("annunci") .set({ ...data, modificato_il: new Date(), }) .where("id", "=", annuncioId) .returning("codice") .executeTakeFirstOrThrow(() => { throw new Error(`Annuncio not found - annuncioId: ${annuncioId}`); }); await revalidate(`/annuncio/${annuncio.codice}`); return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore interno: ${(e as Error).message}`, }); } }; export const getProprietarioDataHandler = async ({ annuncioId, servizioId, }: { annuncioId: AnnunciId; servizioId: ServizioServizioId; }) => { try { const opened_at = await db .selectFrom("servizio_annunci") .select("open_contatti_at") .where("servizio_id", "=", servizioId) .where("annunci_id", "=", annuncioId) .executeTakeFirst(); if (!opened_at?.open_contatti_at) { return { opened_at: null, propData: null }; } const propData = await db .selectFrom("annunci") .select([ "codice", "comune", "indirizzo", "civico", "provincia", "regione", "cap", "lat", "lon", "numero", "locatore", "tipo_locatore", ]) .where("id", "=", annuncioId) .executeTakeFirstOrThrow(() => { throw new Error(`Annuncio not found - annuncioId: ${annuncioId}`); }); return { opened_at: opened_at.open_contatti_at, propData }; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore recupero dati proprietario: ${(e as Error).message}`, }); } }; 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}`, }); } }; export const getAnnunciSchedaData = async ( id: AnnunciId, ): Promise => { try { const data = await db .selectFrom("annunci") .select([ "codice", "locatore", "numero", "indirizzo", "civico", "comune", "provincia", "cap", "lat", "lon", "tipo", "prezzo", "titolo_it", "desc_it", "disponibile_da", "tipo_locatore", "caratteristiche", ]) .where("id", "=", id) .executeTakeFirstOrThrow( () => new Error(`Annuncio non trovato - id: ${id}`), ); if ( !data.locatore || !data.numero || !data.indirizzo || !data.lat || !data.lon || !data.tipo || !data.prezzo || !data.titolo_it || !data.desc_it || !data.disponibile_da ) { throw new Error( `Dati incompleti per generazione scheda annuncio - id: ${id}`, ); } return { codice: data.codice, numero: data.numero, nominativo: `${data.locatore}${data.tipo_locatore ? ` (${data.tipo_locatore})` : ""}`, indirizzo: `${data.indirizzo}, ${data.civico} ${data.comune} ${data.cap} (${data.provincia})`, lat: data.lat, lon: data.lon, tipo: data.tipo, prezzo: data.prezzo, titolo_it: data.titolo_it, desc_it: data.desc_it, disponibile_da: data.disponibile_da.toLocaleDateString("it-IT"), caratteristiche: data.caratteristiche ? filteredCaratteristiche(data.caratteristiche) .filter((c) => c.value !== null) .map((c) => [c.text, c.value || ""]) : [], }; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Errore recupero dati scheda annuncio: ${(e as Error).message}`, }); } };