/* 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(); }}>
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 });