import React from 'react'; import { formatEuro, type Acquisition } from '../../types'; /* * Die Liste der gefundenen Projekte. Genau die Spalten, die beim Durchgehen * zaehlen - alles Weitere steht im Formular und in der Opportunity. * * Der Titel ist ein Link, sobald einer hinterlegt ist. Ohne Link bleibt er * einfacher Text: ein Link, der nirgends hinfuehrt, ist schlimmer als keiner. */ export type AcquisitionSortKey = 'found_at' | 'sender' | 'provider' | 'channel' | 'title' | 'consultant'; export interface AcquisitionSortConfig { key: AcquisitionSortKey; direction: 'asc' | 'desc'; } interface AcquisitionTableProps { entries: Acquisition[]; sortConfig: AcquisitionSortConfig; onSort: (key: AcquisitionSortKey) => void; editingId: number | null; busyId: number | null; onEdit: (entry: Acquisition) => void; onDelete: (entry: Acquisition) => void; onConvert: (entry: Acquisition) => void; } const COLUMNS: { key: AcquisitionSortKey | null; label: string; className?: string }[] = [ { key: 'found_at', label: 'Datum' }, { key: 'sender', label: 'Von' }, { key: 'provider', label: 'Anbieter' }, { key: 'channel', label: 'Kanal' }, { key: 'title', label: 'Projekttitel' }, { key: 'consultant', label: 'Berater' }, { key: null, label: 'Opportunity' }, { key: null, label: '', className: 'text-right' }, ]; /** Farbe je Kanal, damit sich die Liste beim Durchsehen sortiert anfuehlt. */ const CHANNEL_STYLES: Record = { Mail: 'bg-sky-50 text-sky-700 border-sky-200', Telefon: 'bg-amber-50 text-amber-700 border-amber-200', Portal: 'bg-violet-50 text-violet-700 border-violet-200', LinkedIn: 'bg-blue-50 text-blue-700 border-blue-200', XING: 'bg-teal-50 text-teal-700 border-teal-200', Empfehlung: 'bg-brand-50 text-brand-700 border-brand-200', Sonstiges: 'bg-ink-100 text-ink-600 border-ink-200', }; /** Kurzform des Funddatums plus Alter in Tagen als Titelhinweis. */ function formatFound(value: string): { label: string; title: string } { const date = new Date(`${value.slice(0, 10)}T00:00:00`); if (Number.isNaN(date.getTime())) return { label: '—', title: '' }; const days = Math.round((Date.now() - date.getTime()) / 86_400_000); const age = days <= 0 ? 'heute' : days === 1 ? 'vor 1 Tag' : `vor ${days} Tagen`; return { label: date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }), title: `Gefunden ${age}`, }; } /** * Nur absolute http- und https-Adressen werden zum Link. Das Backend legt * ohnehin nur solche ab; die Pruefung hier faengt Altbestand und Eintraege ab, * die auf anderem Weg in die Tabelle gekommen sind. "hays.de/projekt/1" wuerde * der Browser sonst als Pfad innerhalb des CRM auflegen und ins Leere fuehren. */ function safeLink(value: string | null): string | null { if (!value) return null; return /^https?:\/\//i.test(value) ? value : null; } /** * Kennzeichen fuer eine Bewerbung, die ins Archiv zurueckgefallen ist: sie war * schon eine Opportunity, und die ist weg. Ohne den Hinweis stuende sie * unerklaerlich im Archiv. */ function returnedNote(entry: Acquisition): string | null { if (entry.converted_at === null) return null; if (entry.opportunity_deleted_at) return 'Opportunity im Papierkorb'; return 'Opportunity gelöscht'; } /** Kurze Zusammenfassung der Angaben, die keine eigene Spalte haben. */ function extras(entry: Acquisition): string[] { const parts: string[] = []; if (entry.company) parts.push(entry.company); if (entry.fixed_price !== null) parts.push(`${formatEuro(entry.fixed_price)} Festpreis`); else if (entry.daily_rate !== null) parts.push(`${formatEuro(entry.daily_rate)}/Tag`); if (entry.duration_days !== null) parts.push(`${entry.duration_days} PT`); return parts; } const SortIcon: React.FC<{ active: boolean; direction: 'asc' | 'desc' }> = ({ active, direction }) => ( ); export const AcquisitionTable: React.FC = ({ entries, sortConfig, onSort, editingId, busyId, onEdit, onDelete, onConvert, }) => (
{COLUMNS.map((column) => ( ))} {entries.map((entry) => { const found = formatFound(entry.found_at); const link = safeLink(entry.link); const extraParts = extras(entry); const returned = returnedNote(entry); const isBusy = busyId === entry.id; return ( ); })}
{column.key ? ( ) : ( column.label )}
{found.label} {entry.sender || '—'} {entry.provider || '—'} {entry.channel ? ( {entry.channel} ) : ( — )} {link ? ( {entry.title} ↗ ) : ( {entry.title} )} {extraParts.length > 0 && (

{extraParts.join(' · ')}

)} {returned && ( {returned} )}
{entry.consultant || '—'}
);