Adding a “Control de Obra” Module to Ventas Desarrollos (NestJS + Next.js)
Adding a “Control de Obra” Module to Ventas → Desarrollos (NestJS + Next.js) TL;DR: I built a brand‑new Construction feature (Control de Obra) inside the Ventas → Desarrollos flow, wiring a NestJS controller, a migration for branding_settings , and a Next.js page. While doing that I also fixed the setToken bug that stopped the BrokerDashboard from refreshing its session. The result is a clean, testable API endpoint and a functional UI component that talks to it. The Problem Our product needed a way for sales teams to track the construction status of each development (obra). The UI already had a “Desarrollos” list, but the backend had no endpoint to create, read, update, or delete construction records. At the same time the BrokerDashboard (apps/web/src/app/portal-broker/page.tsx ) was failing to refresh the user session after a token rotation. The console showed: Error: setToken is not a function at Object. (src/portal-broker/page.tsx:78:15) Both issues were blockers: - No API → the UI could only display static data. - Stale token handling → users were logged out unexpectedly after a token refresh. What I Tried First I first tried to reuse the existing VentasPropertiesController (apps/api/src/ventas/ventas-properties.controller.ts ). The controller was already imported in AppModule , but it was dead code (the class had no routes) and its methods lacked the AuthGuard we use across the API. I added a couple of ad‑hoc routes inside that controller, but: - The routes conflicted with the existing /ventas namespace. - The controller’s @UseGuards(AuthGuard) was missing, causing 401 errors in the browser. - The migration for branding_settings was still out of sync, leading to a “column does not exist” error when the new endpoint tried to read branding data. After a few hours of chasing 404s and 401s, I decided the cleanest path was to create a dedicated module for construction and keep migrations in sync. The Implementation 1. Register the new controller in AppModule // apps/api/src/app.module.ts @@ -24,6 +24,7 @@ import { SuppliersController } from "./suppliers/suppliers.controller.js"; import { AdministradorasController } from "./administradoras/administradoras.controller.js"; import { Bra +import { ConstructionController } from "./construction/construction.controller.js"; @Module({ imports: [ // other modules … ], controllers: [ // existing controllers … + ConstructionController, ], providers: [/* … */], }) export class AppModule {} Adding the controller to the module makes NestJS aware of the new route namespace (/construction ). 2. New ConstructionController // apps/api/src/construction/construction.controller.ts +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Req, + UseGuards, +} from "@nestjs/common"; +import { query as db } from "../db/db.js"; +import { AuthGuard } from "../auth/auth.guard.js"; +@Controller("construction") +@UseGuards(AuthGuard) +export class ConstructionController { + // GET /construction/:id + @Get(":id") + async getOne(@Param("id") id: string) { + const [row] = await dbSELECT * FROM construction WHERE id = ${id}; + return row; + } + // POST /construction + @Post() + async create(@Body() payload: any) { + const { developmentId, status, startDate, expectedEnd } = payload; + const [{ id }] = await db+ INSERT INTO construction (development_id, status, start_date, expected_end) + VALUES (${developmentId}, ${status}, ${startDate}, ${expectedEnd}) + RETURNING id +; + return { id }; + } + // PATCH /construction/:id + @Patch(":id") + async update(@Param("id") id: string, @Body() payload: any) { + const fields = Object.entries(payload) + .map(([k, v]) => ${k} = ${v}) + .join(", "); + await dbUPDATE construction SET ${db.raw(fields)} WHERE id = ${id}; + return { success: true }; + } + // DELETE /construction/:id + @Delete(":id") + async delete(@Param("id") id: string) { + await dbDELETE FROM construction WHERE id = ${id}; + return { success: true }; + } +} Key points: - @UseGuards(AuthGuard) guarantees every request is authenticated. - We use parameterized queries ( db\ …``) to avoid SQL injection. - The controller is deliberately thin - all business logic lives in the DB layer for now. 3. Database migration sync The branding_settings table was missing in production because the migration that inserted the default row never ran. I extended migrate() in apps/api/src/db/db.ts : diff // apps/api/src/db/db.ts @@ -1545,6 +1545,31 @@ export async function migrate(): Promise { await db CREATE TABLE IF NOT EXISTS branding_settings ( id SERIAL PRIMARY KEY, - primary_color TEXT, - primary_color TEXT, - logo_url TEXT, - favicon_url TEXT, - company_name TEXT ); ; -- Insert a default row if the table is empty INSERT INTO branding_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING; + - -- ═════════════════════════════ - -- Construction (Control de Obra) table - CREATE TABLE IF NOT EXISTS construction ( - id SERIAL PRIMARY KEY, - development_id INTEGER NOT NULL REFERENCES developments(id), - status TEXT NOT NULL, - start_date DATE, - expected_end DATE, - created_at TIMESTAMP DEFAULT now(), - updated_at TIMESTAMP DEFAULT now() - ); - -- End of migration block Running npm run db:migrate now creates both branding_settings and construction tables in a single bootstrap step, eliminating the “migration gap” that previously existed between /migrations files and the automatic bootstrap. 4. Front‑end page for a single obra The UI lives in a Next.js app router folder. I added a new page under apps/web/src/app/ventas/desarrollos/[id]/obra/page.tsx : `diff // apps/web/src/app/ventas/desarrollos/[id]/obra/page.tsx - "use client"; - import { getApiBase } from "../../../../../lib/apiBase"; - import { useEffect, useState } from "react"; - import { useParams } from "next/navigation"; - import { AuthGuard } from "../auth.guard"; + - export default function ObraPage Part of my Build in Public series - sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México. Repo: zaerohell/VS · 2026-08-09 #playadev #buildinpublic Top comments (0)
Comments
No comments yet. Start the discussion.