// ============================================================
// MOORE AI v13 — PIPELINE TAB
// Requires: shared/utils.js, shared/components.jsx loaded first
// ============================================================
const PipelineView = ({ crmData, filteredOpps, pipelines, pipelineView, setPipelineView, pipelineFilter, setPipelineFilter, setSelectedLead, updateOppStatus, searchQuery, setSearchQuery, syncOpportunities, dragItemRef, selectedPipelineId, setSelectedPipelineId }) => {
const { useMemo } = React;
const pipelineOptions = useMemo(() => {
const opts = [];
if (pipelines && pipelines.length > 0) {
pipelines.forEach(p => opts.push({ id: p.id, name: p.name }));
} else {
const seen = {};
filteredOpps.forEach(o => {
if (o.pipelineId && !seen[o.pipelineId]) {
seen[o.pipelineId] = true;
opts.push({ id: o.pipelineId, name: o.pipelineName || `Pipeline ${Object.keys(seen).length}` });
}
});
}
return opts;
}, [pipelines, filteredOpps]);
const handlePipelineChange = (id) => {
setSelectedPipelineId(id);
syncOpportunities(id);
};
const statusFilteredOpps = useMemo(() => {
if (pipelineFilter === 'all') return filteredOpps;
return filteredOpps.filter(o => o.status === pipelineFilter);
}, [filteredOpps, pipelineFilter]);
const stages = useMemo(() => {
const stageMap = {};
const stageOrder = [];
const oppsForStages = statusFilteredOpps.filter(o => o.status !== 'won' && o.status !== 'lost');
oppsForStages.forEach(o => {
const key = o.stageName || 'Prospect';
if (!stageMap[key]) {
stageMap[key] = { name: key, id: o.stageId || key, opps: [], total: 0 };
stageOrder.push(key);
}
stageMap[key].opps.push(o);
stageMap[key].total += Number(o.value) || 0;
});
return stageOrder.map(k => stageMap[k]);
}, [statusFilteredOpps]);
const stageColors = ['#3b82f6', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ec4899', '#f97316'];
const wonOpps = filteredOpps.filter(o => o.status === 'won');
const lostOpps = filteredOpps.filter(o => o.status === 'lost');
return (
{/* Toolbar */}
Pipeline
{pipelineOptions.length > 0 && (
)}
{['all', 'open', 'won', 'lost'].map(f => (
))}
{/* Stats row */}
{filteredOpps.length} total
{filteredOpps.filter(o=>o.status==='open').length} open
{wonOpps.length} won
{lostOpps.length} lost
{selectedPipelineId && pipelineOptions.length > 0 && (
— {pipelineOptions.find(p=>p.id===selectedPipelineId)?.name || 'Selected Pipeline'}
)}
Value: {fmt$(filteredOpps.reduce((a,o)=>a+Number(o.value||0),0))}
{/* Content */}
{pipelineView === 'kanban' ? (
{stages.map((stage, si) => (
{stage.opps.length} deals · {fmt$(stage.total)}
{stage.opps.map(opp => (
setSelectedLead(opp)}
className="bg-[#0d1424] border border-slate-800/70 rounded-xl p-3 cursor-pointer hover:border-slate-600/70 hover:bg-slate-900/60 transition-all group">
))}
{stage.opps.length === 0 &&
Empty
}
))}
{(pipelineFilter === 'all' || pipelineFilter === 'won') && (
{wonOpps.length} deals · {fmt$(wonOpps.reduce((a,o)=>a+Number(o.value||0),0))}
{wonOpps.length === 0
?
No won deals
: wonOpps.map(opp => (
setSelectedLead(opp)}
className="bg-emerald-950/30 border border-emerald-900/30 rounded-xl p-3 cursor-pointer hover:border-emerald-700/40 transition-all">
{opp.name}
{opp.contact}
{fmt$(opp.value)}
))}
)}
{(pipelineFilter === 'all' || pipelineFilter === 'lost') && (
{lostOpps.length === 0
?
No lost deals
: lostOpps.map(opp => (
setSelectedLead(opp)}
className="bg-red-950/20 border border-red-900/20 rounded-xl p-3 cursor-pointer hover:border-red-700/30 transition-all">
{opp.name}
{opp.contact}
{fmt$(opp.value)}
))}
)}
) : (
{['Deal Name', 'Contact', 'Stage', 'Value', 'Status', 'Source', ''].map((h, i) => (
| {h} |
))}
{statusFilteredOpps.map(opp => (
setSelectedLead(opp)}
className="border-b border-slate-800/30 data-row cursor-pointer transition-colors">
|
|
{opp.contact} |
{opp.stageName || '—'} |
{fmt$(opp.value)} |
|
{opp.source} |
|
))}
{statusFilteredOpps.length === 0 && (
{filteredOpps.length === 0 ? 'No deals in this pipeline — try Refresh' : 'No records match filter'}
)}
)}
);
};