feat(akquise): geloeschte Opportunity gibt die Bewerbung ans Archiv zurueck

Wer eine Opportunity loescht, hat bisher auch die Bewerbung dahinter verloren:
aus der Akquise war sie mit der Uebernahme heraus, und die Opportunity gab es
nicht mehr. Der Eintrag stand nirgends.

Jetzt taucht er im Archiv auf - abgeleitet, nicht gespeichert: `converted_at`
bleibt stehen, sichtbar wird der Eintrag, solange seine Opportunity im
Papierkorb liegt oder endgueltig weg ist. Wird sie zurueckgeholt, verschwindet
der Eintrag von selbst wieder. Es gibt damit keinen Zustand, der nachgezogen
werden muesste und auseinanderlaufen kann.

Die Regel steht als IS_OPEN an einer Stelle und gilt fuer Liste, Zaehler und den
Aufraeumlauf: ein Rueckfaeller altert wie jeder andere Eintrag nach 90 Tagen
heraus, und seine Anzahl wird vorher in den Verlauf gesichert.

Ausserdem:
- Bearbeiten und erneutes Uebernehmen sind wieder erlaubt, sobald die
  Opportunity weg ist. Gesperrt bleibt der Eintrag nur, solange sie lebt - dort
  wird dann gepflegt.
- Der Eingang zeigt weiter nur, was nie uebernommen wurde.
- Die Zeile traegt ein Kennzeichen ("Opportunity im Papierkorb" bzw.
  "Opportunity geloescht"), sonst stuende sie unerklaerlich im Archiv.
- Im Dashboard zaehlt eine Wandlung nur mit lebender Opportunity. Wird sie
  geloescht, faellt die Bewerbung zurueck - dann soll die Quote nicht behaupten,
  aus ihr sei eine Opportunity geworden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-18 15:52:35 +02:00
co-authored by Claude Opus 5
parent 6a81258ca0
commit c380d05b42
5 changed files with 132 additions and 23 deletions
@@ -73,6 +73,17 @@ function safeLink(value: string | null): string | 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[] = [];
@@ -139,6 +150,7 @@ export const AcquisitionTable: React.FC<AcquisitionTableProps> = ({
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 (
@@ -183,6 +195,18 @@ export const AcquisitionTable: React.FC<AcquisitionTableProps> = ({
{extraParts.length > 0 && (
<p className="mt-0.5 truncate text-[11px] text-ink-400">{extraParts.join(' · ')}</p>
)}
{returned && (
<span
title={
entry.opportunity_deleted_at
? 'Diese Bewerbung war schon eine Opportunity. Die liegt im Papierkorb - wird sie dort zurückgeholt, verschwindet dieser Eintrag wieder.'
: 'Diese Bewerbung war schon eine Opportunity. Die ist endgültig gelöscht, deshalb steht die Bewerbung wieder hier.'
}
className="mt-1 inline-flex items-center gap-1 rounded-md border border-amber-200 bg-amber-50 px-2 py-0.5 text-[10px] font-extrabold uppercase tracking-wider text-amber-700"
>
{returned}
</span>
)}
</td>
<td className="px-4 py-3 text-sm text-ink-700">{entry.consultant || '—'}</td>
@@ -170,6 +170,11 @@ export const Acquisitions: React.FC<AcquisitionsProps> = ({ view = 'eingang', re
weiter bearbeiten und jederzeit in eine Opportunity übernehmen. Neue Projekte werden
unter <em>Akquise</em> eingetragen.
</p>
<p className="mt-1 text-xs text-ink-500">
Hier stehen auch Bewerbungen, deren <strong className="font-bold">Opportunity gelöscht</strong>{' '}
wurde - sonst wären sie nirgends mehr zu finden. Wird die Opportunity aus ihrem
Papierkorb zurückgeholt, verschwindet der Eintrag wieder von selbst.
</p>
</div>
) : (
<AcquisitionForm
+9 -1
View File
@@ -364,11 +364,19 @@ export interface Acquisition {
next_step: string | null;
notes: string | null;
tags: string[];
/** Gesetzt = uebernommen; der Eintrag verschwindet dann aus beiden Listen. */
/**
* Gesetzt = uebernommen. Der Eintrag verschwindet dann aus beiden Listen -
* es sei denn, die Opportunity ist inzwischen weg (`opportunity_gone`): dann
* faellt die Bewerbung ins Archiv zurueck.
*/
converted_at: string | null;
opportunity_id: number | null;
opportunity_title?: string | null;
opportunity_stage?: OpportunityStage | null;
/** Gesetzt = die Opportunity liegt im Papierkorb (und ist zurueckholbar). */
opportunity_deleted_at?: string | null;
/** Die Opportunity ist im Papierkorb oder endgueltig geloescht. */
opportunity_gone?: boolean;
created_by: number | null;
created_by_name?: string | null;
created_at?: string;