From eff1988d66d010121fdd37ea27df702da69ef104 Mon Sep 17 00:00:00 2001 From: Horacio Daniel Ros Date: Wed, 16 Sep 2026 10:44:54 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20agregar=20funcionalidad=20para=20obtene?= =?UTF-8?q?r=20organizaciones=20p=C3=BAblicas=20y=20actualizar=20el=20site?= =?UTF-8?q?map=20con=20rutas=20din=C3=A1micas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Companies/Companies.Adapter.Mongoose.ts | 2 + server/src/Models/Views/Views.ts | 59 +++++++++++++++++++ server/src/api/Views/Views.Controller.ts | 22 +++++++ server/src/api/Views/Views.Service.ts | 10 ++++ txclient/src/app/landing/org/[oid]/layout.tsx | 44 ++++++++++++++ txclient/src/app/sitemap.xml/route.ts | 39 +++++++++++- 6 files changed, 175 insertions(+), 1 deletion(-) diff --git a/server/src/Models/Companies/Companies.Adapter.Mongoose.ts b/server/src/Models/Companies/Companies.Adapter.Mongoose.ts index e66174b..7a2f2f0 100644 --- a/server/src/Models/Companies/Companies.Adapter.Mongoose.ts +++ b/server/src/Models/Companies/Companies.Adapter.Mongoose.ts @@ -72,6 +72,8 @@ export class CompaniesAdapterMongoose implements ICompaniesAdapter { showPublicProfessionals: { type: Boolean, required: false, default: true }, }); + this.schema.index({ published: 1, banned: 1 }); + this.companyList = model("Company", this.schema); } diff --git a/server/src/Models/Views/Views.ts b/server/src/Models/Views/Views.ts index f08e985..965d2bd 100644 --- a/server/src/Models/Views/Views.ts +++ b/server/src/Models/Views/Views.ts @@ -1,5 +1,6 @@ import { isNull } from "../../helpers/IsNull"; import CompaniesList from "../Companies/Companies"; +import { CompaniesAdapterMongoose } from "../Companies/Companies.Adapter.Mongoose"; import ServiceList from "../Services/Service"; import { PublicServicesResults, @@ -15,6 +16,24 @@ import { BotEventParams, BotView } from "../WapServer/WapServer.Interface"; import WapServerList from "../WapServer/WapServer"; import { COMPANY_PUBLISHED_STATUS } from "../Companies/Companies.Interface"; +export type PublicOrganizationsParams = { + page: number; + limit: number; +}; + +export type PublicOrganizationListItem = { + slug: string; + name: string; + description: string; + creationDate: Date; +}; + +export type PublicOrganizationsResult = { + data: PublicOrganizationListItem[]; + page: number; + pages: number; +}; + export type HomeView = { education: PublicServicesResults; health: PublicServicesResults; @@ -39,6 +58,7 @@ export type PublicOrganizationViewParams = { export type PublicOrganizationView = { id: string; + slug: string; headerFile: string; headerColor: string; headerFontColor: string; @@ -143,6 +163,7 @@ class Views { return { id: isNull(company.id, ""), + slug: isNull(company.slug, ""), name: isNull(company.name, ""), description: isNull(company.description, ""), categoryId: isNull(company.categoryId, 50000), @@ -249,6 +270,44 @@ class Views { }; } + public async publicOrganizations( + data: PublicOrganizationsParams + ): Promise { + const { page, limit } = data; + const filter = { + published: COMPANY_PUBLISHED_STATUS.PUBLISHED, + banned: { $ne: true }, + }; + + const companyList = (CompaniesList.companies as CompaniesAdapterMongoose).companyList; + + const count = await companyList + .countDocuments(filter) + .exec(); + + const skip = (page - 1) * limit; + + const results = (await companyList + .find(filter) + .select("slug name description creationDate") + .sort({ creationDate: 1 }) + .skip(skip) + .limit(limit) + .lean() + .exec()) as any[]; + + return { + data: results.map((company) => ({ + slug: String(company.slug || ""), + name: String(company.name || ""), + description: String(company.description || ""), + creationDate: company.creationDate, + })), + page, + pages: Math.ceil(count / limit), + }; + } + public async wapView(data: BotEventParams): Promise { return await WapServerList.botView(data); } diff --git a/server/src/api/Views/Views.Controller.ts b/server/src/api/Views/Views.Controller.ts index 0a8baf3..2bbec3a 100644 --- a/server/src/api/Views/Views.Controller.ts +++ b/server/src/api/Views/Views.Controller.ts @@ -9,6 +9,8 @@ import { PublicOrganizationServiceView, UserAppointmentViewParams, UserAppointmentView, + PublicOrganizationsParams, + PublicOrganizationsResult, } from "../../Models/Views/Views"; import { ViewsService } from "./Views.Service"; import { BotEventParams, BotView } from "../../Models/WapServer/WapServer.Interface"; @@ -109,3 +111,23 @@ export class WapServerBotViewController extends Controller { } } } + +@Route("views/public-organizations") +export class PublicOrganizationsController extends Controller { + @Response(500, "Ha ocurrido un error") + @SuccessResponse(200, "Done") + @Post() + public async publicOrganizations( + @Body() requestBody: PublicOrganizationsParams + ): Promise { + try { + const result = await new ViewsService().publicOrganizations(requestBody); + this.setStatus(200); + return result; + } catch (e) { + const errorOccurred: Error = e as Error; + this.setStatus(500); + return new ApiValidationError(500, errorOccurred.message); + } + } +} diff --git a/server/src/api/Views/Views.Service.ts b/server/src/api/Views/Views.Service.ts index 40981c9..22716c3 100644 --- a/server/src/api/Views/Views.Service.ts +++ b/server/src/api/Views/Views.Service.ts @@ -7,6 +7,8 @@ import views, { PublicOrganizationServiceView, UserAppointmentViewParams, UserAppointmentView, + PublicOrganizationsParams, + PublicOrganizationsResult, } from "../../Models/Views/Views"; import { BotView, BotEventParams } from "../../Models/WapServer/WapServer.Interface"; @@ -45,4 +47,12 @@ export class ViewsService { const view = await views.wapView(data); return view; } + + public async publicOrganizations( + data: PublicOrganizationsParams + ): Promise { + await connect(`${process.env.DATABASE_CONNECTION}`); + const result = await views.publicOrganizations(data); + return result; + } } diff --git a/txclient/src/app/landing/org/[oid]/layout.tsx b/txclient/src/app/landing/org/[oid]/layout.tsx index 32faa15..caaafac 100644 --- a/txclient/src/app/landing/org/[oid]/layout.tsx +++ b/txclient/src/app/landing/org/[oid]/layout.tsx @@ -3,6 +3,7 @@ import ApiServerService from "@services/Api.Server.Service"; import { PublicOrganizationView } from "@core/Models/Company.model"; import OrganizationHeader from "@core/app/components/Home/OrganizationHeader/OrganizationHeader"; import OrganizationDataConnector from "./components/OrganizationData"; +import type { Metadata } from "next"; const GetOrganizationData = async (companyId: string): Promise => { try { @@ -28,6 +29,49 @@ type Props = { children: React.ReactNode; }; +export async function generateMetadata({ params }: { params: Promise }): Promise { + const { oid } = await params; + const orgData = await GetOrganizationData(oid); + + if (!orgData || !orgData.id) { + return { + robots: { index: false, follow: false }, + }; + } + + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://turnosxpress.com.ar"; + const canonicalUrl = `${baseUrl}/landing/org/${oid}`; + const description = + orgData.description && orgData.description.trim().length > 0 + ? orgData.description + : `${orgData.name} - TurnosXpress`; + + return { + title: `${orgData.name} | TurnosXpress`, + description, + alternates: { + canonical: canonicalUrl, + }, + openGraph: { + type: "website", + title: `${orgData.name} | TurnosXpress`, + description, + url: canonicalUrl, + siteName: "TurnosXpress", + locale: "es_AR", + }, + twitter: { + card: "summary", + title: `${orgData.name} | TurnosXpress`, + description, + }, + robots: { + index: true, + follow: true, + }, + }; +} + export default async function OrganizationPublicProfile({ params, children }: Props) { const { oid } = await params; const orgData = await GetOrganizationData(oid); diff --git a/txclient/src/app/sitemap.xml/route.ts b/txclient/src/app/sitemap.xml/route.ts index 6f69fae..85f5cd4 100644 --- a/txclient/src/app/sitemap.xml/route.ts +++ b/txclient/src/app/sitemap.xml/route.ts @@ -165,7 +165,44 @@ export async function GET() { console.error("Error generating sitemap:", error); } - const allRoutes = [...staticRoutes, ...videoRoutes, ...authRoutes, ...marketingRoutes, ...explorationRoutes, ...legalRoutes, ...serviceRoutes]; + let orgRoutes: SitemapRoute[] = []; + + try { + let currentPage = 1; + let totalPages = 1; + + do { + const orgsResult = await ApiServerService.post<{ + data: { slug: string; name: string; description: string; creationDate?: string }[]; + page: number; + pages: number; + }>("views/public-organizations", { + page: currentPage, + limit: 1000, + }); + + if (orgsResult && orgsResult.data) { + totalPages = orgsResult.pages || 1; + + const newRoutes = orgsResult.data.map((org) => ({ + url: `${baseUrl}/landing/org/${org.slug}`, + changeFrequency: "weekly" as const, + priority: 0.8, + })); + + orgRoutes = [...orgRoutes, ...newRoutes]; + } else { + break; + } + + currentPage++; + } while (currentPage <= totalPages); + + } catch (error) { + console.error("Error generating org sitemap:", error); + } + + const allRoutes = [...staticRoutes, ...videoRoutes, ...authRoutes, ...marketingRoutes, ...orgRoutes, ...explorationRoutes, ...legalRoutes, ...serviceRoutes]; const sitemapXml = `