/* global React, IconStar, IconPlus, IconMore, IconChevR, IconCalendar */ // Kanban pipeline — una tarjeta por llamada de Calendly. Drag & drop, // etapas editables, scroll horizontal. Compartido por todos los pipelines. const { useState, useRef, useEffect } = React; const STAGE_COLOR_SWATCHES = [ "#8b5cf6", "#3b82f6", "#0e9f6e", "#f59e0b", "#059669", "#ef4444", "#14b8a6", "#ec4899", "#6366f1", "#94a3b8"]; // ---- Etiqueta de fecha/hora corta para la tarjeta ---------------- function shortWhen(date) { if (!(date instanceof Date)) return ""; const now = new Date(); const sameDay = (a, b) => a.toDateString() === b.toDateString(); const tom = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1); const hh = date.toLocaleTimeString("es-ES", { hour: "2-digit", minute: "2-digit", hour12: false }); if (sameDay(date, now)) return "Hoy " + hh; if (sameDay(date, tom)) return "Mañana " + hh; if (date < now) return date.toLocaleDateString("es-ES", { day: "numeric", month: "short" }); return date.toLocaleDateString("es-ES", { weekday: "short", day: "numeric" }) + " " + hh; } function StageDot({ color, onClick, editable }) { return ( ); } function SourceBadge({ source, size = "sm" }) { const SOURCES = window.REVOLVR_DATA.SOURCES; const s = SOURCES[source] || { label: source, color: "#94a3b8" }; return ( {s.label} ); } function KanCard({ c, onOpen, onDragStart, onDragEnd, isDragging, showCloser, justLanded }) { const { CLOSERS, CALL_STATUS } = window.REVOLVR_DATA; const closer = CLOSERS.find((x) => x.id === c.closerId); const st = CALL_STATUS[c.status] || {}; return (
onDragStart(e, c.id)} onDragEnd={onDragEnd} onClick={() => onOpen(c.id)}>
{c.name.split(" ").map((s) => s[0]).slice(0, 2).join("")}
{c.name}
{c.eventType}
{c.form.objetivo}
{c.stage === "ganada" && c.sale && }
{showCloser && closer && {closer.name.split(" ")[0]}} {shortWhen(c.scheduledAt)}
); } function paymentState(sale) { const pagado = sale.tipo === "total" ? sale.total : (sale.pagado || 0); const saldo = Math.max(0, sale.total - pagado); return { pagado, saldo, paid: saldo <= 0 }; } function SaleBadge({ sale }) { const { saldo, paid } = paymentState(sale); return (
{sale.programa} {"$" + (sale.total || 0).toLocaleString("en-US")} {paid ? "pagado" : "saldo $" + saldo.toLocaleString("en-US")}
); } function KanColumn({ stage, calls, onOpen, onDrop, dragId, setDragId, showCloser, justMovedId, hoverStage, setHoverStage, onRename, onRecolor, onDelete, onMove, isFirst, isLast, stageDragId, setStageDragId, stageHoverId, setStageHoverId, onReorderStages }) { const isOver = hoverStage === stage.id && dragId; const [menuOpen, setMenuOpen] = useState(false); const [colorPickerOpen, setColorPickerOpen] = useState(false); const [editingName, setEditingName] = useState(false); const [draftName, setDraftName] = useState(stage.label); const [overIndex, setOverIndex] = useState(null); const menuRef = useRef(null); const inputRef = useRef(null); useEffect(() => { if (!menuOpen && !colorPickerOpen) return; const onDown = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) { setMenuOpen(false); setColorPickerOpen(false); } }; document.addEventListener("mousedown", onDown); return () => document.removeEventListener("mousedown", onDown); }, [menuOpen, colorPickerOpen]); useEffect(() => { if (editingName && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [editingName]); const commitName = () => { const n = draftName.trim(); if (n && n !== stage.label) onRename(stage.id, n); setEditingName(false); }; return (
{ if (editingName) return; setStageDragId(stage.id); e.dataTransfer.effectAllowed = "move"; }} onDragEnd={() => { setStageDragId(null); setStageHoverId(null); }} onDragOver={(e) => { if (stageDragId && stageDragId !== stage.id) { e.preventDefault(); e.stopPropagation(); setStageHoverId(stage.id); } }} onDragLeave={(e) => { if (stageDragId && e.currentTarget === e.target) setStageHoverId(null); }} onDrop={(e) => { if (stageDragId && stageDragId !== stage.id) { e.preventDefault(); e.stopPropagation(); onReorderStages(stageDragId, stage.id); setStageDragId(null); setStageHoverId(null); } }}> { setColorPickerOpen((v) => !v); setMenuOpen(false); }}/> {editingName ? setDraftName(e.target.value)} onBlur={commitName} onKeyDown={(e) => { if (e.key === "Enter") commitName(); if (e.key === "Escape") { setDraftName(stage.label); setEditingName(false); } }}/> : setEditingName(true)} title="Doble clic para renombrar">{stage.label}} {calls.length} {(menuOpen || colorPickerOpen) &&
{colorPickerOpen ? <>
Color de etapa
{STAGE_COLOR_SWATCHES.map((cc) => { onRecolor(stage.id, cc); setColorPickerOpen(false); }}/>)}
: <>

}
}
{ e.preventDefault(); setHoverStage(stage.id); if (dragId && calls.length === 0) setOverIndex(0); }} onDragLeave={(e) => { if (e.currentTarget === e.target) { setHoverStage(null); setOverIndex(null); } }} onDrop={(e) => { e.preventDefault(); setHoverStage(null); if (dragId) { const beforeId = (overIndex != null && overIndex < calls.length) ? calls[overIndex].id : null; onDrop(dragId, stage.id, beforeId); } setOverIndex(null); setDragId(null); }}> {calls.map((c, i) =>
{ if (!dragId) return; e.preventDefault(); const r = e.currentTarget.getBoundingClientRect(); setOverIndex(e.clientY > r.top + r.height / 2 ? i + 1 : i); }}> setDragId(id)} onDragEnd={() => { setDragId(null); setHoverStage(null); setOverIndex(null); }} isDragging={dragId === c.id}/>
)} {calls.length === 0 &&
Sin llamadas en esta etapa
}
); } function NewStageTile({ onAdd }) { const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [color, setColor] = useState(STAGE_COLOR_SWATCHES[0]); const inputRef = useRef(null); useEffect(() => { if (open && inputRef.current) inputRef.current.focus(); }, [open]); const commit = () => { const n = name.trim(); if (!n) return; onAdd(n, color); setName(""); setColor(STAGE_COLOR_SWATCHES[0]); setOpen(false); }; if (!open) { return ( ); } return (
{ if (e.key === "Escape") setOpen(false); if (e.key === "Enter" && name.trim()) commit(); }}>
Nombre de la etapa
setName(e.target.value)} placeholder="Ej. Seguimiento" style={{ width: "100%", marginTop: 4 }}/>
Color
{STAGE_COLOR_SWATCHES.map((c) => setColor(c)}/>)}
); } function DeleteStageConfirm({ stage, stages, count, onCancel, onConfirm }) { const others = stages.filter((s) => s.id !== stage.id); const [moveTo, setMoveTo] = useState(others[0]?.id || ""); return (
e.stopPropagation()}>

Eliminar etapa "{stage.label}"

{count > 0 ? <>

Esta etapa tiene {count} llamada{count === 1 ? "" : "s"}. Muévelas a otra etapa antes de eliminar.

:

Esta etapa está vacía. Se eliminará permanentemente.

}
); } function PipelineView({ calls, stages, onOpen, onMove, filters, setFilters, showCloser, justMovedId, onAddStage, onRenameStage, onRecolorStage, onDeleteStage, onMoveStage, onReorderStages, onExport }) { const [dragId, setDragId] = useState(null); const [hoverStage, setHoverStage] = useState(null); const [stageDragId, setStageDragId] = useState(null); const [stageHoverId, setStageHoverId] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const scrollRef = useRef(null); const [scrollState, setScrollState] = useState({ left: false, right: false }); const SOURCES = window.REVOLVR_DATA.SOURCES; const updateScrollState = () => { const el = scrollRef.current; if (!el) return; setScrollState({ left: el.scrollLeft > 4, right: el.scrollLeft + el.clientWidth < el.scrollWidth - 4 }); }; useEffect(() => { updateScrollState(); const el = scrollRef.current; if (!el) return; el.addEventListener("scroll", updateScrollState); window.addEventListener("resize", updateScrollState); return () => { el.removeEventListener("scroll", updateScrollState); window.removeEventListener("resize", updateScrollState); }; }, [stages.length]); const scrollBy = (dx) => scrollRef.current?.scrollBy({ left: dx, behavior: "smooth" }); const filtered = calls.filter((c) => { if (filters.source && c.utm.source !== filters.source) return false; if (filters.dateRange && (filters.dateRange.from || filters.dateRange.to)) { const d = c.createdAt; if (filters.dateRange.from && d < filters.dateRange.from) return false; if (filters.dateRange.to) { const end = new Date(filters.dateRange.to); end.setHours(23, 59, 59, 999); if (d > end) return false; } } if (filters.q) { const q = filters.q.toLowerCase(); if (!`${c.name} ${c.email} ${c.form.industria} ${c.eventType}`.toLowerCase().includes(q)) return false; } return true; }); const byStage = {}; stages.forEach((s) => byStage[s.id] = []); filtered.forEach((c) => byStage[c.stage]?.push(c)); const handleConfirmDelete = (moveTo) => { onDeleteStage(confirmDelete.stage.id, moveTo); setConfirmDelete(null); }; return (
{PERIODS.map((p) => { const active = matchingPeriodId(filters.dateRange) === p.id; return setFilters({ ...filters, dateRange: active ? null : periodToRange(p.id) })}>{p.label}; })} setFilters({ ...filters, dateRange: r })}/>
{filtered.length} de {calls.length} llamadas
{stages.map((s, idx) => setConfirmDelete({ stage: stages.find((x) => x.id === id), count: byStage[id]?.length || 0 })} onMove={onMoveStage} isFirst={idx === 0} isLast={idx === stages.length - 1} stageDragId={stageDragId} setStageDragId={setStageDragId} stageHoverId={stageHoverId} setStageHoverId={setStageHoverId} onReorderStages={onReorderStages}/>)}
{confirmDelete && setConfirmDelete(null)} onConfirm={handleConfirmDelete}/>}
); } function Chip({ children, active, onClick }) { return ; } // ---- Period presets ---------------------------------------------- const PERIODS = [ { id: "7d", label: "7 días", days: 7 }, { id: "14d", label: "14 días", days: 14 }, { id: "30d", label: "30 días", days: 30 }, { id: "90d", label: "90 días", days: 90 }, ]; function periodToRange(id) { const today = new Date(); today.setHours(0, 0, 0, 0); const p = PERIODS.find((x) => x.id === id); if (!p) return null; return { from: new Date(today.getTime() - p.days * 86400000), to: today }; } function sameDay(a, b) { return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); } function matchingPeriodId(range) { if (!range || !range.from || !range.to) return null; for (const p of PERIODS) { const pr = periodToRange(p.id); if (sameDay(range.from, pr.from) && sameDay(range.to, pr.to)) return p.id; } return null; } // ---- Date range picker ------------------------------------------- function DateRangeFilter({ value, onChange }) { const [open, setOpen] = useState(false); const today = new Date(); today.setHours(0, 0, 0, 0); const [viewMonth, setViewMonth] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1)); const ref = useRef(null); useEffect(() => { if (!open) return; const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", onDown); return () => document.removeEventListener("mousedown", onDown); }, [open]); const fmt = (d) => d ? d.toLocaleDateString("es-ES", { day: "numeric", month: "short" }) : ""; const hasValue = value && (value.from || value.to); const label = hasValue ? (value.to ? `${fmt(value.from)} – ${fmt(value.to)}` : `Desde ${fmt(value.from)}`) : "Por fecha"; const daysInMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 0).getDate(); const firstDay = new Date(viewMonth.getFullYear(), viewMonth.getMonth(), 1).getDay(); const offset = (firstDay + 6) % 7; const monthName = viewMonth.toLocaleDateString("es-ES", { month: "long", year: "numeric" }); const cells = []; for (let i = 0; i < offset; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(viewMonth.getFullYear(), viewMonth.getMonth(), d)); const sd = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); const inRange = (d) => value && value.from && value.to && d > value.from && d < value.to; const pickDate = (d) => { if (!value || !value.from || (value.from && value.to)) onChange({ from: d, to: null }); else if (value.from && !value.to) { if (d < value.from) onChange({ from: d, to: value.from }); else if (sd(d, value.from)) onChange({ from: d, to: d }); else onChange({ from: value.from, to: d }); } }; return (
{open &&
{[{ label: "Hoy", days: 0 }, { label: "Últimos 7 días", days: 7 }, { label: "Últimos 30 días", days: 30 }].map((p) => )}
{monthName}
{["L", "M", "X", "J", "V", "S", "D"].map((d) => {d})}
{cells.map((d, i) => d ? : )}
}
); } Object.assign(window, { PipelineView, Chip, StageDot, DateRangeFilter, SourceBadge, shortWhen, PERIODS, paymentState });