import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import api from "../../../api/client"; import { CommTypeIconBadge, CommDirectionIcon } from "../../components/CommIcons"; import { REL_STATUS_LABELS, REL_STATUS_STYLES, OrderStatusChip, fmtDate } from "./shared"; import clientIcon from "../../../assets/customer-status/client.svg?raw"; import inactiveIcon from "../../../assets/customer-status/inactive.svg?raw"; import churnedIcon from "../../../assets/customer-status/churned.svg?raw"; import exclamationIcon from "../../../assets/customer-status/exclamation.svg?raw"; import wrenchIcon from "../../../assets/customer-status/wrench.svg?raw"; import orderIcon from "../../../assets/customer-status/order.svg?raw"; const LANGUAGE_LABELS = { af:"Afrikaans",sq:"Albanian",am:"Amharic",ar:"Arabic",hy:"Armenian",az:"Azerbaijani", eu:"Basque",be:"Belarusian",bn:"Bengali",bs:"Bosnian",bg:"Bulgarian",ca:"Catalan", zh:"Chinese",hr:"Croatian",cs:"Czech",da:"Danish",nl:"Dutch",en:"English", et:"Estonian",fi:"Finnish",fr:"French",ka:"Georgian",de:"German",el:"Greek", gu:"Gujarati",he:"Hebrew",hi:"Hindi",hu:"Hungarian",id:"Indonesian",it:"Italian", ja:"Japanese",kn:"Kannada",kk:"Kazakh",ko:"Korean",lv:"Latvian",lt:"Lithuanian", mk:"Macedonian",ms:"Malay",ml:"Malayalam",mt:"Maltese",mr:"Marathi",mn:"Mongolian", ne:"Nepali",no:"Norwegian",fa:"Persian",pl:"Polish",pt:"Portuguese",pa:"Punjabi", ro:"Romanian",ru:"Russian",sr:"Serbian",si:"Sinhala",sk:"Slovak",sl:"Slovenian", es:"Spanish",sw:"Swahili",sv:"Swedish",tl:"Tagalog",ta:"Tamil",te:"Telugu", th:"Thai",tr:"Turkish",uk:"Ukrainian",ur:"Urdu",uz:"Uzbek",vi:"Vietnamese", cy:"Welsh",yi:"Yiddish",zu:"Zulu", }; const CONTACT_TYPE_ICONS = { email:"๐Ÿ“ง", phone:"๐Ÿ“ž", whatsapp:"๐Ÿ’ฌ", other:"๐Ÿ”—" }; const COMM_TYPE_LABELS = { email:"e-mail", whatsapp:"whatsapp", call:"phonecall", sms:"sms", note:"note", in_person:"in person", }; const labelStyle = { fontSize: 11, fontWeight: 600, color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 4 }; function AddressField({ loc }) { if (!loc) return null; const parts = [loc.address, loc.city, loc.postal_code, loc.region, loc.country].filter(Boolean); if (!parts.length) return null; return (
Address
{parts.join(", ")}
); } function TagsField({ tags }) { if (!tags || !tags.length) return null; return (
Tags
{tags.map((tag) => ( {tag} ))}
); } // Verbose description per relationship status const REL_STATUS_DESCRIPTIONS = { lead: "This contact is a potential new lead. No active engagement yet.", prospect: "Actively engaged โ€” exploring possibilities before a formal order.", active: "Active customer with ongoing or recent commercial activity.", inactive: "No recent engagement. May need a follow-up to re-activate.", churned: "Customer has disengaged. Orders declined or no activity for a long period.", }; // Icon per relationship status (same logic as CustomerList resolveStatusIcon base cases) const REL_STATUS_ICONS = { lead: clientIcon, prospect: clientIcon, active: clientIcon, inactive: inactiveIcon, churned: churnedIcon, }; // Status badge: shows current status + gear icon to open inline change dropdown function RelStatusSelector({ customer, onUpdated, canEdit }) { const statuses = ["lead", "prospect", "active", "inactive", "churned"]; const current = customer.relationship_status || "lead"; const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, []); const handleClick = async (status) => { if (!canEdit || status === current) return; setOpen(false); try { const updated = await api.patch(`/crm/customers/${customer.id}/relationship-status`, { status }); onUpdated(updated); } catch (err) { alert(err.message); } }; const st = REL_STATUS_STYLES[current] || REL_STATUS_STYLES.lead; const icon = REL_STATUS_ICONS[current]; const description = REL_STATUS_DESCRIPTIONS[current] || ""; ensureShimmer(); const shimmerGradient = `linear-gradient(120deg, ${st.border}44 0%, ${st.border}cc 40%, ${st.border}ff 50%, ${st.border}cc 60%, ${st.border}44 100%)`; return (
{open && (
{statuses.map((s) => { const sst = REL_STATUS_STYLES[s] || {}; const isActive = s === current; return ( ); })}
)}
); } const STAT_CARD_STYLES = { issue: { bg: "var(--crm-issue-active-bg,rgba(224,53,53,0.12))", color: "var(--crm-issue-active-text)", border: "rgba(224,53,53,0.35)" }, support: { bg: "var(--crm-support-active-bg,rgba(247,103,7,0.12))", color: "var(--crm-support-active-text)", border: "rgba(247,103,7,0.35)" }, order: { bg: "var(--badge-blue-bg,rgba(59,130,246,0.12))", color: "var(--badge-blue-text)", border: "rgba(59,130,246,0.35)" }, }; const STAT_ICONS = { issue: exclamationIcon, support: wrenchIcon, order: orderIcon, }; const STAT_LABELS = { issue: "Open Issues", support: "Support Assists", order: "Open Orders", }; // Shared shimmer keyframes injected once let _shimmerInjected = false; function ensureShimmer() { if (_shimmerInjected) return; _shimmerInjected = true; const style = document.createElement("style"); style.textContent = ` @keyframes crm-border-shimmer { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } .crm-shimmer-card { position: relative; border-radius: 10px; overflow: visible; } .crm-shimmer-card::before { content: ""; position: absolute; inset: -1.5px; border-radius: 11px; padding: 1.5px; background: var(--crm-shimmer-gradient); background-size: 200% 200%; animation: crm-border-shimmer 3s ease infinite; -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; z-index: 0; } `; document.head.appendChild(style); } function renderMaskedIconOv(icon, color, size = 16) { const svgMarkup = icon .replace(/<\?xml[\s\S]*?\?>/gi, "") .replace(//gi, "") .replace(//g, "") .replace( /]*)>/i, ``, ); return ( ); } // Stat card โ€” mirrors the status hero layout exactly so all cards share the same height function StatCard({ count, onClick, type }) { ensureShimmer(); const s = STAT_CARD_STYLES[type] || {}; const icon = STAT_ICONS[type]; const label = STAT_LABELS[type] || type; const shimmerGradient = `linear-gradient(120deg, ${s.border}44 0%, ${s.border}cc 40%, ${s.border}ff 50%, ${s.border}cc 60%, ${s.border}44 100%)`; return ( ); } // Modal to show full note text โ€” also doubles as quick-edit modal function NoteExpandModal({ note, noteIndex, onClose, canEdit, onSaveEdit, startEditing }) { const [editing, setEditing] = useState(!!startEditing); const [editText, setEditText] = useState(note.text); const [saving, setSaving] = useState(false); const handleSave = async () => { if (!editText.trim()) return; setSaving(true); try { await onSaveEdit(noteIndex, editText.trim()); onClose(); } catch (err) { alert(err.message); } finally { setSaving(false); } }; return (
e.stopPropagation()} > {editing ? ( <>