DEV Community

Roberto Luna
Roberto Luna

Posted on

Flagging Duplicate Tablet Rooms in the Dashboard and Inventory Views

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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:

  1. Pagination – The duplicate could be on another page, so the badge never appeared.
  2. Performance – Running Array.findIndex on 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
  }));
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 a destructive variant 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)