In-house calling and placement CRM for a recruitment agency in Accra. Supabase + Google Apps Script + vanilla JS, with database-enforced record locking for concurrent agents.
# CEP Caller CRM
A calling and placement tracker built for **CareerEdu Personnel**, a recruitment agency in Accra that places nannies, drivers, security staff and household workers.
It replaced a shared Google Sheet that two or three agents were calling from at the same time. It ran in production on real candidate data.
**Live demo →** — populated with synthetic data. No real candidate ever appears in this repository.
> It will ask for your name on first load. That name is what stamps you as the owning agent, so open it in two browsers with two different names to watch the lock work.
---
## The problem
The agency collected applicants through Google Forms and called them from the response spreadsheet. That breaks down fast:
- **Two agents call the same person.** Both open the sheet, both start at the top. The candidate gets called twice within the hour and the agency looks disorganised. Worse, whoever saves second silently overwrites the first agent's note.
- **Calling from a spreadsheet on a phone is miserable.** Find the row, select the number, copy it, leave the app, open the dialer, paste. Every call. Agents were dropping back to paper.
- **Every campaign meant a new sheet.** Different form, different column names, no shared view of who had already been contacted.
- **Nobody could answer "where are we?"** Counting how many people were interested, invited, or actually placed meant filtering by hand.
## What it does
- **One tap to call, WhatsApp, or SMS** from the candidate card, with the message already written.
- **First agent to reach a candidate owns them.** Enforced in the database, not in the UI (see below).
- **Status is a set of tags, not a single column** — a candidate is `Interested` *and* `Location Sent` *and* `Shortlisted`. The funnel is queryable.
- **Live counts** across the whole pipeline: to call, no answer, interested, invited, placed.
- **Any intake sheet imports itself.** The importer reads the header row and works out which column is t …