Files
lynkeduppro-crm/src/components/dashboard/leads-data.ts
T
Mayur Shinde fd2e02086e 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>
2026-07-24 15:20:29 +05:30

231 lines
10 KiB
TypeScript

// ============================================================
// LynkedUp Pro — Leads mock data.
// A storm-restoration roofing pipeline: door-knocked leads in
// the Plano, TX hail zone. Each lead carries a rich detail
// record (contact, property, job, insurance, assignment) that
// powers the lead-detail popup. All client-side so the screen
// is fully interactive without a backend.
// ============================================================
export type LeadStatus = "new" | "contacted" | "appointed" | "closed";
export type LeadPriority = "high" | "medium" | "low";
export type Phone = { number: string; type: "Mobile" | "Home" | "Work"; primary?: boolean };
export type Email = { address: string; type?: string; primary?: boolean };
/** A site photo (or other file) stored via the media presign flow (§11.5).
* `contentRef` is the IIOS object key — render it through a signed download URL. */
export type Attachment = { id?: string; contentRef: string; mimeType: string; sizeBytes: number; filename: string };
export type Lead = {
id: string; // SAL-001 (display code)
refId?: string; // opaque server id (live mode); commands target this — falls back to `id`
/** Set once the lead has been pushed to the verification queue — blocks a duplicate send.
* Populated from the backend (e.g. LeadDTO.verificationId) when available. */
verificationId?: string;
initials: string;
name: string;
gradient: string;
priority: LeadPriority;
status: LeadStatus;
tag: string; // "Storm Zone"
updated: string; // "3d ago"
setter: string; // compact chip name
// storm banner
storm: { zone: string; date: string; detail: string };
// contact
phones: Phone[];
emails: Email[];
// site photos (uploaded via the media presign flow; shown in the detail popup)
attachments?: Attachment[];
// property
property: { address: string; city: string; state: string; zip: string; type: string };
// job details
job: {
source: string; leadType: string; workType: string; tradeType: string;
urgency: string; canvasser: string; notes: string;
};
// insurance
insurance: {
company: string; claimStatus: string; claimNumber: string;
policyNumber: string; adjusterName: string; adjusterPhone: string;
};
// assignment
assignment: {
assignedTo: string; priority: string; followUp: string;
createdBy: string; createdAt: string;
};
};
const G = {
orange: "linear-gradient(135deg,#fda913,#fd6d13)",
blue: "linear-gradient(135deg,#4f8cff,#2c5cff)",
purple: "linear-gradient(135deg,#b07bf2,#7b53e0)",
green: "linear-gradient(135deg,#33c98a,#1fa46c)",
cyan: "linear-gradient(135deg,#34c9d6,#1f9aa4)",
};
export const LEADS: Lead[] = [
{
id: "SAL-001", initials: "JM", name: "John Martinez", gradient: G.orange,
priority: "high", status: "contacted", tag: "Storm Zone", updated: "3d ago", setter: "Cody",
storm: { zone: "E Plano / Spring Creek Pkwy", date: "2026-04-28", detail: '2.5" hail (severe)' },
phones: [
{ number: "(469) 500-1000", type: "Mobile", primary: true },
{ number: "(214) 600-2000", type: "Home" },
],
emails: [{ address: "john.martinez@gmail.com", primary: true }],
property: { address: "4821 Spring Creek Pkwy", city: "Plano", state: "TX", zip: "75023", type: "Single Family" },
job: {
source: "Door Knock", leadType: "Insurance", workType: "Roof Replacement", tradeType: "Roofing",
urgency: "High", canvasser: "Cody Tatum",
notes: "Homeowner showed significant granule loss on south-facing slopes; agreed to inspection.",
},
insurance: {
company: "State Farm", claimStatus: "Filed", claimNumber: "CLM-2026-1000",
policyNumber: "POL-080000", adjusterName: "Marcus Powell", adjusterPhone: "(972) 700-3000",
},
assignment: {
assignedTo: "Jesus Gonzales", priority: "High", followUp: "Jun 4, 2026",
createdBy: "Cody Tatum", createdAt: "May 28, 2026",
},
},
{
id: "SAL-002", initials: "SK", name: "Sarah Kim", gradient: G.purple,
priority: "high", status: "appointed", tag: "Storm Zone", updated: "2d ago", setter: "Shelby",
storm: { zone: "E Plano / Custer Rd", date: "2026-04-28", detail: '2.5" hail (severe)' },
phones: [
{ number: "(972) 501-1037", type: "Mobile", primary: true },
{ number: "(214) 601-2044", type: "Home" },
],
emails: [{ address: "sarah.kim@outlook.com", primary: true }],
property: { address: "4905 Custer Rd", city: "Plano", state: "TX", zip: "75023", type: "Single Family" },
job: {
source: "Door Knock", leadType: "Insurance", workType: "Roof Replacement", tradeType: "Roofing",
urgency: "High", canvasser: "Hannah Reyes",
notes: "Visible mat exposure on rear elevation. Appointment set for adjuster meet.",
},
insurance: {
company: "Allstate", claimStatus: "Approved", claimNumber: "CLM-2026-1037",
policyNumber: "POL-081037", adjusterName: "Dana Whitfield", adjusterPhone: "(972) 700-3037",
},
assignment: {
assignedTo: "Hannah Reyes", priority: "High", followUp: "Jun 6, 2026",
createdBy: "Shelby Greer", createdAt: "May 29, 2026",
},
},
{
id: "SAL-003", initials: "RC", name: "Robert Chen", gradient: G.green,
priority: "high", status: "closed", tag: "Storm Zone", updated: "2d ago", setter: "Dalton",
storm: { zone: "E Plano / Independence Pkwy", date: "2026-04-28", detail: '2.5" hail (severe)' },
phones: [
{ number: "(469) 502-1074", type: "Mobile", primary: true },
],
emails: [{ address: "robert.chen@gmail.com", primary: true }],
property: { address: "5012 Independence Pkwy", city: "Plano", state: "TX", zip: "75023", type: "Single Family" },
job: {
source: "Door Knock", leadType: "Insurance", workType: "Roof Replacement", tradeType: "Roofing",
urgency: "High", canvasser: "Travis Boone",
notes: "Full replacement approved and installed. Final invoice cleared.",
},
insurance: {
company: "Farmers", claimStatus: "Paid", claimNumber: "CLM-2026-1074",
policyNumber: "POL-081074", adjusterName: "Leah Ortiz", adjusterPhone: "(972) 700-3074",
},
assignment: {
assignedTo: "Travis Boone", priority: "High", followUp: "—",
createdBy: "Dalton Pruitt", createdAt: "May 20, 2026",
},
},
{
id: "SAL-004", initials: "MG", name: "Maria Garcia", gradient: G.cyan,
priority: "high", status: "closed", tag: "Storm Zone", updated: "2d ago", setter: "Hannah",
storm: { zone: "E Plano / Alma Dr", date: "2026-04-28", detail: '2.5" hail (severe)' },
phones: [
{ number: "(972) 503-1111", type: "Mobile", primary: true },
],
emails: [{ address: "maria.garcia@gmail.com", primary: true }],
property: { address: "4720 Alma Dr", city: "Plano", state: "TX", zip: "75023", type: "Single Family" },
job: {
source: "Door Knock", leadType: "Insurance", workType: "Roof Replacement", tradeType: "Roofing",
urgency: "High", canvasser: "Shelby Greer",
notes: "Signed contract; build complete. Awaiting review request.",
},
insurance: {
company: "USAA", claimStatus: "Paid", claimNumber: "CLM-2026-1111",
policyNumber: "POL-081111", adjusterName: "Grant Mueller", adjusterPhone: "(972) 700-3111",
},
assignment: {
assignedTo: "Shelby Greer", priority: "High", followUp: "—",
createdBy: "Hannah Reyes", createdAt: "May 18, 2026",
},
},
{
id: "SAL-005", initials: "DT", name: "David Thompson", gradient: G.blue,
priority: "high", status: "appointed", tag: "Storm Zone", updated: "1d ago", setter: "Travis",
storm: { zone: "E Plano / Spring Creek Pkwy", date: "2026-04-28", detail: '2.5" hail (severe)' },
phones: [
{ number: "(469) 504-1148", type: "Mobile", primary: true },
{ number: "(214) 604-2148", type: "Work" },
],
emails: [{ address: "david.thompson@gmail.com", primary: true }],
property: { address: "5130 Spring Creek Pkwy", city: "Plano", state: "TX", zip: "75023", type: "Single Family" },
job: {
source: "Door Knock", leadType: "Insurance", workType: "Roof Replacement", tradeType: "Roofing",
urgency: "High", canvasser: "Dalton Pruitt",
notes: "Adjuster appointment confirmed for next week. Bring hail map + photos.",
},
insurance: {
company: "Liberty Mutual", claimStatus: "Filed", claimNumber: "CLM-2026-1148",
policyNumber: "POL-081148", adjusterName: "Priya Nair", adjusterPhone: "(972) 700-3148",
},
assignment: {
assignedTo: "Dalton Pruitt", priority: "High", followUp: "Jun 9, 2026",
createdBy: "Travis Boone", createdAt: "May 30, 2026",
},
},
];
// Header stat — the full book is larger than the loaded page.
export const TOTAL_LEADS = 35;
export const STATUS_META: Record<LeadStatus, { label: string; tone: string }> = {
new: { label: "New", tone: "blue" },
contacted: { label: "Contacted", tone: "orange" },
appointed: { label: "Appointed", tone: "purple" },
closed: { label: "Closed", tone: "green" },
};
export const PRIORITY_META: Record<LeadPriority, { label: string; tone: string }> = {
high: { label: "High", tone: "red" },
medium: { label: "Medium", tone: "orange" },
low: { label: "Low", tone: "muted" },
};
/* ---------------------------------------------------------- */
/* Reps + option lists — power the New Lead form */
/* ---------------------------------------------------------- */
export type Rep = { id: string; initials: string; name: string; email: string };
export const REPS: Rep[] = [
{ id: "LUP-1040", initials: "CT", name: "Cody Tatum", email: "cody.tatum@lynkeduppro.com" },
{ id: "LUP-1041", initials: "HR", name: "Hannah Reyes", email: "hannah.reyes@lynkeduppro.com" },
{ id: "LUP-1042", initials: "TB", name: "Travis Boone", email: "travis.boone@lynkeduppro.com" },
{ id: "LUP-1043", initials: "SG", name: "Shelby Greer", email: "shelby.greer@lynkeduppro.com" },
{ id: "LUP-1044", initials: "DP", name: "Dalton Pruitt", email: "dalton.pruitt@lynkeduppro.com" },
];
export const LEAD_SOURCES = ["Door Knock", "Referral", "Storm Chase", "Mailer / Postcard", "Sign Call", "Insurance Agent Referral", "Repeat Customer", "Social Media", "Other"];
export const LEAD_TYPES = ["Insurance", "Retail"];
export const WORK_TYPES = ["Roof Replacement", "Roof Repair", "Inspection", "Gutter Install"];
export const TRADE_TYPES = ["Roofing", "Gutters", "Siding", "Windows"];
export const PROPERTY_TYPES = ["Single Family", "Multi Family", "Commercial"];
export const CLAIM_STATUSES = ["Not Filed", "Filed", "Approved", "Paid", "Denied"];