Workshop ERP · Technical service

Taller PCCoreA repair-shop ERP, from the work order to the quote the customer approves on their phone

Repair-shop ERP for PCCore: work orders, quotes customers approve by link, PDFs by email or WhatsApp, and the previous system's history migrated in.

Technologies

  • Next.js 16
  • React 19
  • TypeScript
  • NestJS 11
  • Prisma 7
  • PostgreSQL
  • Tailwind CSS v4
  • TanStack Query
  • Zustand
  • React Hook Form + Zod
  • Turborepo + pnpm
  • PDFKit
  • ExcelJS
  • Resend
  • Cloudflare R2
  • node-firebird
  • Swagger / OpenAPI
  • Jest · Vitest
  • Docker
  • Railway
Home dashboard of PCCore's workshop ERP in demo mode, with sample data: delivery commitments and workload per technician
  • 10,000+

    work orders migrated from the old system

  • 37

    ERP screens

  • 40

    API modules

  • 12

    exportable reports

My role

Full stack development of the workshop ERP - the API, the web panel and the shared package -, from the data model and the migration off the previous system to the Railway deployment and the security hardening. Delivered through NM Tech Studio.

The problem

PCCore repairs computers, laptops and printers in Durán, Ecuador. The workshop ran on SGTaller 3, a Delphi desktop system on Firebird that held its whole history - more than 10,000 work orders, 6,679 customers and 1,017 items - and much of its logic in stored procedures and triggers. Same business and same customers as the online store, yet they shared neither customer records nor inventory, and finding out how a repair was going or what it would cost meant the customer had to call the shop.

What I built

  • An ERP with 37 screens - work orders, quotes, customers, devices, RMA, schedule, inventory, purchasing, sales and cash - on top of a 40-module REST API.
  • The full work-order cycle: intake with the accessories received and a promised date, an automatically linked quote, the technical report, parts that draw down stock, an activity log, photos and delivery.
  • Customer-facing documents: the work order and the quote as PDFs, sent by email or WhatsApp, with every send recorded so the same thing is not sent twice.
  • Quote approval by link: the customer reviews the breakdown on their phone and approves it without creating an account; the date, the accepted total and the IP are kept on record.
  • A printed work order with the pickup terms and a QR code that opens the repair's tracking page on PCCore's online store.
  • Inventory with per-item stock control, stocktakes with adjustments and the store catalogue imported every hour as items.
  • A delivery schedule by promised date and 12 reports - by technician, customer, brand, device type, warranty and period -, exportable to Excel and PDF.
  • Four roles - administrator, technician, front desk and sales - on a permission matrix by resource and action shared by the API and the web.
  • Migration off the previous system from inside the app: the admin uploads the Firebird backup, the system analyses it and only replaces the data after explicit confirmation and a fresh backup.

How it's structured

A pnpm workspace with Turborepo, deployed as two services - the API and the panel -. It shares PostgreSQL with PCCore's online store, but keeps its own schema, migrations and authentication.

  • apps/api: NestJS 11 + Prisma 7, a REST API under /api/v1 with Swagger docs outside production; apps/web: Next.js 16 with TanStack Query; packages/shared: the types, enums and permission matrix both sides use.
  • The browser never talks to the API directly: the web exposes it through a BFF route that checks the origin, caps size and time, and streams large uploads through. The session travels in an httpOnly cookie.
  • Documents are generated on the server: the work-order and quote PDFs are built and sent from the API with PDFKit and Resend, and reports export to Excel with ExcelJS and to PDF with PDFKit.
  • Scheduled jobs in the API: the hourly import of the store catalogue and pg_dump backups to Cloudflare R2, at a frequency set from Settings, with a log that also records failed runs.
  • Deployed on Railway with a multi-stage Dockerfile per service, non-root containers and health checks, on PostgreSQL 18.
  • Tests with Jest on the API and Vitest on the web; CodeQL and a security workflow with dependency audit, tests and build run on every push and pull request, and Dependabot watches the dependencies.

The workshop flow

The work order is the central entity - it already had 47 columns in the original system - and everything else hangs off it: the quote, the parts, the notes, the activity log and the tracking.

  • Intake: the front desk records the customer (or a walk-in customer, with no record), the device by serial number, the fault, the accessories received, the technician, the branch and the promised date. On creation the order is numbered, writes its first status to the history and generates its linked quote, all in a single transaction.
  • Quote: labour and materials lines with their tax; it moves from not quoted to quoted, and from there to accepted or not accepted, with every change in its history.
  • Sending: the PDF goes out by email, with the breakdown in the body too for people who do not open attachments on their phone, or over WhatsApp as a token-protected link, because wa.me only accepts text. Every send is recorded with channel, destination and user.
  • Approval: the customer opens their link, reviews and approves with a second click. The approval is stored atomically - two quick taps do not leave two records - and is noted in the quote's history and in the work order's activity log.
  • Repair: the technician enters the report, the parts used, which draw down stock when the item tracks it, and notes; the order moves from not reviewed to in repair to finished, with warranty branches and RMA to the supplier.
  • Tracking: the QR code printed on the work order opens the store's tracking page, which reads the stage, the report, the notes and the quote straight from the workshop database.
  • Delivery: the Deliver button stores the delivery date and notes it in the activity log; the paper carries the pickup terms set in Settings, and the schedule and dashboard show what was promised and is still pending.

Layered security

The ERP holds the records of thousands of customers and its quotes are approved from a link with no session, so security was reinforced in layers without changing screens or flows for the people who use it.

  • The BFF checks Origin and Sec-Fetch-Site on every mutation, caps requests at 32 MB and 120 seconds, and when the API fails it returns a generic error with an x-request-id to trace it.
  • An internal secret, compared in constant time, lets the API reject any access that does not come through the panel.
  • Temporary account lockout after failed attempts: while it lasts, sign-in is refused without even comparing the password.
  • Security headers on the web: CSP, anti-framing, nosniff, referrer policy and HSTS in production, with no X-Powered-By.
  • Limits in PostgreSQL - pool size, statement_timeout, query_timeout and idle_in_transaction_session_timeout - so a heavy query cannot hog the database.

Inside the site

  • Taller PCCore: Repair orders with status, device and actions (demo mode, sample data)
    Repair orders with status, device and actions (demo mode, sample data).
  • Taller PCCore: Budgets with their approval status (demo mode)
    Budgets with their approval status (demo mode).
  • Taller PCCore: Promised-delivery agenda, overdue and upcoming (demo mode)
    Promised-delivery agenda, overdue and upcoming (demo mode).
  • Taller PCCore: Parts and inventory (demo mode)
    Parts and inventory (demo mode).
  • Taller PCCore: Reports by technician, customer, warranty and delivery (demo mode)
    Reports by technician, customer, warranty and delivery (demo mode).
  • Taller PCCore: Statistical reports of orders by status (demo mode)
    Statistical reports of orders by status (demo mode).

The admin panel

Each module covers one part of the day-to-day operation, without leaving the panel.

  • Home

    Urgent things first - overdue commitments and today's deliveries -, then workload per technician and queue age, and the monthly flow last. Revenue only reaches the administrator, filtered on the server.

  • Work orders

    Device intake with accessories, fault, technician, branch and promised date; status changes with history, activity log, notes, photos and bulk intake of several devices at once.

  • Quotes

    Labour from a task catalogue with reference prices and materials from inventory, with tax, status history and customer approval by link.

  • Customer sends

    Work-order and quote PDFs by email or WhatsApp; every send is recorded and the system warns when that document has already gone out.

  • Customers

    Customer records with branches and an accent-insensitive search - 'munoz' finds 'MUÑOZ' -; the same records the store's checkout looks up.

  • Devices

    Each device is identified by its serial number and keeps its repair history, with a technical sheet when it is an electric motor.

  • Items and inventory

    Per-item stock control, parts drawn down when used on a work order, stocktakes with adjustments and the store catalogue imported every hour.

  • Schedule

    Undelivered orders sorted by promised date and split into overdue, due today and upcoming.

  • Reports

    12 reports - repairs by technician, customer, brand or device type, deliveries, warranties and orders received per period -, exportable to Excel and PDF.

  • Staff

    Technicians and front-desk staff with their commission; the dashboard shows how many open, ready-for-pickup and overdue orders each technician has.

  • Users and roles

    Four roles - administrator, technician, front desk and sales - on a permission matrix by resource and action, with account lockout after failed sign-in attempts.

  • Settings

    Pickup terms printed on the work order, the tracking address behind the QR code, automatic backups to Cloudflare R2 and data migration from a file.

Technical decisions

  • The email button opens a page and approval takes a second click: mail filters open every link to scan it, and a link that approved on open would get approved by a bot. If the shop changed the total while the customer was reviewing it, the approval is rejected.
  • Tracking and approval tokens are generated randomly by PostgreSQL and are never derived from the order number, which is sequential, or from the national ID. They are also separate from each other: the QR token is printed on paper, and approving a charge calls for a key that only reaches the customer's email or WhatsApp.
  • The permission matrix lives in a shared package: the web uses it to show each role only what it can see, and a parity test fails if any API decorator drifts from it. Sensitive data, such as the dashboard's revenue, is cut on the server, not with an if in the UI.
  • A demo mode switched on by a build variable: the web never touches the network or the database, serves data generated from a single source so the dashboard, lists and reports agree with each other, with dates relative to today, and enables only the 10 modules of the workshop cycle. Anything still half-built stays out: an empty module in a demo does more harm than a missing one.
  • One database, two owners: the workshop shares PostgreSQL with PCCore's store, but Prisma only knows the workshop tables and the store keeps its own SQL migrations. The link between a product and an item is an id with a unique index, with no declared foreign key between the two platforms.
  • The store publishes prices with VAT included and the workshop adds VAT when quoting, so importing a product stores the net price: an item priced $650 on the web still costs $650 at the counter, instead of $747.50.

Challenges

  • Rebuilding SGTaller 3 - Delphi on Firebird 2.1, 95 tables, 12 stored procedures and 132 triggers - without losing its logic: numbering, status history and each order's automatic quote now happen inside API transactions. The migration audit matched the main tables row by row and the sales amounts to the cent.
  • Cleaning up the inherited data: empty dates came out of Firebird as 12/30/1899, which made all 1,226 open orders show up as overdue. The dashboard discards dates before 1990 as migration sentinels.
  • Making a data-replacing migration safe to run from the browser: the format is detected from the file signature, never the extension, a backup is taken before purging, the purge is an allow-list that never touches the store tables or the users, and files of hundreds of megabytes stream through the BFF.

Result

The ERP runs in production on Railway - the workshop API and panel - on the same PostgreSQL as the store. The previous system's history moves into the new one from inside the app, every work order is printed with its QR code, and customers follow their repair and approve their quote from their phone, without calling and without creating an account. The repository runs CodeQL, dependency audits and automated tests in CI, the database backs itself up to Cloudflare R2, and the demo mode makes it possible to show the system with sample data without exposing any real customer.

Need something like this?

Tell me what you're building and I'll come back with scope, stack and a realistic timeline.