Flagging Duplicate Tablet Rooms in the Dashboard and Inventory Views
TL;DR: I added a “Duplicate Rooms” card to the Dashboard and highlighted duplicate tablets in the Inventory table. The change lives in a new UI component, a service helper, and a few tweaks to the existing table logic.
The Problem
Our SaaS product lets hotels register tablets per room. Over time a few rooms ended up with more than one tablet recorded in the database. The UI showed those rooms as separate entries, which confused staff and broke downstream reports. The symptom was simple:
[WARN] Duplicate tablet detected for room 203 – 2 entries found
The dashboard didn’t surface the issue, and the inventory table listed the same room twice with no visual cue. We needed a fast way to surface duplicates without redesigning the whole data model.
What I Tried First
My first instinct was to handle the problem purely on the client side:
// src/features/inventory/InventoryTable.tsx (initial attempt)
const duplicateRooms = rows.filter(
(r, i, arr) => arr.findIndex(x => x.roomId === r.roomId) !== i
);
I added a red badge next to any row whose roomId appeared more than once. The approach worked for the current page, but it failed in two ways:
- Pagination – The duplicate could be on another page, so the badge never appeared.
-
Performance – Running
Array.findIndexon every render caused noticeable lag on tables with >1k rows.
Because the logic belonged to the UI layer, it also duplicated the same detection code in the dashboard later on. I needed a single source of truth and a solution that scales.
The Implementation
1. Service Layer – dashboard.service.ts
I added a new helper that runs a single aggregated query on the backend and returns the list of rooms with more than one tablet. Keeping this logic in the service layer guarantees consistency across UI components.
// src/services/dashboard.service.ts
import { db } from "@/lib/db";
export async function getDuplicateRooms(): Promise<{ roomId: string; count: number }[]> {
// Returns rooms where tablet count > 1
const rows = await db
.selectFrom("tablets")
.select(["room_id as roomId"])
.groupBy("room_id")
.having(db.raw("COUNT(*) > 1"))
.execute();
// Transform to { roomId, count }
return rows.map(r => ({
roomId: r.roomId,
count: Number(r.count ?? 1), // count is added by the DB driver
}));
}
Why this design?
- Single source of truth – All UI components call the same service.
- Database‑level aggregation – Offloads work from the client and avoids pagination pitfalls.
- Typed return – The function returns a predictable shape, making it easy to consume in React.
2. UI Component – duplicate-rooms.tsx
A brand‑new component lives under src/components/dashboard/. It fetches the duplicate list via React Query (already used in the project) and renders a card with a badge for each problematic room.
// src/components/dashboard/duplicate-rooms.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { useQuery } from "@tanstack/react-query";
import { getDuplicateRooms } from "@/services/dashboard.service";
import { formatDistanceToNow } from "date-fns";
export default function DuplicateRooms() {
const { data, isLoading, error } = useQuery({
queryKey: ["duplicateRooms"],
queryFn: getDuplicateRooms,
staleTime: 5 * 60_000, // 5 minutes
});
if (isLoading) return <p>Loading duplicate rooms...</p>;
if (error) return <p>Failed to load duplicate rooms.</p>;
if (!data?.length) return null; // No duplicates – hide the card
return (
<Card className="bg-amber-50 border-amber-200">
<CardHeader>
<CardTitle>Duplicate Tablet Rooms</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{data.map(room => (
<div key={room.roomId} className="flex items-center justify-between">
<span>Room {room.roomId}</span>
<Badge variant="destructive">{room.count} tablets</Badge>
</div>
))}
<p className="text-sm text-muted-foreground">
Updated {formatDistanceToNow(new Date(), { addSuffix: true })}
</p>
</CardContent>
</Card>
);
}
Key points:
- Conditional rendering – The card disappears when there are no duplicates, keeping the dashboard clean.
-
Badge styling – Uses the existing UI library (
ui/badge) with adestructivevariant for immediate visual impact. - Stale‑time – Prevents excessive polling while still keeping data relatively fresh.
3. Dashboard Page – page.tsx
I wired the new component into the dashboard layout and added a tiny import change.
// src/app/(dashboard)/page.tsx
import { SyncStatusCard } from "@/components/dashboard/sync-status-card";
import { CountCards } from "@/components/dashboard/count-cards";
import { VersionBreakdown } from "@/components/dashboard/version-breakdown";
+import DuplicateRooms from "@/components/dashboard/duplicate-rooms";
export default function DashboardPage() {
return (
<main className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<SyncStatusCard />
<CountCards />
<VersionBreakdown />
+ <DuplicateRooms />
</main>
);
}
Only five lines were added (+5/-1 in the diff) – the new import and component placement. The rest of the page stays untouched.
4. Inventory Table – InventoryTable.tsx
The inventory view now highlights rows that belong to duplicate rooms. I added a Copy icon (from lucide-react) for quick room ID copying and a new column that checks the duplicate list.
tsx
// src/features/inventory/InventoryTable.tsx
import {
Search,
ArrowUpDown,
Copy, // <- new icon
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { getDuplicateRooms } from "@/services
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*
*Repo: `zaerohell/craveview` · 2026-08-18*
\#playadev #buildinpublic
Top comments (0)