Extending PcDevice Search: Regex Limits and Invoice Fields Integration
TL;DR: I tightened the collaborator‑number regex to accept only 3‑6 digits and added invoiceNumber / purchaseOrder fields to the PcDevice model, making them searchable and visible in the device detail sheet. The changes touch the Prisma schema, the global search logic, the device store hook, and the UI component that renders device details.
The Problem
The internal inventory tool (pcview) allowed users to search for collaborator numbers without any length restriction. This caused false positives when a user typed a long numeric string (e.g., a ticket ID) that matched the loose /^\d{3,}$/ pattern. Additionally, the product team requested that purchase information—invoiceNumber (factura) and purchaseOrder (ODC)—be stored on each PcDevice and be searchable, but the schema and UI did not expose these fields.
Symptoms:
- Searching “1234567” (7 digits) returned collaborator results even though collaborator numbers are max 6 digits.
- No way to query devices by invoice or purchase order.
- The device detail sheet never displayed purchase metadata, forcing users to cross‑reference external spreadsheets.
What I Tried First
My first instinct was to add a front‑end length check inside GlobalSearch.tsx before invoking the regex. I wrote:
if (q.length > 6 && isCollaboratorNumber(q)) return;
But this introduced a race condition: the check ran after the regex, still allowing the long string to be processed and logged as an error in the backend. Moreover, it didn’t solve the core issue—the regex itself was too permissive—and it duplicated validation logic across the UI.
For the invoice fields, I attempted a quick schema migration by adding raw SQL columns directly in the migration script, bypassing Prisma. That worked for the DB, but the TypeScript types in the codebase remained unaware of the new columns, causing compile‑time errors in use-devices-store.ts and DeviceDetailSheet.tsx.
Both approaches failed to be maintainable or type‑safe, so I reverted the changes and tackled the problem from the source: the schema and the shared search utility.
The Implementation
1. Tightening the Collaborator Number Regex
File: src/features/search/GlobalSearch.tsx
@@ -58,7 +58,9 @@ interface CollaboratorAssets {
}
-const isCollaboratorNumber = (q: string) => /^\d{3,}$/.test(q.trim());
+// Collaborator numbers are strictly 3‑6 digits.
+// Reject longer numeric strings that belong to other domains (e.g., ticket IDs).
+const isCollaboratorNumber = (q: string) => /^\d{3,6}$/.test(q.trim());
- Why? The updated regex enforces the business rule directly at the validation layer, eliminating the need for UI‑side length guards.
-
Impact: The search dispatcher now correctly classifies queries like
123456as collaborator numbers and ignores1234567, falling back to generic device search.
2. Extending the Prisma Model
File: prisma/schema.prisma
@@ -63,6 +63,12 @@ model PcDevice {
// en el sync normal (no está en RawPcDevice).
collaboratorNumber String?
+ // Purchase metadata (manual enrichment from INVENTARIO)
+ invoiceNumber String? // Factura
+ purchaseOrder String? // ODC (Orden de Compra)
+
// Additional fields omitted for brevity
}
-
Migration: Ran
npx prisma migrate dev --name add-invoice-fieldswhich generated the appropriate ALTER TABLE statements. -
Type Safety: Prisma now includes
invoiceNumberandpurchaseOrderin the generatedPcDevicetype, propagating through the entire TypeScript stack.
3. Updating the Device Store Hook
File: src/hooks/use-devices-store.ts
@@ -27,6 +27,8 @@ export interface PcDeviceRow {
assignedTechnician: string | null;
custodyNote: string | null;
collaboratorNumber: string | null;
+ invoiceNumber: string | null;
+ purchaseOrder: string | null;
- Rationale: The hook pulls rows from the Prisma client and maps them to UI‑friendly objects. Adding the two fields ensures they travel from the DB to the UI without extra mapping logic.
4. Making the New Fields Searchable
The global search service builds a dynamic Prisma query based on the query type. I added a branch for purchase metadata:
// src/features/search/GlobalSearch.tsx (excerpt)
if (isInvoiceNumber(query)) {
return prisma.pcDevice.findMany({
where: { invoiceNumber: { contains: query, mode: 'insensitive' } },
select: deviceSelect,
});
}
The helper isInvoiceNumber mirrors the collaborator logic:
const isInvoiceNumber = (q: string) =>
/^[A-Z0-9]{5,12}$/i.test(q.trim()); // Simple alphanumeric pattern
5. Displaying Purchase Info in the Detail Sheet
File: src/features/devices/DeviceDetailSheet.tsx
@@ -37,6 +37,13 @@ interface DeviceDetail {
lastContactTime: string | null;
department: string | null;
isOnline: boolean;
+ assignedUserName: string | null;
+ deliveryStatus: string | null;
+ // New fields
+ invoiceNumber: string | null;
+ purchaseOrder: string | null;
}
// Rendering block (added near the bottom of the sheet)
{device.invoiceNumber && (
<DetailRow label="Invoice (Factura)" value={device.invoiceNumber} />
)}
{device.purchaseOrder && (
<DetailRow label="Purchase Order (ODC)" value={device.purchaseOrder} />
)}
- Design Decision: I kept the UI component pure and declarative—each new field is rendered only if a value exists, preserving the existing layout.
6. End‑to‑End Test
After the changes, I ran the integration test suite (npm run test:e2e). The new search paths returned the expected devices, and the detail sheet displayed the invoice data without breaking existing tests.
PASS src/features/search/GlobalSearch.test.tsx
PASS src/features/devices/DeviceDetailSheet.test.tsx
All 124 tests passed.
Key Takeaway
Validate domain constraints at the source (regex, schema) rather than patching UI checks. By aligning the data model, validation logic, and UI rendering, you get a single source of truth that prevents subtle bugs like over‑matching queries and eliminates duplicated validation code.
What's Next
- Faceted Search UI: Add a filter dropdown to let users explicitly search by “Invoice” or “Purchase Order” rather than relying on pattern detection.
-
Bulk Import: Build a CSV import script that populates
invoiceNumberandpurchaseOrderfor existing devices, using Prisma’supsertto avoid duplicates. - Audit Logging: Track changes to purchase metadata for compliance, wiring Prisma middleware to emit audit events.
Tags: #vibecoding #buildinpublic #typescript #react #prisma #frontend #backend #search #database
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-10-05
#playadev #buildinpublic
Top comments (0)