/WORK / Construction operations and contractor management
Contractor Management
One job, from the work order to the cost that remains.
Contractor Management is a construction operations application. A job starts with a client and a project, then a work order and a bill of quantities. Site cost arrives as materials, labour, vehicles, challans, and expenses. Client receipts come back in. Profit and loss is contract value minus those costs. Office staff and site staff use the same system, with a layout that still works on a phone.
- Category
- Construction operations and contractor management
- People
- Owners and admins
- Project managers and site managers
- Accountants
- Viewers with read access
- Contribution
- I built this product myself, frontend and backend. That covers the operations shell, the shared form contracts, labour and challan workflows, the reporting screens, and the API behind them.
- Status
- Live
The challenge
A contractor's cost does not live in one register. Materials are purchased and consumed. Labour is attendance plus a wage. Vehicles create road challans. Expenses and client payments move on different days. If those records disagree about money, the profit figure is only a guess.
The same company has people in an office and people on a site. Both need the record. They do not need the same navigation density, and they should not see modules their role cannot use.
The solution
The signed-in shell groups the work the way the job runs: commercial, procurement, site, finance, and admin. Dashboard metrics and charts come from the API. Clients, projects, work orders, and bill-of-quantities lines are maintained as records. Materials, suppliers, purchases, and consumption sit in procurement.
Labour is one hub: workers, categories, a month attendance calendar, and wage payments with a live net preview. Fleet covers vehicles, drivers, and challans that can be printed or shared. Expenses, client payments, documents, and site photos cover the rest of the file. Ten report types share one viewer with filters, paging, and Excel or PDF download when export is allowed.
- Clients, projects, work orders, and bill-of-quantities lines.
- Materials, suppliers, purchases, and consumption.
- Attendance on a month calendar, with create and edit in a drawer.
- Wage preview using the same paise helpers as the API.
- Vehicles, drivers, and road challans, including PDF download and Web Share.
- Expenses, client payments, documents, and site photos, with a camera input on the photo flow.
- A metadata-driven report viewer and a dashboard of server-calculated figures.
- Users, role permissions, company settings, and an audit log.
System architecture
The web app is a React and TypeScript single-page app on Vite. At startup it restores the session before routes render, so a refresh does not flash the login page. Redux holds the in-memory access token, the user, and a few UI preferences. The theme choice is the preference that is persisted. Server data lives in TanStack Query, with a short stale time and retries limited to one.
Axios sends the bearer token and cookies. A 401 starts a single refresh request and retries the original call once. The refresh token is an HttpOnly cookie. The access token is not written to local storage, and legacy token keys are deleted on load. Successful writes toast unless a call opts out. Screens handle read errors themselves.
Most create and edit forms use React Hook Form and Zod schemas from a shared package. That package also exports permission codes and integer-paise money helpers. The API validates with the same schemas and checks permissions with the same helper. Feature folders own their pages. A shared shell supplies the data table, the form page, and the page header.
How the clients, shared code, and API relate. Read the columns from left to right.
01 Browser
- React SPATypeScript and Vite. Shell, modules, and mobile actions.
- SessionRedux access token in memory. Theme preference persisted.
- Server cacheTanStack Query per module. Forms use React Hook Form.
02 Shared package
- ContractsZod schemas used by the forms and the API.
- Money and accessInteger paise helpers and permission codes.
03 API
- ExpressJWT, session version, and permission checks.
- Files and exportsUploads, challan PDFs, and report Excel or PDF.
04 Data
- MongoDBProjects, cost, labour, fleet, and payments.
Navigation hides modules the user cannot use, and some routes redirect to a forbidden page. The API still checks the permission. The drawing does not include private hosts or demo credentials.
Key engineering decisions
Refresh in a cookie, access token in memory
Engineering challenge
A long-lived token in local storage is convenient and easy to steal with any script that can read the page. A reload still has to restore the session.
Implementation approach
The access token stays in Redux. The refresh token is an HttpOnly cookie scoped to the auth path. Boot calls the refresh endpoint before protected routes render. Concurrent 401s share one refresh promise. Older token keys are removed and not rewritten.
Engineering rationale
The credential that can mint a new access token is not readable by page script. The interface can still survive a reload.
Trade offs
The first paint depends on the API. The in-memory access token is still visible to script while the tab is open. Login returns to the dashboard rather than to the URL the person originally asked for.
One permission list for the menu, the route, and the API
Engineering challenge
Owners, project managers, site managers, accountants, and viewers need different modules. A menu that drifts from the API will hide a button and still leave the request open, or the reverse.
Implementation approach
Permission codes and a helper that requires every listed permission live in the shared package. The sidebar filters on that helper. Protected routes send a failed check to a forbidden page. The API loads the role and uses the same helper.
Engineering rationale
Adding a module means adding a code, not a one-off boolean in three codebases.
Trade offs
The shell is a convenience. A person who can open the dashboard URL still needs the API to agree. Role defaults are written by hand beside the catalog.
Wage math in integer paise, shared with the form
Engineering challenge
A wage preview that uses floating currency will not match the payment the API stores. Officers notice that on the first payroll they check by hand.
Implementation approach
Money helpers in the shared package use integer paise. The labour payment form calls the same calculation the domain uses and shows the net before save. Profit and loss uses the same idea: contract value minus material, labour, and other cost.
Engineering rationale
The number on the form and the number in the ledger come from one function. The officer can check the wage without a second formula in the component.
Trade offs
The preview is a client render of the shared helper. The saved payment is still the API write, so the ledger stays the record.
Attendance as a month, and reports as metadata
Engineering challenge
Site attendance is a calendar problem. Reporting is a dozen similar tables. Building a bespoke page for each report, and a spreadsheet for each worker-day, does not stay consistent.
Implementation approach
The labour hub shows a month grid filtered by worker and project. Editing happens in a drawer and the query is keyed by worker and month. Reports use a catalog: the server describes columns and totals, and one viewer pages, prints, and downloads Excel or PDF when the export permission is present.
Engineering rationale
The attendance UI matches the way a site marks days. The report UI stays stable when a new report is added to the catalog.
Trade offs
The calendar loads the worker-month it is showing, and a day with more than one project keeps a single cell. The viewer is generic, so a report-specific layout would be a new decision, not a default.
Core capabilities
Clients, projects, and the bill of quantities
Staff keep client and project records, add bill-of-quantities lines, and open a project summary. Work orders can be listed and created.
Value. The commercial file exists before site cost starts arriving against it.
Complexity. Create forms validate with the shared schemas, so the browser and the API reject the same shape.
Materials and consumption
A materials hub links the catalog, purchases, and consumption, with suppliers maintained beside them.
Value. Purchases and site use are separate events, which is what a later cost report needs.
Complexity. Each write invalidates the module cache the dashboard and lists depend on.
Labour calendar and wages
Attendance is marked on a month grid. Wage payments show a live net calculated in paise before the officer saves.
Value. A site manager can mark days, and an accountant can see the wage that will be stored.
Complexity. The drawer writes attendance for a date. The preview function is the shared payment helper.
Challans, expenses, and photos
Road challans can be printed, downloaded as PDF, or shared from the browser. Expenses and client payments are their own records. Site photos accept a camera capture.
Value. Paper that used to leave the site can be filed against the job from the phone.
Complexity. The PDF bytes come from the API. Web Share is used when the browser offers it.
Dashboard and reports
The dashboard filters server-calculated figures and charts. The report viewer runs the operational reports, including profit and loss, with paging and export.
Value. A review meeting can start from the same numbers the records are producing.
Complexity. Export is its own permission. Column labels and totals come from the report payload, not from a hard-coded table per report.
Office and site layout
Wide screens use the full shell. Narrow screens keep a bottom nav for home, projects, challans, expenses, and photos, and a sticky action for new challans and expenses.
Value. The record can be updated where the work is happening, not only back at a desk.
Complexity. Theme follows light, dark, or the system preference. The bottom nav does not try to host every module.
Technology stack
Frontend
- React
- TypeScript
- Vite
- React Router
State
- Redux Toolkit for session and theme
- TanStack Query for server data
Forms and UI
- React Hook Form
- Zod
- MUI
- Emotion
- Recharts
- DM Sans
Data handling
- Axios
- Shared paise helpers
- decimal.js
- Blob download
Backend
- Express
- MongoDB
- JWT
- PDF and Excel generation on the API
Testing and delivery
- Vitest
- Testing Library
- Playwright
- Oxlint
- Docker and nginx packaging
- GitHub Actions
Engineering impact
Domain rules that must match, especially money, permissions, and form shape, live in one package imported by the screen and the API. That is what keeps a wage preview from becoming a second payroll.
Operational pages share a list and form shell, so a new record type does not invent a new page anatomy.
Site workflows have a real small-screen path for the actions people repeat away from a desk, without collapsing the office navigation into the same five icons.
Reporting stays one viewer. Adding a report is catalog and API work, which is a smaller change than a new screen with its own filters and export button.
Workflows and modules
These drawings describe the job structure. They are not captures of a contractor's projects, documents, or payments.
Commercial
- Clients
- Projects
- Work orders
- Bill of quantities
Site
- Materials
- Labour
- Fleet
- Challans
- Photos
Finance
- Expenses
- Client payments
- Dashboard
- Reports
Admin
- Users
- Roles
- Settings
- Audit log
- 01Open the job
Create the client, the project, the work order, and the bill of quantities.
- 02Record the site
Add materials, mark attendance, and file challans, expenses, and photos.
- 03Take receipt
Record what the client paid against the job.
- 04Read the cost
The dashboard and the profit-and-loss report use those same records.
Engineering takeaways
- I put shared money and shared validation in one package both sides import.
- I design the site path as a real layout, not as a squeezed office screen.
- I keep authorization in the API even when the navigation already hides the module.