feat(opportunities): Phasen des Vermittlungsprozesses, Kanban zuerst, Zahlen im Menue

Drei Dinge:

- Die Phasen sind jetzt die des Vermittlungsprozesses: Neu, Beworben,
  Erstgespraech Berater, Kundenvorstellung, Interview Kunde, Zusage, Beauftragt,
  Absage. Offen ist alles bis einschliesslich Zusage - dort ist der Einsatz
  bestaetigt, die Beauftragung steht aber noch aus. migrateOpportunityStages()
  stellt bestehende Datenbanken um und setzt alle vorhandenen Opportunities auf
  "Neu": in welcher der neuen Phasen ein Projekt steckt, sagt der alte Trichter
  nicht (so mit dem Nutzer entschieden).

  Der Datenbankwert heisst 'Erstgespraech Berater' ohne Umlaut - Projektkonvention
  fuer Datenbankwerte. Angezeigt wird er ueber STAGE_LABELS; dazu gibt es
  STAGE_HINTS mit der Erlaeuterung je Phase als Titelhinweis an Auswahl, Badge
  und Spaltenkopf.

- Kanban ist die erste Ansicht der Opportunities. Die Pipeline ist die
  Arbeitsansicht, Kachel und Liste sind zum Nachschauen.

- Hinter "Archiv", beiden "Papierkorb"-Punkten und "Unpassend" steht die Anzahl,
  wie schon bei "Aus Mails". Die Zahlen kommen aus einem eigenen Endpunkt
  (/api/badges), weil sie alle ins Menue gehoeren und zusammen kommen sollen;
  Inhalte laedt weiter jede Ansicht selbst. Der Scanner liegt in einer eigenen
  Datenbank und kann fehlen - sein Zaehler ist dann null und die Leiste zeigt
  dort einfach keine Zahl. Der Aufruf haengt bewusst hinter den Daten und nicht
  im Promise.all: faellt er aus, bleibt das Menue ohne Zahlen statt die Ansicht
  mit einem Fehler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-18 14:34:41 +02:00
co-authored by Claude Opus 5
parent 1ac0e2d2c7
commit 42639c65fa
17 changed files with 313 additions and 55 deletions
+27 -2
View File
@@ -18,7 +18,15 @@ import { ScannerPortals } from './components/scanner/ScannerPortals';
import { useToast } from './components/Toast';
import { NavigationProvider } from './lib/navigation';
import { useAuth } from './lib/auth';
import { connectionsApi, eventsApi, leadsApi, opportunitiesApi, usersApi } from './lib/api';
import {
badgesApi,
connectionsApi,
eventsApi,
leadsApi,
opportunitiesApi,
usersApi,
type SidebarBadges,
} from './lib/api';
import {
OPEN_STAGES,
formatEuro,
@@ -71,6 +79,7 @@ const App: React.FC = () => {
const [users, setUsers] = useState<AppUser[]>([]);
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [opportunities, setOpportunities] = useState<Opportunity[]>([]);
const [badges, setBadges] = useState<SidebarBadges | null>(null);
const [isDataLoading, setIsDataLoading] = useState(false);
/**
@@ -104,6 +113,16 @@ const App: React.FC = () => {
setEvents(eventRows);
setUsers(userRows);
setOpportunities(opportunityRows);
/*
* Die Zahlen der Seitenleiste haengen bewusst hinter den Daten und nicht
* im Promise.all: sie sind Beiwerk. Faellt der Aufruf aus, bleibt das
* Menue ohne Zahlen statt die ganze Ansicht mit einem Fehler.
*/
badgesApi
.get()
.then(setBadges)
.catch(() => setBadges(null));
} catch (err) {
addToast(err instanceof Error ? err.message : 'Daten konnten nicht geladen werden.', 'error');
} finally {
@@ -180,7 +199,13 @@ const App: React.FC = () => {
onViewChange={goTo}
isOpen={isSidebarOpen}
onToggle={() => setIsSidebarOpen((open) => !open)}
mailPoolCount={mailPoolLeads.length}
counts={{
'Aus Mails': mailPoolLeads.length,
Archiv: badges?.acquisition_archive,
'Opportunity-Papierkorb': badges?.opportunity_trash,
Papierkorb: badges?.mail_trash,
Unpassend: badges?.scanner_rejected,
}}
/>
<main className="flex min-w-0 flex-1 flex-col overflow-hidden">