import React, { useCallback, useEffect, useState } from 'react'; import { ConfirmDialog } from '../ui/Modal'; import { UserAvatar } from '../ui/UserAvatar'; import { useToast } from '../Toast'; import { acquisitionsApi } from '../../lib/api'; import { formatEuro, type Acquisition } from '../../types'; import { ConsultantList } from './acquisitionUi'; /* * Papierkorb der Akquise. * * Loeschen war hier lange endgueltig: der Eintrag verschwand aus Eingang und * Archiv und stand nirgends mehr - ein Fehlklick kostete den Fund. Jetzt setzt * das Loeschen nur `deleted_at`, und diese Ansicht ist die Stelle, an der man * ihn zurueckholt. * * Bewusst eine eigene, schlanke Liste und nicht die Akquise mit einem Schalter - * dasselbe Muster wie beim Papierkorb der Opportunities: hier zaehlt, was weg * ist, wer es weggeworfen hat und wie lange es noch da liegt. Suche, Sortierung * und die Uebernahme in eine Opportunity waeren dafuer Beiwerk. */ interface AcquisitionTrashProps { /** Daten der App neu laden - die Zahlen am Menue haengen daran. */ reload: () => void; } /** MariaDB-DATETIME steht in UTC; ohne das angehaengte Z liest der Browser Ortszeit. */ function parseUtc(value: string | null | undefined): Date | null { if (!value) return null; const date = new Date(`${value.replace(' ', 'T')}Z`); return Number.isNaN(date.getTime()) ? null : date; } function formatDeleted( value: string | null | undefined, keepDays: number ): { label: string; remaining: string } { const date = parseUtc(value); if (!date) return { label: '—', remaining: '' }; const days = Math.floor((Date.now() - date.getTime()) / 86_400_000); const left = Math.max(0, keepDays - days); return { label: date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }), remaining: left === 0 ? 'wird beim nächsten Aufräumen gelöscht' : `noch ${left} Tage`, }; } /** Funddatum kurz - es entscheidet, wo der Eintrag nach dem Zurückholen steht. */ function formatFound(value: string): string { const date = new Date(`${value.slice(0, 10)}T00:00:00`); if (Number.isNaN(date.getTime())) return '—'; return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit' }); } /** Kurze Zusammenfassung der Angaben ohne eigene Spalte - wie in der Akquise. */ 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; } export const AcquisitionTrash: React.FC = ({ reload }) => { const { addToast } = useToast(); const [entries, setEntries] = useState([]); const [keepDays, setKeepDays] = useState(90); const [archiveAfter, setArchiveAfter] = useState(14); const [isLoading, setIsLoading] = useState(true); const [busyId, setBusyId] = useState(null); const [toPurge, setToPurge] = useState(null); const [isEmptyOpen, setIsEmptyOpen] = useState(false); const load = useCallback( async (silent = false) => { if (!silent) setIsLoading(true); try { const data = await acquisitionsApi.list('papierkorb'); setEntries(data.entries); setKeepDays(data.trash_delete_after_days); setArchiveAfter(data.archive_after_days); } catch (err) { addToast(err instanceof Error ? err.message : 'Papierkorb nicht abrufbar.', 'error'); } finally { setIsLoading(false); } }, [addToast] ); useEffect(() => { load(); }, [load]); const restore = async (entry: Acquisition) => { setBusyId(entry.id); try { await acquisitionsApi.restore(entry.id); /* * Wohin der Eintrag zurueckkommt, entscheidet sein Funddatum - deshalb * steht es in der Meldung. Sonst sucht man ihn im Eingang, obwohl er alt * genug fuer das Archiv ist. */ const age = Math.floor( (Date.now() - new Date(`${entry.found_at.slice(0, 10)}T00:00:00`).getTime()) / 86_400_000 ); const target = age > archiveAfter ? 'Archiv' : 'Eingang'; addToast(`„${entry.title}" steht wieder im ${target}.`, 'success'); await load(true); reload(); } catch (err) { addToast(err instanceof Error ? err.message : 'Wiederherstellen fehlgeschlagen.', 'error'); await load(true); } finally { setBusyId(null); } }; const purge = async () => { if (!toPurge) return; try { await acquisitionsApi.purge(toPurge.id); addToast('Eintrag endgültig gelöscht.', 'success'); await load(true); reload(); } catch (err) { addToast(err instanceof Error ? err.message : 'Löschen fehlgeschlagen.', 'error'); } finally { setToPurge(null); } }; const emptyTrash = async () => { setIsEmptyOpen(false); try { const result = await acquisitionsApi.emptyTrash(); addToast( result.purged === 1 ? '1 Eintrag endgültig gelöscht.' : `${result.purged} Einträge endgültig gelöscht.`, 'success' ); await load(true); reload(); } catch (err) { addToast(err instanceof Error ? err.message : 'Leeren fehlgeschlagen.', 'error'); } }; return (

{entries.length === 0 ? 'Der Papierkorb ist leer.' : `${entries.length} ${ entries.length === 1 ? 'gelöschter Eintrag' : 'gelöschte Einträge' }`}

Von Hand gelöschte Akquise-Einträge liegen hier {keepDays} Tage und lassen sich zurückholen. Danach werden sie automatisch entfernt. Wohin ein Eintrag zurückkommt, entscheidet sein Funddatum: in den Eingang, wenn er jünger als{' '} {archiveAfter} Tage ist, sonst ins Archiv.

In der Auswertung zählt ein gelöschter Eintrag nicht mit - ein Fehleintrag soll die Zahl der Bewerbungen nicht verfälschen.

{isLoading && entries.length === 0 ? (
) : entries.length === 0 ? (
Hier liegt nichts. Gelöschte Akquise-Einträge erscheinen an dieser Stelle.
) : (
{['Projekttitel', 'Anbieter', 'Berater', 'Gefunden', 'Gelöscht', 'Von', ''].map( (label, index) => ( ) )} {entries.map((entry) => { const deleted = formatDeleted(entry.deleted_at, keepDays); const extraParts = extras(entry); const isBusy = busyId === entry.id; return ( ); })}
{label}

{entry.title}

{extraParts.length > 0 && (

{extraParts.join(' · ')}

)}
{entry.provider || '—'} {formatFound(entry.found_at)}

{deleted.label}

{deleted.remaining}

{entry.deleted_by === null || entry.deleted_by === undefined ? ( — ) : (
{entry.deleted_by_name ?? 'unbekannt'}
)}
)} {toPurge && ( setToPurge(null)} /> )} {isEmptyOpen && ( setIsEmptyOpen(false)} /> )}
); };