Unifying the “Hoy” Dashboard for Condominiums – Architecture, Bugs & Fixes
TL;DR: I refactored the condominium dashboard to reuse the “Hoy” sales/rentals pattern, fixed two Sentry‑related bugs (missing PDF attachment and a stray .map()), and bumped the whole monorepo to v2.0.0. The changes touch API email handling, provider page data mapping, and the shared shell components, delivering a consistent UI and cleaner error reporting.
The Problem
Our product ships two very similar dashboards: one for Ventas/Rentas (sales/rentals) and another for Condominios (condominiums). The latter was a copy‑paste of the former with a few hard‑coded strings, which caused three concrete issues:
-
Inconsistent UI navigation – the shell component (
CrmShellCondos) still referenced old routes (/ventas/dashboard). -
Sentry error “Report generated without attachment” – the monthly PDF report service (
EmailService.sendOwnerMonthlyReport) was always sending apdfBufferfield, even when the buffer wasnull, leading Sentry to flag a missing attachment. -
Runtime crash on the providers page – a stray
.map()on an undefined array (providers) threwTypeError: Cannot read property 'map' of undefined, surfacing in production logs.
These bugs broke our “build in public” promise of a reliable, observable system and forced us to duplicate logic across two dashboards.
What I Tried First
My initial attempt was to copy the existing sales dashboard files into the condo folder and rename a few strings. I left the original CrmShell component untouched, assuming the UI would work out of the box. I also tried a quick fix for the Sentry issue by adding a conditional if (pdfBuffer) before attaching the file, but that only masked the type error; the service signature still required a non‑optional pdfBuffer, so TypeScript complained and the code never compiled.
For the provider crash, I wrapped the .map() in a ternary (providers ? providers.map(...) : []) but that only postponed the error because the API sometimes returned an empty object instead of an array, and the type definitions still expected Provider[].
All those patches were superficial; they didn’t address the root cause: the lack of a shared abstraction for the dashboard and incorrect type contracts in the email service.
The Implementation
1. Introduce a Shared “Hoy” Pattern
The sales/rentals dashboard already had a clean pattern: a top‑level page (/dashboard/hoy/page.tsx) that pulls a “today” snapshot from the API, renders it inside CrmShell, and reuses the same layout for both sales and rentals. I extracted that pattern into a reusable component HoyDashboard placed under apps/web/src/app/_components/.
File: apps/web/src/app/_components/HoyDashboard.tsx
import React from "react";
import { useSWR } from "swr";
import CrmShell from "./CrmShell";
import type { HoyData } from "@/types/hoy";
interface Props {
endpoint: string; // e.g. "/condominios/hoy"
context: "condos" | "rentas" | "ventas";
}
export default function HoyDashboard({ endpoint, context }: Props) {
const { data, error } = useSWR<HoyData>(endpoint);
if (error) return <div className="error">Failed to load data</div>;
if (!data) return <div className="loading">Loading…</div>;
return (
<CrmShell context={context}>
{/* Render generic cards, charts, etc. */}
<section className="grid gap-4">
<h2 className="text-xl font-bold">Hoy – {context}</h2>
{/* Example: total contracts */}
<div className="card">
<p>Total contracts: {data.totalContracts}</p>
</div>
{/* …more UI… */}
</section>
</CrmShell>
);
}
Now the condo dashboard page simply imports this component with the proper endpoint.
File: apps/web/src/app/condominios/dashboard/hoy/page.tsx
import HoyDashboard from "@/app/_components/HoyDashboard";
export default function CondoHoyPage() {
return (
<HoyDashboard endpoint="/api/condominios/hoy" context="condos" />
);
}
The old dashboard/page.tsx now redirects to the new “hoy” route, keeping backwards compatibility.
2. Align Shell Navigation
CrmShellCondos still pointed to sales routes. I edited it to use condo‑specific navigation links.
Diff snippet (apps/web/src/app/_components/CrmShellCondos.tsx):
-<a href="/ventas/dashboard" onClick={() => setOpenMenu(null)} style={{ display:"flex" }}>
+<a href="/condominios/dashboard" onClick={() => setOpenMenu(null)} style={{ display:"flex" }}>
Dashboard
</a>
Only a one‑line change, but it eliminates the broken navigation that was confusing users and generating 404 errors in Sentry.
3. Fix the Email Service Signature
The monthly report email service was defined as:
export async function sendOwnerMonthlyReport(data: {
propertiesCount: number;
activeContractsCount: number;
pendingCount: number;
pdfBuffer: Buffer;
}) { … }
When the PDF generation failed (e.g., missing data), pdfBuffer became undefined, causing the Sentry “attachment missing” error. I made the buffer optional and added a runtime guard.
File: apps/api/src/email/email.service.ts
@@ -475,15 +475,26 @@ export async function sendOwnerMonthlyReport(data: {
activeContractsCount: number;
pendingCount: number;
- pdfBuffer: Buffer;
+ pdfBuffer?: Buffer; // <-- made optional
}) {
const { pdfBuffer, ...rest } = data;
const attachments = pdfBuffer
- ? [{ filename: "report.pdf", content: pdfBuffer }]
- : []; // previously always added an empty attachment
+ ? [{ filename: "report.pdf", content: pdfBuffer }]
+ : []; // safe guard – no attachment if buffer missing
await transporter.sendMail({
to: ownerEmail,
subject: "Monthly Report",
html: renderTemplate(rest),
attachments,
});
}
The TypeScript compiler now accepts calls without a buffer, and Sentry no longer logs a missing attachment error.
4. Guard the Providers .map()
The providers page fetched a list of complex providers and then called .map() on the result without confirming it was an array.
File: apps/web/src/app/condominios/proveedores/page.tsx
@@ -43,8 +43,14 @@ export default function ProveedoresPage() {
const [r, c] = await Promise.all([
fetch(`${getApiBase()}/complex/complexes?pageSize=50`, { headers:h }),
]);
const d = await r.json(); const cxd = await c
- const providers = d.results.map(p => ({ id: p.id, name: p.name }));
+ // Guard against malformed API responses
+ const providers = Array.isArray(d.results)
+ ? d.results.map(p => ({ id: p.id, name: p.name }))
+ : []; // fallback to empty list
Now the page renders gracefully even if the backend returns { results: null }.
5. Version Bump & Docs Sync
All packages in the monorepo were still at 1.8.0. I updated the package.json files for both API and web apps
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-31
#playadev #buildinpublic
Top comments (0)