Adding Lifecycle Status & Device‑Type Splits to the PCView Inventory Dashboard
TL;DR:
I extended the Prisma schema and UI to track equipment lifecycle (Baja / Transferido) and introduced real sidebar categories for Phones vs. Tablets. The changes required schema migration, store hooks updates, UI badge components, and route separation, all while keeping the sync process safe.
The Problem
Our inventory dashboard was treating every mobile entry as a generic “MobileDevice”. Two pain points emerged:
-
Lifecycle visibility – Technicians needed to see if a device was de‑commissioned (
Baja) or reassigned (Transferido). The UI only showed online/offline status, so we kept adding ad‑hoc notes in a free‑text column, which broke the sync logic. - Poor navigation – The sidebar listed “Mobile” as a single entry, but the team uses phones and tablets very differently (different filters, different reporting). Clicking “Mobile” opened a mixed list, making it hard to locate a specific device type.
Both issues manifested as noisy UI and extra manual work. The sync service (genericSync.service.ts) was also warning us about “unexpected null” when it tried to write lifecycle fields that weren’t in the schema.
What I Tried First
My initial attempt was to add a lifecycle column directly in the UI without touching the database. I created a statusNote field in src/components/inventory/status-badge.tsx and stored the value in a local React state.
const [lifecycle, setLifecycle] = useState<string>("Active");
This worked visually, but the sync service threw:
Error: Column `lifecycleStatus` does not exist on model `PcDevice`
Because the sync layer (mobileSync.service.ts) pulls the Prisma model directly, any field not defined in schema.prisma caused a runtime exception. Moreover, the value never persisted, so a page refresh wiped the status. I also tried to filter phones vs. tablets by adding a query param (?type=phone) to the existing MobileView component, but the sidebar still rendered a single “Mobile” entry, so navigation didn’t improve.
The approach failed because the source of truth is the Prisma schema, not the UI.
The Implementation
1. Extend the Prisma schema
I added two fields to the PcDevice and MobileDevice models. The lifecycleStatus field stores "Baja" | "Transferido" | "Active" and lifecycleNote holds an optional comment. For MobileDevice I introduced a type enum to differentiate phones and tablets.
// prisma/schema.prisma
@@ -28,6 +28,13 @@ model PcDevice {
department String?
isOnline Boolean @default(false)
+ // Manual operational classification — NEVER touched by sync (sync only
+ // writes fields present in the sheet).
+ lifecycleStatus String? // "Baja", "Transferido", "Active"
+ lifecycleNote String?
+}
+
+enum MobileType {
+ Phone
+ Tablet
+}
@@ -101,6 +108,9 @@ model MobileDevice {
// Derived, recomputed every sync — not stored in the sheet.
department String?
isOnline Boolean @default(false)
+ // "Phone" | "Tablet" — classification used by UI
+ type MobileType
}
After the migration (npx prisma migrate dev --name add-lifecycle-and-type) the DB reflected the new columns.
2. Update the TypeScript types
I synced the generated types in src/types/device.ts:
export interface PcDevice {
id: string;
name: string;
department?: string;
isOnline: boolean;
lifecycleStatus?: "Baja" | "Transferido" | "Active";
lifecycleNote?: string;
}
export interface MobileDevice {
id: string;
name: string;
department?: string;
isOnline: boolean;
type: "Phone" | "Tablet";
}
3. Extend the device store hooks
src/hooks/use-devices-store.ts now pulls the new fields from Prisma and exposes them to the UI.
// src/hooks/use-devices-store.ts
@@ -12,6 +12,8 @@ export interface PcDeviceRow {
lastContactTime: string | null;
department: string | null;
isOnline: boolean;
+ lifecycleStatus: string | null;
+ lifecycleNote: string | null;
}
The fetchDevices service (src/services/dashboard.service.ts) also returns the new columns:
// src/services/dashboard.service.ts
@@ -4,7 +4,7 @@ export async function getDeviceCounts() {
const [total, online, offline, lowDisk, deptGroups] = await Promise.all([
prisma.pcDevice.count(),
- prisma.pcDevice.count({ where: { isOnline: true } }),
+ prisma.pcDevice.count({ where: { isOnline: true, lifecycleStatus: { not: "Baja" } } }),
prisma.pcDevice.count({ where: { isOnline: false } }),
prisma.pcDevice.count({ where: { diskSpace: { lt: 10 } } }),
prisma.pcDevice.groupBy({
4. New badge components
I added LifecycleBadge to src/components/inventory/status-badge.tsx:
// src/components/inventory/status-badge.tsx
export function StatusBadge({ isOnline }: { isOnline: boolean }) {
return isOnline ? (
<Badge variant="default">Online</Badge>
) : (
<Badge variant="destructive">Offline</Badge>
);
}
export function LifecycleBadge({ status }: { status: string }) {
const variant = status === "Baja"
? "destructive"
: status === "Transferido"
? "secondary"
: "default";
return <Badge variant={variant}>{status || "Active"}</Badge>;
}
The InventoryTable now renders both badges per row:
// src/features/inventory/InventoryTable.tsx
<TableRow key={device.id}>
<TableCell>{device.name}</TableCell>
<TableCell><StatusBadge isOnline={device.isOnline} /></TableCell>
<TableCell><LifecycleBadge status={device.lifecycleStatus ?? "Active"} /></TableCell>
<TableCell>{device.department ?? "—"}</TableCell>
</TableRow>
5. Split the mobile view into “Phones” and “Tablets”
Two new pages were added under the dashboard route:
// src/app/(dashboard)/phones/page.tsx
import { MobileView } from "@/features/mobile/MobileView";
export default function PhonesPage() {
return (
<MobileView filterType="Phone" title="Phones" />
);
}
// src/app/(dashboard)/tablets/page.tsx
import { MobileView } from "@/features/mobile/MobileView";
export default function TabletsPage() {
return (
<MobileView filterType="Tablet" title="Tablets" />
);
}
MobileView now receives filterType and filters the Prisma query accordingly:
// src/features/mobile/MobileView.tsx
@@ -8,7 +8,11 @@ export function MobileView({ filterType, title }: { filterType: "Phone" | "Tablet"; title: string }) {
const { data, isLoading } = useQuery(["mobileDevices", filterType], async () => {
- return await prisma.mobileDevice.findMany({ where: {} });
+ return await prisma.mobileDevice.findMany({
+ where: {
+ type: filterType,
+ },
+ });
});
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/pcview · 2026-08-20
#playadev #buildinpublic
Top comments (0)