Recruiteware
An office CRM for Thomas Grant and Apex Recruitment — a Laravel and React front on the existing RecruitWare backend, so consultants could search, book and check compliance without the old PHP shell.
2025
- Client
- Thomas Grant Recruitment, with Apex
- Type
- Office CRM on a legacy recruitment backend
- Stack
- Laravel 11, React, Inertia.js, Tailwind, MySQL
- Team
- Greyzip, with Fungai Motezu on the existing APIs









Beyond a WordPress skin
The work was three audiences sharing one backend, not a prettier template.
RecruitWare already held candidates, clients, bookings and payroll. Thomas Grant used it for warehouse and driving; Apex needed the same system for healthcare. The office screens were a PHP WordPress shell talking to a Notes database over session cookies.
Initial proposal
Host a new front on WordPress, follow an existing layout, and spend the first 2.5 months mostly on look and feel, with some front-end behaviour added on top of the current system.
Discovery
The product had three surfaces: a public website that had to write registrations into the CRM, an office system for consultants, and a candidate portal that did not exist yet. Apex wanted candidate records usable immediately. Public registration lived in WordPress form builders — some could only email — until Avada went back live with field names that matched the application pack. The office dashboard already had a spec: last-week versus this-week bookings by client.
Product response
I proposed Laravel so the website, office CRM and portal could share APIs without re-implementing validation, scheduled jobs and reports. The office build then ran in a fixed order: Chart.js dashboards against the existing booking spec, Notes login and session cookies, candidate tables swapping fixture data for live views, then menus and the candidate form. Fungai stayed on the APIs and the existing processing. I designed and built the SPA in React and Inertia.
Mapping the reality.
The office CRM, then the surfaces that still had to talk to it.
Built at Greyzip with Fungai Motezu on the RecruitWare APIs, July to September 2024. Dashboard charts first, then login, then tables and menus, then the candidate form. The live deployment is the office system. The candidate portal was in the brief; it is not in these screenshots.

Problem
Apex needed people on candidate records before the rest of the product existed. The office app had to log into the existing RecruitWare session, not mint a second identity. Wrong credentials returned the HTML login page. Right credentials returned a 404 — the login was succeeding, the redirect was not.
Solution
An external CRM authentication guard that follows the Notes login redirect, keeps the session cookie and folder, then lands in the SPA. Logout sits on the same rails. Consultants, compliance and admins then see different slices of the same records.
Before
- WordPress PHP templates on a shared session cookie
- Successful login looking like a 404
- No candidate login
After
- Laravel auth following the existing redirect and cookie
- Login and logout in the office app
- Role-gated navigation once the session is real
Problem
The previous candidates view was a PHP DataTable over a pipe-delimited Notes response. Columns came from per-user settings. The office nav was a delimited menu string: Live, Pending, Compliance Checks, Audit Checks, Leavers, Archive, then clients, contacts and jobs. A first live hook only showed Live Candidates no matter which view was requested.
Solution
Tables were designed against fixture data first, then swapped for the live payload once login worked. Views keep those statuses. Column toggles, search, advanced filters and a live-updates badge sit in the SPA. The nav parses the menu string — including catching missing prefixes and stray punctuation — instead of drawing a new information architecture.
Before
- PHP table assembled from cookie-authenticated curl
- One WordPress page per section, switching by reload
- Menu items as a delimited string the front had to interpret
After
- Live Candidates with search, columns and advanced filters
- Polling with a visible Live Updates state
- Same status split, driven by the existing menu ops
Problem
Profiles were still shaped for warehouse and driving. Healthcare needed compliance, attachments and auditing on the same candidate. The form could not be fully generated: labels and tabs had to be placed by hand, while data and action buttons would arrive on an API call, then submit back to the server.
Solution
A single candidate with inner navigation authored in the front: details, core documents, employment history, medical, convictions, payroll, jobs compliance, compliance check, expiry check, audit check, attachments and bookings. Edit and leaver stay on the record. Availability, preferred client and assigned jobs stay visible next to the compliance tabs.
Before
- Warehouse and driving fields as the default profile
- Compliance treated as a separate, easy-to-miss workflow
- Audit timestamp not part of the everyday record
After
- Healthcare and logistics fields on the same candidate
- Convictions, medical and expiry as first-class tabs
- Last audit shown on the details view
Problem
The office needed pipeline and booking volume on screen, not as an export. A dashboard spec already asked for last-week versus this-week day counts by client, bookings this week versus last, and a weekly difference. The backend could describe chart type, labels, data and order. The front did not yet render them.
Solution
Chart.js first, with dummy candidates, clients and bookings so the graphs could be proven before login worked. Then the same tables and charts — bar, line, gauge, pie — against live data. Finance and weekend costings stay siblings in the chrome, not mixed into this view.
Before
- Report generation buried in background processing
- A dashboard spec with no office front to render it
After
- Dashboard as the first office destination
- Tables and Chart.js views of the same booking split
- API-described chart type, labels, data and order
Problem
Clients, jobs and locations already existed in the backend. The menu ops listed them next to candidates: All Clients, Contacts, Jobs. The office shell did not give a consistent table for browsing them, so booking and availability still depended on knowing the old screens.
Solution
Locations as a live table: branch, reference, manager, town, with the same search, columns and polling as candidates. Clients and jobs sit in the same sidebar because that is how the backend already grouped them.
Before
- Locations only reachable through the legacy shell
- No shared table pattern across candidates and branches
After
- Locations list with live updates
- Clients and jobs as sibling modules
- Branch references such as TG0005 kept from the existing data
Problem
Apex and Thomas Grant already had registration and DBS forms on the public site. One plugin could only email details, so tests never hit the database. Apex’s bar was that candidates could complete it and the office could see the data. The CRM expected a long list of field names — identity, next of kin, employment history, medical, DBS, GDPR — that had to match exactly or someone would map them by hand.
Solution
Greyzip restored the Avada forms so entries stayed on the site and could be linked. Field names were set to the CRM placeholders. The pack was split into sections with a next step, because a single page could not hold it. Signature pages became documents the applicant ticks to confirm, not fields inside the form.
Before
- Multiple form builders on the same site
- Submissions that only arrived as email
- Field names that would have needed manual matching
After
- Avada live again, with CRM field names
- Stored entries as a fallback when the live write fails
- A multi-section pack, with policy documents ticked not signed in-form
Problem
Candidates had no access to the system. The intended loop was: register on the website, get a profile in the CRM, then use an app for profile, documents and booking info. Whether uploads happened at registration or after vetting was still being decided.
Solution
Laravel was chosen so a later portal could use the same APIs as the office SPA. The screens here are the office product that shipped. A candidate-facing portal remains the third audience, not a screen I am presenting as finished.
Before
- No candidate login
- Documents and availability handled inside the office only
After
- APIs shaped so a portal can read profile and bookings
- Office CRM shipped first
- Portal left as the next surface, not a fake module screenshot
04 / Deep dives
Three problems that were not a new CRM
The brief looked like a UI refresh on WordPress. The work that mattered was Chart.js against a booking spec, Notes login that looked like a 404, tables that only showed Live, and Avada field names that had to match the CRM.
From a WordPress shell to a front that can share APIs
Problem
Fungai’s first hosting thought was WordPress, because that is where the public site already lived. The office UI was already PHP templates on WordPress, curling recruitware.uk with a session cookie and parsing pipe-delimited Notes views. Re-skinning that would have left website, office and portal as three disconnected fronts.
Contribution
I asked whether we were working on the candidate portal or the office system, then proposed Laravel so the three surfaces could talk through APIs without rebuilding validation, scheduled tasks and reports. In July I stood up Chart.js on dummy candidates, clients and bookings against the existing dashboard spec, and shared a Laravel and React app so we could plan how those graphs would take the API. Fungai agreed to focus on the APIs while Greyzip took the office front.
Constraints
- Do not re-do background processing, validation, scheduled tasks or report generation.
- The existing database and session model had to keep working.
- The dashboard API would indicate chart type, labels, data and order — the front had to render whatever that described.
- First phase ran July to September 2024.
Keep the processing, replace the office shell
Laravel sits in front of the existing RecruitWare work. Payroll and reports stay where they already run.
Prove the graphs before the session works
Bar, gauge and line charts on fixture data meant we could argue about layout without waiting on login. The live dashboard is that spec: last-week versus this-week bookings by client.
Read the old calls before inventing new ones
Fungai sent the PHP candidates view, the curl helper, and the view process — type, URL, columns, query, return list, then per-user settings. The Live / Pending / Compliance split in the new UI is that view.
Office dashboard. Chart.js on Laravel and React — not a WordPress template over the old PHP table.
Outcome
Shipped as the office CRM on DigitalOcean. The live product is Laravel 11, React, Inertia and Tailwind against the existing backend — not Divi, MemberPress, or a WordPress plugin stack.
05 / Design to code
Where building protected the design
The existing shell taught the data model: cookie session, Notes views, settings-driven columns, a menu string. Laravel, Inertia and React were how that model became an office SPA. Chart.js came first because the dashboard spec already knew the questions.
Follow the Notes login, keep the cookie and folder
An external authentication guard with session sync keeps RecruitWare as the source of who someone is. A 404 after good credentials was the redirect, not a failed password.
Let the API describe the chart, then render it
Chart type, labels, data and order come from the backend. The dashboard spec already asked for last-week versus this-week bookings by client. Dummy data proved the graphs; live data replaced them.
Author tabs, bind fields
Candidate form labels and tabs are placed in the SPA because they have to match the current system. Data and buttons arrive on an API call and submit back. Healthcare fields can grow without a new information architecture.
- Laravel 11
- PHP
- React
- Inertia.js
- Tailwind CSS
- MySQL
- Chart.js
- DigitalOcean
06 / Outcome and status
Where the product stood
The office CRM shipped on DigitalOcean for Thomas Grant and Apex. Login is available on request. Public registration went back to Avada with CRM field names. A candidate portal was in the original three-part brief and is not claimed as a finished module here.
Evidence
- The live office app is on DigitalOcean. Access is behind a login that shares the RecruitWare session, available on request.
- Candidate lists keep Live / Pending / Compliance / Audit / Leavers / Archive from the existing menu ops, with search, column toggles and a visible live-updates state.
- The dashboard matches the original spec: day and week bookings by client, including last-week versus this-week difference, rendered in Chart.js.
What's next
Candidate self-service for profile, documents, availability and bookings, using the same APIs. Keep registration write-back on the Avada field names so intake never depends on an email plugin.
Reflection
The honest version of this project is a front on a working backend, built in order: graphs, then login, then tables, then the form. Mapping the Notes calls, the menu string and the Avada field names mattered more than the first hosting suggestion. I would still start there. I would not publish percentage load-time claims I cannot evidence, and I would not describe Divi or MemberPress as this stack.
