import React, { useState } from 'react'; import { UserAvatar } from '../ui/UserAvatar'; import { STAGE_COLORS, StageBadge, formatDate, formatDays, relativeDays } from './opportunityUi'; import { OPPORTUNITY_STAGES, contactFullName, formatEuro, opportunityVolume, type Opportunity, type OpportunityStage, } from '../../types'; interface OpportunityPipelineProps { opportunities: Opportunity[]; onEdit: (opportunity: Opportunity) => void; /** Verschiebt eine Karte in eine andere Phase. */ onStageChange: (opportunity: Opportunity, stage: OpportunityStage) => void; } /** * Pipeline nach Phasen. Karten lassen sich per Drag & Drop in eine andere Spalte * ziehen; wer nicht ziehen mag, nutzt das Auswahlfeld auf der Karte. */ export const OpportunityPipeline: React.FC = ({ opportunities, onEdit, onStageChange, }) => { const [draggedId, setDraggedId] = useState(null); const [hoverStage, setHoverStage] = useState(null); const byStage = (stage: OpportunityStage) => opportunities.filter((opportunity) => opportunity.stage === stage); const columnSum = (stage: OpportunityStage) => byStage(stage).reduce((sum, opportunity) => sum + (opportunityVolume(opportunity) ?? 0), 0); const handleDrop = (stage: OpportunityStage) => { setHoverStage(null); const dragged = opportunities.find((opportunity) => opportunity.id === draggedId); setDraggedId(null); if (dragged && dragged.stage !== stage) onStageChange(dragged, stage); }; return (
{OPPORTUNITY_STAGES.map((stage) => { const items = byStage(stage); const color = STAGE_COLORS[stage]; const isTarget = hoverStage === stage; return (
{ // Ohne preventDefault laesst der Browser kein Ablegen zu. event.preventDefault(); setHoverStage(stage); }} onDragLeave={() => setHoverStage((current) => (current === stage ? null : current))} onDrop={() => handleDrop(stage)} className={`flex w-[250px] shrink-0 flex-col rounded-2xl border transition ${ isTarget ? 'border-brand-400 bg-brand-50/60' : 'border-ink-200 bg-ink-50/60' }`} >
{stage} {items.length}
{formatEuro(columnSum(stage))}
{items.length === 0 ? (

Karte hierher ziehen

) : ( items.map((opportunity) => { const interaction = relativeDays(opportunity.last_interaction_at); return (
setDraggedId(opportunity.id)} onDragEnd={() => { setDraggedId(null); setHoverStage(null); }} onClick={() => onEdit(opportunity)} role="button" tabIndex={0} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onEdit(opportunity); } }} className={`cursor-grab rounded-xl border border-ink-200 bg-surface p-2.5 shadow-card transition hover:border-brand-300 active:cursor-grabbing ${ draggedId === opportunity.id ? 'opacity-40' : '' }`} >

{opportunity.title}

{opportunity.company || 'Ohne Firma'}

{formatEuro(opportunityVolume(opportunity))} {opportunity.probability}%

{formatDays(opportunity.duration_days)} {opportunity.start_date && ` ab ${formatDate(opportunity.start_date)}`}

{interaction && (

Kontakt {interaction}

)}
{opportunity.contacts.length === 0 ? 'kein Ansprechpartner' : opportunity.contacts.length === 1 ? contactFullName(opportunity.contacts[0]) : `${opportunity.contacts.length} Ansprechpartner`} {opportunity.owner_id !== null && ( )}
{/* Tastatur- und Touch-Weg ohne Drag & Drop */}
); }) )}
); })}
{/* Legende der Phasen, damit die Farben auch ausserhalb der Pipeline sitzen */}
{OPPORTUNITY_STAGES.map((stage) => ( ))}
); };