The situation
An integrator runs many installation and service projects at once. Running one meant moving between sales orders, purchase orders, receipts, returns, billing records and a long list of saved searches, each on its own NetSuite screen. Scope changes went through ad-hoc orders, with no consistent approval step and no record of why a project's cost moved.
What we built
A React and TypeScript single-page application, built with Vite, served from one Suitelet and organised by domain.
- Billing. A queue of billing requests per project. Staff create, submit, approve or reject them, with comments and attached documents on each request. On approval, the app transforms the project's sales order into the invoice.
- Purchasing. Queues for purchase orders to cut, receipts to record, orders to ship and returns to receive. Each item is one action from the queue, with a detail page per transaction.
- Change orders. A change order record with add, replace and deduct lines moves through nine statuses. On submission the app works out the variance. A change that breaches both a percentage and a flat tolerance goes to budget review, and anything smaller goes straight to purchasing review. A confirmed change generates its downstream sales order and return authorization.
- Returns. The full sequence from return authorization through vendor return, receipt, credit memo and vendor credit, tracked on its own record.
- Reports. Project overview figures and a customer return-on-investment report, built on shared filter and table components.
- Administration. NetSuite roles map to app roles, app roles bundle permissions, and a permission matrix decides what each module shows and allows. An administrator edits all of it in custom records.
What it does now
The application is live in production and in active development. The older NetSuite screens for the same tasks now send users to the app. The app works on the same custom records, so history carried over without a migration. The change order data model predates the app, and we reused it unchanged.
Under the hood
- Roles, app roles and the permission matrix are custom records, and the server checks them on every action.
- Reads go through SuiteQL. Writes are explicit server actions that check the caller's permission first.
- A change goes to budget review only when it breaches both thresholds.
- A Playwright suite drives the deployed app in a NetSuite sandbox.
- Development runs on a sandbox branch and a production branch. Each developer has a personal build slot, and a production deploy requires a typed confirmation.
- The Suitelet, scripts, records and front-end bundle ship as one SDF project.