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:
Mayur Shinde
2026-07-24 15:20:29 +05:30
parent f380c7d465
commit fd2e02086e
10 changed files with 2539 additions and 171 deletions
+30
View File
@@ -1278,8 +1278,23 @@
.dash-root .nl-photos-s { font-size: 11px; }
.dash-root .nl-photo-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.dash-root .nl-photo-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; padding: 5px 10px; border-radius: 99px; background: color-mix(in srgb, var(--green) 15%, transparent); color: var(--green); }
.dash-root .nl-photo-chip-x { display: inline-flex; align-items: center; justify-content: center; margin-left: 2px; padding: 0; border: none; background: none; color: inherit; opacity: 0.65; cursor: pointer; }
.dash-root .nl-photo-chip-x:hover { opacity: 1; }
.dash-root .ds-select.is-placeholder { color: var(--faint, var(--muted)); }
/* Detail popup — property photo gallery */
.dash-root .ld-photo-block { margin-bottom: 14px; }
.dash-root .ld-photo-block .ld-section-head { margin-bottom: 8px; }
.dash-root .ld-photos { display: grid; grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); gap: 8px; }
.dash-root .ld-photo { position: relative; display: block; aspect-ratio: 1; border-radius: 10px; overflow: hidden; border: 1px solid var(--border-2); background: var(--panel-2); cursor: pointer; }
.dash-root .ld-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dash-root .ld-photo-ph { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--muted); }
/* Edit mode — photo tile with a remove badge */
.dash-root .nl-edit-photos { margin-bottom: 8px; }
.dash-root .ld-photo-edit { position: relative; }
.dash-root .ld-photo-x { position: absolute; top: 4px; right: 4px; display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border: none; border-radius: 99px; background: rgba(0,0,0,0.6); color: #fff; cursor: pointer; }
.dash-root .ld-photo-x:hover { background: var(--red, #e5484d); }
/* Canvasser search (Door Knock source) */
.dash-root .nl-canvasser { position: relative; }
.dash-root .nl-canvasser-menu { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 30; max-height: 220px; overflow-y: auto; border: 1px solid var(--border-2); border-radius: 12px; background: var(--panel); box-shadow: 0 12px 32px rgba(0,0,0,0.28); padding: 5px; }
@@ -1358,6 +1373,21 @@
.lv-menu-item svg { color: var(--muted, #8c8c8c); flex: 0 0 auto; }
.lv-menu-item:hover svg { color: var(--orange, #fda913); }
/* ---- change-assignee popup ---- */
.dash-root .lv-assign { display: flex; flex-direction: column; gap: 12px; }
.dash-root .lv-assign-search { display: flex; align-items: center; gap: 8px; padding: 9px 11px; border-radius: 11px; border: 1px solid var(--border); background: var(--panel-2); }
.dash-root .lv-assign-search svg { color: var(--muted); flex: 0 0 auto; }
.dash-root .lv-assign-search input { flex: 1; min-width: 0; border: 0; background: none; color: var(--text); font-family: inherit; font-size: 13px; outline: none; }
.dash-root .lv-assign-x { display: inline-flex; border: 0; background: none; color: var(--muted); cursor: pointer; padding: 0; }
.dash-root .lv-assign-x:hover { color: var(--text); }
.dash-root .lv-assign-list { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; margin: -4px; padding: 4px; }
.dash-root .lv-assign-opt { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border: 0; border-radius: 10px; background: none; color: var(--text-2); font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; text-align: left; }
.dash-root .lv-assign-opt:hover { background: var(--panel-3, #1b1b22); color: var(--text); }
.dash-root .lv-assign-opt.current { background: color-mix(in srgb, var(--orange) 12%, transparent); }
.dash-root .lv-assign-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-root .lv-assign-opt.current > svg { color: var(--orange); flex: 0 0 auto; }
.dash-root .lv-assign-empty { padding: 20px 10px; text-align: center; color: var(--muted); font-size: 12.5px; }
/* ---- verification detail popup ---- */
.dash-root .lv-detail { display: flex; flex-direction: column; gap: 16px; }
.dash-root .lv-d-identity { display: flex; align-items: center; gap: 14px; }