DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding Yodeck Sync to PCView: Extending the Inventory with a New Category

Adding Yodeck Sync to PCView: Extending the Inventory with a New Category

TL;DR: I added a full‑stack Yodeck inventory pipeline to the PCView monorepo, exposing API routes, a new Prisma model, and a dashboard page. The change lets us pull the “approved” Yodeck category from Google Sheets and keep the UI in sync with the database.


The Problem

Our inventory dashboard tracks cameras, office phones, guest phones, and a handful of other assets. The latest client request was to include Yodeck digital‑signage players (Raspberry Pi units) as a new inventory category.

The immediate blocker was two‑fold:

  1. No schemaprisma/schema.prisma had no model for Yodeck devices, so the ORM rejected any attempt to query or persist them.
  2. Missing UI & API – The dashboard’s settings page only rendered sync buttons for existing categories, and there were no Next.js API routes to fetch or sync Yodeck data.

When I tried to add a quick endpoint that returned a hard‑coded list of Yodeck devices, the request failed with a 404 because the route didn’t exist, and the Prisma client threw Model "Yodeck" is not defined in your schema.prisma.


What I Tried First

My first attempt was a throw‑away mock:

// src/app/api/yodeck/devices/route.ts (initial version)
export async function GET() {
  return NextResponse.json({
    devices: [{ id: "dev-001", name: "Lobby Screen", status: "online" }],
  });
}
Enter fullscreen mode Exit fullscreen mode

I added the route, hit /api/yodeck/devices, and the JSON came back. However, this approach had two fatal flaws:

  • No persistence – The data never hit the database, so any subsequent sync would overwrite it.
  • No type safety – I was returning an ad‑hoc object that didn’t match any TypeScript interface, leading to mismatched props downstream.

I also tried to extend the existing GenericSyncButton component by passing a custom endpoint, but the component only accepted a hard‑coded sheetTabName prop, which meant the Google Sheets integration would never know which tab to read for Yodeck.

Both attempts proved unsustainable for a production‑grade feature.


The Implementation

1. Extend the configuration

pcview.config.ts now knows about the Yodeck Google Sheet tab:

// pcview.config.ts
export interface PcViewConfig {
  cameras: { gid: string };
  guestPhones: { gid: string };
  officePhones: { gid: string };
+  yodeck: { gid: string }; // <- new entry
  alerts: {
    // ...
  };
}
Enter fullscreen mode Exit fullscreen mode

The gid is the Google Sheet tab identifier that the YodeckGoogleSheetsProvider will read.

2. Add a Prisma model

The biggest change lives in prisma/schema.prisma. I introduced a Yodeck model that mirrors the shape of the Google Sheet rows:

// prisma/schema.prisma
model Yodeck {
  id          String   @id @default(cuid())
  name        String
  serial      String   @unique
  approved    Boolean  @default(false)
  location    String?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  syncLogId   String?  @relation(fields: [syncLogId], references: [id])
  @@index([createdAt])
  @@index([syncLogId])
}
Enter fullscreen mode Exit fullscreen mode
  • approved reflects the “missing approved category” the client needed.
  • A foreign key to SyncLog lets us audit each import batch.

After updating the schema, I ran:

npx prisma migrate dev --name add_yodeck_model
Enter fullscreen mode Exit fullscreen mode

which generated the migration and updated the SQLite/Postgres DB.

3. New TypeScript type

To keep the codebase type‑safe, I added a Device union in src/types/device.ts:

// src/types/device.ts
export type Device = Camera | OfficePhone | GuestPhone | Yodeck;

export interface Yodeck {
  id: string;
  name: string;
  serial: string;
  approved: boolean;
  location?: string;
}
Enter fullscreen mode Exit fullscreen mode

Now any service that works with generic devices can handle Yodeck without casting.

4. Google Sheets provider

src/providers/googleSheets/YodeckGoogleSheetsProvider.ts implements the same interface as the other providers:

// src/providers/googleSheets/YodeckGoogleSheetsProvider.ts
import { GoogleSheetsProvider } from "./GoogleSheetsProvider";
import { Yodeck } from "@/types/device";

export class YodeckGoogleSheetsProvider extends GoogleSheetsProvider<Yodeck> {
  protected mapRow(row: string[]): Yodeck {
    const [id, name, serial, approved, location] = row;
    return {
      id,
      name,
      serial,
      approved: approved.toLowerCase() === "true",
      location: location || undefined,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

The provider reads the tab identified by config.yodeck.gid and returns a typed array of Yodeck objects.

5. Sync service

The core sync logic lives in src/services/yodeckSync.service.ts:

// src/services/yodeckSync.service.ts
import { prisma } from "@/lib/prisma";
import { YodeckGoogleSheetsProvider } from "@/providers/googleSheets/YodeckGoogleSheetsProvider";
import { SyncLog } from "@prisma/client";

export async function runYodeckSync(): Promise<SyncLog> {
  const provider = new YodeckGoogleSheetsProvider();
  const rows = await provider.fetchRows();

  const syncLog = await prisma.syncLog.create({
    data: { entity: "YODECK", startedAt: new Date() },
  });

  const upserts = rows.map((device) =>
    prisma.yodeck.upsert({
      where: { serial: device.serial },
      update: { ...device, syncLogId: syncLog.id },
      create: { ...device, syncLogId: syncLog.id },
    })
  );

  await Promise.all(upserts);

  return prisma.syncLog.update({
    where: { id: syncLog.id },
    data: { finishedAt: new Date() },
  });
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Upsert ensures we don’t duplicate devices on repeated syncs.
  • The syncLogId ties each row to the import batch for audit purposes.

6. API routes

Three new endpoints expose the Yodeck data:

Route Method File
/api/yodeck/devices GET src/app/api/yodeck/devices/route.ts
/api/yodeck/devices/[id] GET src/app/api/yodeck/devices/[id]/route.ts
/api/yodeck/sync POST src/app/api/yodeck/sync/route.ts

List devices

// src/app/api/yodeck/devices/route.ts
import { NextResponse } from "next/server";
import { listGeneric } from "@/services/genericDevice.service";

export async function GET() {
  const devices = await listGeneric("YODECK");
  return NextResponse.json({ devices });
}
Enter fullscreen mode Exit fullscreen mode

Single device

// src/app/api/yodeck/devices/[id]/route.ts
import { NextResponse } from "next/server";
import { getGenericById } from "@/services/genericDevice.service";

export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const device = await getGenericById("YODECK", params.id);
  return device ? NextResponse.json(device) : NextResponse.notFound();
}
Enter fullscreen mode Exit fullscreen mode

Trigger sync


ts
// src/app/api/yodeck/sync/route.ts
import { NextResponse } from "next/server";
import { runYodeckSync } from "@/services/yodeckSync.service";

export async function POST() {
  try {
    const result = await runYodeckSync();
    return NextResponse.json({ success: true, result });
  } catch (e) {
    console.error

---

*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/pcview` · 2026-08-22*

\#playadev #buildinpublic
Enter fullscreen mode Exit fullscreen mode

Top comments (0)