Building a Manual Wi‑Fi Concierge Log with Prisma, Next.js 13 App Router, and a Client‑Side Dashboard
TL;DR: I added a new WifiConciergeLog model to Prisma, exposed CRUD endpoints via the Next.js 13 App Router, and built a full‑stack dashboard (WifiConciergeView) that lets the team record daily Wi‑Fi reports and see trend summaries—all without any external spreadsheet sync.
The Problem
Our field team needed a reliable way to capture daily Wi‑Fi performance notes for each resort. The existing process was a shared Google Sheet that was often out‑of‑sync, produced version‑control headaches, and gave us no programmatic way to compute trends (e.g., “average reports per day” or “most common issue”). The symptom in the app was a missing UI route and API that could store a simple JSON payload:
POST /api/wifi-concierge
{
"reportDate": "2026-08-26",
"elaboradoPor": "Juan Pérez",
"notes": "Intermittent drop at 14:00"
}
When we tried to hit that endpoint we got a 404 because the route didn’t exist, and there was no Prisma model to persist the data.
What I Tried First
My first attempt was to reuse the generic DeviceHistory model that already existed in prisma/schema.prisma. I added optional fields (wifiNotes, wifiReporter) and tried to filter them in the service layer. The approach failed for two reasons:
-
Schema bloat –
DeviceHistoryis meant for device telemetry, not manual free‑text notes. Adding unrelated columns made migrations noisy and broke existing queries that expected a strict shape. -
API mismatch – The existing
listDeviceHistoriesendpoint returned paginated telemetry, not a simple list of daily logs, so the front‑end would have needed heavy transformation.
The API returned a type error:
Error: Argument of type '{ wifiNotes?: string; ... }' is not assignable to parameter of type 'DeviceHistoryCreateInput'.
Property 'wifiNotes' does not exist on type 'DeviceHistoryCreateInput'.
That forced me to backtrack and create a dedicated model.
The Implementation
1. Prisma Model
I added a clean, purpose‑built model in prisma/schema.prisma. The diff added ~45 lines, but the core is:
/// Manual daily log for the Wi‑Fi Concierge – never synced to any Sheet
model WifiConciergeLog {
id Int @id @default(autoincrement())
reportDate DateTime @unique // one entry per day
elaboradoPor String? // optional reporter name
notes String? // free‑form observations
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([reportDate])
}
Running npx prisma migrate dev --name add-wifi-concierge-log generated the migration and updated the SQLite/Postgres DB.
2. Service Layer (src/services/wifiConcierge.service.ts)
All DB interactions are encapsulated here. The file contains 113 lines; the essential functions are:
// src/services/wifiConcierge.service.ts
import { prisma } from "@/lib/prisma";
export interface WifiConciergeLogInput {
reportDate: string; // ISO date, e.g. "2026-08-26"
elaboradoPor?: string;
notes?: string;
}
/**
* Insert or update a log for a given day.
* If a log already exists for `reportDate`, we upsert it.
*/
export async function upsertWifiConciergeLog(
data: WifiConciergeLogInput,
) {
const { reportDate, ...rest } = data;
return prisma.wifiConciergeLog.upsert({
where: { reportDate: new Date(reportDate) },
update: rest,
create: { reportDate: new Date(reportDate), ...rest },
});
}
/** Return all logs ordered by date descending */
export async function listWifiConciergeLogs() {
return prisma.wifiConciergeLog.findMany({
orderBy: { reportDate: "desc" },
});
}
/** Compute a simple summary: total logs and last 7‑day count */
export async function getWifiConciergeSummary() {
const total = await prisma.wifiConciergeLog.count();
const lastWeek = await prisma.wifiConciergeLog.findMany({
where: {
reportDate: {
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
},
});
return {
total,
last7Days: lastWeek.length,
};
}
3. API Routes
a. Main CRUD route (src/app/api/wifi-concierge/route.ts)
// src/app/api/wifi-concierge/route.ts
import { NextResponse } from "next/server";
import {
upsertWifiConciergeLog,
listWifiConciergeLogs,
} from "@/services/wifiConcierge.service";
export async function GET() {
try {
const logs = await listWifiConciergeLogs();
return NextResponse.json(logs);
} catch (e) {
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const payload = await request.json();
const log = await upsertWifiConciergeLog(payload);
return NextResponse.json(log, { status: 201 });
} catch (e) {
return NextResponse.json({ error: (e as Error).message }, { status: 400 });
}
}
b. Summary route (src/app/api/wifi-concierge/summary/route.ts)
// src/app/api/wifi-concierge/summary/route.ts
import { NextResponse } from "next/server";
import { getWifiConciergeSummary } from "@/services/wifiConcierge.service";
export async function GET() {
try {
const summary = await getWifiConciergeSummary();
return NextResponse.json(summary);
} catch (e) {
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
}
}
Both routes use the new App Router (route.ts) pattern, which automatically maps to /api/wifi-concierge and /api/wifi-concierge/summary.
4. Front‑End Page (src/app/(dashboard)/wifi-concierge/page.tsx)
A thin wrapper that renders the client component:
// src/app/(dashboard)/wifi-concierge/page.tsx
import { WifiConciergeView } from "@/features/wifi-concierge/WifiConciergeView";
export default function WifiConciergePage() {
return <WifiConciergeView />;
}
5. Dashboard Component (src/features/wifi-concierge/WifiConciergeView.tsx)
This is the heavy‑lifting UI (
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/greenview · 2026-08-27
#playadev #buildinpublic
Top comments (0)