forked from Goutam/lynkeduppro-crm
feat(leads+verification): wire Leads & Lead Verification to be-crm data door
Integrates the Leads and Lead Verification screens with the live be-crm backend (crm.lead.* / crm.leadVerification.* / crm.media.*) behind a mode-agnostic data layer that still falls back to the local mock when the Shell isn't configured. Data layer (new) - src/lib/leads-api.ts — crm.lead.search/get/stats/create/update/ updateStatus/assign/sendForVerification + media (presign upload/download) - src/lib/verify-api.ts — crm.leadVerification.search/get/stats/verify/ markUnverified/assign/reassign/moveToPending Backend → FE wiring - Assignee / creator names: read the resolved DTO fields and, as a safety net, resolve member ids → real names against crm.team.member.search (be-crm currently echoes the raw principalId as the name). - Created By: read the createdBy object the backend returns (was blank). - Site photos: upload via crm.media.presignUpload → PUT, persist through crm.lead.attachment.add (per photo, after create), render in the detail popup via crm.media.presignDownload. - Duplicate guard: surface the backend's real error (detail / duplicate_lead) instead of the SDK's opaque "data command failed: 400". UX - Leads detail: Property Photos gallery + edit-mode photo add/remove. - Verification: "Change Assignee" now opens a searchable, scrollable popup instead of a long inline dropdown. - Removed the static "Storm Zone" tag from lead cards/detail; storm banner only renders when the lead carries storm data. Reference / follow-ups - leads.http, leads.postman_collection.json — be-crm data-door requests. - LEADS_BACKEND_CHANGES.md — required be-crm changes (name resolution, assignee carry-over on sendForVerification) verified against :4010. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
# Leads — Backend Changes Required (be-crm)
|
||||
|
||||
Follow-ups the **frontend now depends on**. The frontend side of each item below is
|
||||
already implemented and shipped on branch `integration-lead-and-verification-be`; these
|
||||
are the matching changes needed in **be-crm** (`crm.lead.*` / `crm.media.*`) for the
|
||||
features to work end-to-end.
|
||||
|
||||
Companion to `LEADS_BACKEND_REQUIREMENTS.md` (the full contract). Section refs (§) point there.
|
||||
|
||||
**How this was found:** integration testing the live data door at `http://localhost:4010`
|
||||
(trust-headers dev mode: `X-Tenant-ID: tnt_demo`, `X-Principal-ID: prn_owner`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Resolve assignee / creator IDs to member display names in `LeadDTO`
|
||||
|
||||
**Status:** IDs are stored correctly; **display names are not resolved.**
|
||||
|
||||
### What we observed
|
||||
`crm.lead.create` with `assignment.assigneeId` → the id persists, and `crm.lead.get`
|
||||
returns the assignee/creator — but the `name` is just the **raw principal id echoed back**,
|
||||
and the dedicated `*Name` fields come back empty:
|
||||
|
||||
```jsonc
|
||||
// crm.lead.get response (relevant fields)
|
||||
"assignedToId": "prn_owner",
|
||||
"assignedToName": "", // ← empty, not resolved
|
||||
"assignee": { "id": "prn_owner", "name": "prn_owner", "initials": "P" }, // name = the id
|
||||
"createdById": "prn_owner",
|
||||
"createdByName": "", // ← empty, not resolved
|
||||
"createdBy": { "id": "prn_owner", "name": "prn_owner", "initials": "P" } // name = the id
|
||||
```
|
||||
|
||||
The contract (§6.2) says the detail projection must carry **"resolved assignee / canvasser /
|
||||
creator display names."** Today they are unresolved, so the detail popup's **Assigned To** and
|
||||
**Created By** show a raw id (or nothing) instead of a person's name.
|
||||
|
||||
### Required change
|
||||
In the `crm.lead.get` (and any `LeadDTO`-returning command) projection, **join the member
|
||||
FKs to the `members` table** and populate the real `displayName`:
|
||||
|
||||
- `assigned_to_id` → `assignedToName` **and** `assignee.name` = member `displayName`
|
||||
- `created_by_id` → `createdByName` **and** `createdBy.name` = member `displayName`
|
||||
- `canvasser_id` → `canvasserName` (same treatment, §5 note 5)
|
||||
|
||||
Fall back to the id only when the member can't be resolved. Note the dev tenant `tnt_demo`
|
||||
returns **0 members** from `crm.team.member.search` — seed members there so assignment can be
|
||||
exercised (§12 note 5 / "[Seed/link required]").
|
||||
|
||||
### Frontend interim (already shipped — remove once backend resolves names)
|
||||
`src/lib/leads-api.ts` now resolves the returned id against the local reps list
|
||||
(`crm.team.member.search`, principalId → name) as a safety net, and reads the `createdBy`
|
||||
**object** (previously the reader only looked at the never-populated `createdByName`, so
|
||||
"Created By" rendered blank). Backend resolution is still the correct long-term source of truth.
|
||||
|
||||
---
|
||||
|
||||
## 2. Site photos — NO backend change (uses existing `crm.lead.attachment.add`)
|
||||
|
||||
**Status:** No new backend work. The endpoints already exist; the frontend was wired to them.
|
||||
|
||||
### Decision: photos persist via `crm.lead.attachment.add` AFTER create (NOT in `crm.lead.create`)
|
||||
Photos are **not** sent in the create payload. The frontend creates the lead first, then calls the
|
||||
existing `crm.lead.attachment.add` command once per photo, keyed by the new lead's id (postman **#24**).
|
||||
|
||||
### Frontend flow (already shipped)
|
||||
1. User picks images in the full form → Property step.
|
||||
2. Each file is uploaded to storage via the shared Media module:
|
||||
`crm.media.presignUpload { mime, sizeBytes }` → `{ objectKey, uploadUrl }`, then browser `PUT`s the bytes (§11.5).
|
||||
3. On submit: `crm.lead.create` (WITHOUT photos) → then per photo:
|
||||
```jsonc
|
||||
{ "action": "crm.lead.attachment.add",
|
||||
"variables": { "id": "<newLeadId>",
|
||||
"attachment": { "contentRef": "<objectKey>", "mimeType": "image/jpeg", "sizeBytes": 12345, "filename": "roof.jpg" } } }
|
||||
```
|
||||
4. Detail popup reads `attachments[]` from `crm.lead.get` and renders each via `crm.media.presignDownload { contentRef }`.
|
||||
|
||||
### Backend expectations (should already hold — just confirm)
|
||||
- `crm.lead.attachment.add` inserts a `lead_attachments` row and enforces the 25 MB cap → `413 file_too_large` (§4.4).
|
||||
- `crm.lead.get` returns the stored photos as **`attachments: Attachment[]`**, each with at least
|
||||
`{ id, contentRef, mimeType, sizeBytes, filename }` (postman #3 / #24 already reference `j.attachments`).
|
||||
- `crm.media.presignUpload` / `crm.media.presignDownload` are registered for this app/tenant (shared Media module).
|
||||
|
||||
### Not verified live
|
||||
Could **not** confirm against `:4010` (dev backend intermittently down during testing). When stable, run:
|
||||
`crm.media.presignUpload` → `crm.lead.create` → `crm.lead.attachment.add` → `crm.lead.get` and confirm
|
||||
`attachments[]` comes back.
|
||||
|
||||
---
|
||||
|
||||
## Quick verification checklist (run against `:4010` when backend is up)
|
||||
|
||||
- [ ] Seed members in `tnt_demo`; `crm.team.member.search` returns > 0 items.
|
||||
- [ ] `crm.lead.create` with `assignment.assigneeId = <member principalId>` → `crm.lead.get`
|
||||
returns `assignedToName` = that member's real display name.
|
||||
- [ ] `crm.lead.get` returns `createdByName` = the creating caller's display name.
|
||||
- [ ] `crm.media.presignUpload` returns `{ objectKey, uploadUrl }`; PUT to `uploadUrl` succeeds.
|
||||
- [ ] `crm.lead.attachment.add { id, attachment }` → `crm.lead.get` returns matching `attachments[]`.
|
||||
- [ ] `crm.media.presignDownload { contentRef }` returns a working signed URL for a stored photo.
|
||||
Reference in New Issue
Block a user