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>
5.4 KiB
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:
// 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→assignedToNameandassignee.name= memberdisplayNamecreated_by_id→createdByNameandcreatedBy.name= memberdisplayNamecanvasser_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)
- User picks images in the full form → Property step.
- Each file is uploaded to storage via the shared Media module:
crm.media.presignUpload { mime, sizeBytes }→{ objectKey, uploadUrl }, then browserPUTs the bytes (§11.5). - On submit:
crm.lead.create(WITHOUT photos) → then per photo:{ "action": "crm.lead.attachment.add", "variables": { "id": "<newLeadId>", "attachment": { "contentRef": "<objectKey>", "mimeType": "image/jpeg", "sizeBytes": 12345, "filename": "roof.jpg" } } } - Detail popup reads
attachments[]fromcrm.lead.getand renders each viacrm.media.presignDownload { contentRef }.
Backend expectations (should already hold — just confirm)
crm.lead.attachment.addinserts alead_attachmentsrow and enforces the 25 MB cap →413 file_too_large(§4.4).crm.lead.getreturns the stored photos asattachments: Attachment[], each with at least{ id, contentRef, mimeType, sizeBytes, filename }(postman #3 / #24 already referencej.attachments).crm.media.presignUpload/crm.media.presignDownloadare 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.searchreturns > 0 items. crm.lead.createwithassignment.assigneeId = <member principalId>→crm.lead.getreturnsassignedToName= that member's real display name.crm.lead.getreturnscreatedByName= the creating caller's display name.crm.media.presignUploadreturns{ objectKey, uploadUrl }; PUT touploadUrlsucceeds.crm.lead.attachment.add { id, attachment }→crm.lead.getreturns matchingattachments[].crm.media.presignDownload { contentRef }returns a working signed URL for a stored photo.