DEV Community

Roberto Luna
Roberto Luna

Posted on

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.<anonymous> (src/portal-broker/page.tsx:78:15)
Enter fullscreen mode Exit fullscreen mode

Both issues were blockers:

  1. No API → the UI could only display static data.
  2. 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 {}
Enter fullscreen mode Exit fullscreen mode

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 db`SELECT * 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 db`UPDATE construction SET ${db.raw(fields)} WHERE id = ${id}`;
+    return { success: true };
+  }

+  // DELETE /construction/:id
+  @Delete(":id")
+  async delete(@Param("id") id: string) {
+    await db`DELETE FROM construction WHERE id = ${id}`;
+    return { success: true };
+  }
+}
Enter fullscreen mode Exit fullscreen mode

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)