DEV Community

Roberto Luna
Roberto Luna

Posted on

Extending PcDevice Search: Regex Limits and Invoice Fields Integration

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

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());
Enter fullscreen mode Exit fullscreen mode
  • 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 123456 as collaborator numbers and ignores 1234567, 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
 }
Enter fullscreen mode Exit fullscreen mode
  • Migration: Ran npx prisma migrate dev --name add-invoice-fields which generated the appropriate ALTER TABLE statements.
  • Type Safety: Prisma now includes invoiceNumber and purchaseOrder in the generated PcDevice type, 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;
Enter fullscreen mode Exit fullscreen mode
  • 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,
  });
}
Enter fullscreen mode Exit fullscreen mode

The helper isInvoiceNumber mirrors the collaborator logic:

const isInvoiceNumber = (q: string) =>
  /^[A-Z0-9]{5,12}$/i.test(q.trim()); // Simple alphanumeric pattern
Enter fullscreen mode Exit fullscreen mode

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

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 invoiceNumber and purchaseOrder for existing devices, using Prisma’s upsert to 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)