# Plan 15 — Light CRM (from system review 1, item A11 / decision D8)

Status: **tasks 1–10 built 2026-09-29** (branch `Clinic`, one commit each); task 11 (kanban board) deferred (C5).

Build notes:
- Capture runs from a model observer (`LeadSourceObserver`) on contact messages, offer subscriptions and
  service requests, so the website, the API and any future form all create leads. The website service-request
  form no longer exists (removed in the site rebuild), so that source only has the imported old rows for now.
- Run `php artisan crm:import-leads` once per environment (idempotent; `--dry-run` to preview).
- Booking a lead: POST `crm/leads/{lead}/convert` finds the patient by phone or opens an account (random password,
  patient resets it), then the normal admin booking form carries `lead_id`; saving converts the lead.
- "My follow-ups" is the leads list pre-filtered (`?assignee=mine&follow_up=due`), plus a sidebar badge and a
  card on the admin clinic home.
- New notification events: `lead_received`, `lead_repeat`, `lead_assigned` (Settings > Notifications).
- Report: `admin/crm/report` (`crm.reports.view`), cohort = leads created in the period.

Goal: one place where the call-centre / reception team follows every person who contacted the centre
from the website until they either book or are closed as lost. Scope chosen in plan 14 (D8): leads from
contact messages, offer subscriptions and service requests; a status pipeline; an assignee; notes and
follow-up dates; convert a lead to a patient + booking. No marketing campaigns, no email/SMS blasts.

---

## 1. What exists today (checked in the code, 2026-09-29)

| Source | Table / screen | What it stores | Follow-up state today |
|---|---|---|---|
| Contact form | `site_contacts` → `admin/contact` | name, email, phone, subject, message | `is_read` / `read_at` only |
| Offer subscription | `site_offer_subscriptions` → `admin/offer-subscriptions` | name, phone, address, contact method, offer, `user_id`, payment (plan 14 task 37) | `status` new / contacted, `contacted_by`, `contacted_at` |
| Service request | `site_service_requests` → `admin/service-requests` | name, phone, email, address, service type, client type, organisation, message, images | **none** (no status column) |
| Patients | `users` + `clinic_patient_profiles` | registered patients | bookings / follow-ups already tracked |

Reusable pieces: `ClinicNotifier::adminsFor()` (admins by permission + branch), the admin notification bell,
`partials.server-table` + `crud-table-js` (server-side tables, AJAX actions, details modal),
`ClinicBranchScope` pattern, the admin booking create form (`admin.dashboard.clinic.bookings.create`),
`PermissionTitle::for()` for new permission titles.

---

## 2. Design

### 2.1 Data

**`crm_leads`**

| Column | Notes |
|---|---|
| `name`, `phone`, `email`, `address` | copied from the source; `phone_normalized` (digits only, indexed) for matching |
| `source` | `contact`, `offer_subscription`, `service_request`, `manual` |
| `source_type` / `source_id` | morph link to the original row (kept for the "original request" panel) |
| `user_id` | patient account when known (offer subscribers are already linked; matched by phone otherwise) |
| `branch_id` | nullable; set when known (manual entry, or the booked clinic's branch) |
| `offer_id`, `interest` | what the person asked about (offer, service type, contact subject) |
| `status` | `new` → `contacted` → `interested` → `converted` / `lost` |
| `lost_reason` | required when `lost` (price, no answer, went elsewhere, not interested, other) |
| `assigned_to` | admin id, nullable |
| `next_follow_up_at` | datetime, indexed — drives "due today / overdue" |
| `converted_booking_id`, `converted_at` | set by the convert flow |
| `created_by`, timestamps, soft deletes | |

**`crm_lead_activities`** (the timeline): `lead_id`, `admin_id` (null = system), `type` (`created`, `note`,
`call`, `whatsapp`, `status`, `assigned`, `follow_up`, `new_request`, `converted`), `body`, `meta` (json:
old/new status, call outcome, …), `created_at`.

### 2.2 Automatic capture

`LeadCaptureService::capture($sourceModel)` runs when a contact message, an offer subscription or a
service request is created (called from the existing store actions, next to the notifier):

1. Normalise the phone. If an **open** lead (not converted / lost) with the same phone exists, add a
   `new_request` activity to it (and refresh `interest`) instead of creating a second lead (C2).
2. Otherwise create a `new` lead, link the source row, try to match `user_id` by phone.
3. Notify the CRM team (`crm.leads.view` admins) through the existing bell.

A one-off command `php artisan crm:import-leads` builds leads from the existing rows (idempotent: skips
source rows already linked), so the CRM does not start empty.

### 2.3 Screens (admin, new sidebar group "CRM")

- **Leads list** — server-side table with status tabs and counts, filters (source, assignee, branch, offer,
  follow-up: overdue / today / this week / none), search by name / phone. Row actions: call, WhatsApp,
  open, take (assign to me).
- **Lead page** — contact card with call / WhatsApp buttons, the original request(s), status + assignee +
  next follow-up controls, add note / log a call (outcome: answered, no answer, busy, wrong number), and
  the timeline. If the person is a patient: link to the patient page and their bookings.
- **Convert** — "Book" button: finds the patient by phone or creates the account (name, phone, email),
  then opens the existing admin booking form pre-filled (patient, and doctor / clinic from the offer when
  it has one). Saving the booking marks the lead `converted` and links the booking.
- **My follow-ups** — the leads assigned to me that are due today or overdue; a sidebar badge with the
  count; a card on the admin home dashboard.
- **CRM report** — leads per source, conversion rate per source and per assignee, average time to first
  contact, lost reasons, for a period (reuses the reports filters and CSV export from plan 14).

### 2.4 Permissions

`crm.leads.view`, `crm.leads.create`, `crm.leads.edit`, `crm.leads.delete`, `crm.leads.assign`,
`crm.reports.view`. Everyone with `crm.leads.view` sees every lead (C1); the list filters by assignee
("mine" / unassigned) instead of hiding rows. Granted by default to Super Admin and Clinic Admin; receptionists get
view/create/edit.

---

## 3. Tracks and tasks (one commit per task)

### Track A — data and capture
1. Migration `crm_leads` + `crm_lead_activities`, permissions with titles, role grants; `Lead` and
   `LeadActivity` models (statuses, colours, lost reasons, scopes: `open()`, `due()`).
2. `LeadCaptureService` (dedupe by phone, activity, notification) hooked into contact, offer subscription
   and service request creation (website and API).
3. `crm:import-leads` command for the existing rows.

### Track B — screens
4. Leads list (server-side, tabs, filters, quick actions, take).
5. Lead page: details, source panel, status / assignee / follow-up, notes and call log, timeline.
6. Manual lead create / edit (walk-ins and phone calls).
7. Sync with the old screens (C3): the lead page links to the source row and the source screens show a
   "lead" link; marking a lead `contacted` also marks the offer subscription contacted / the contact
   message read.

### Track C — conversion and follow-up
8. Convert to patient + booking (find-or-create patient, pre-filled booking form, lead marked converted).
9. My follow-ups page, sidebar badge, admin home card; assignment notification.

### Track D — reporting
10. CRM report (sources, conversion, time to first contact, lost reasons, per assignee) with CSV.
11. Optional (C5): pipeline board (drag a card between statuses).

Each task gets feature tests (SQLite) in `tests/Feature/Crm/`; the list and lead page get a screenshot check.

---

## 4. Decisions to confirm

| # | Question | Recommended |
|---|---|---|
| C1 | Who sees which leads | **Chosen: everyone with CRM access sees all leads** (filter by assignee instead) |
| C2 | Same phone contacts again | **Chosen:** merge into the open lead as a new activity; a new lead only if the old one is converted / lost |
| C3 | Old screens (contacts, offer subscriptions, service requests) | **Chosen:** keep them; the lead status syncs "contacted" / "read" back to them and each links to its lead |
| C4 | Assigning new leads | **Chosen:** manual: a manager assigns or an agent presses "take"; no automatic round-robin |
| C5 | Pipeline board (kanban) | **Chosen:** not now; list with status tabs first, board as optional task 11 |
| C6 | Paid offer subscriptions (plan 14 task 37) | Default taken (not asked): lead goes straight to `interested` with a "paid" badge; `converted` still means a booking was made |
