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
+12 -8
View File
@@ -15,8 +15,12 @@ interface SidebarProps {
onViewChange: (view: ViewState) => void;
isOpen: boolean;
onToggle: () => void;
/** Anzahl der Eintraege in "Aus Mails" - als Zaehler am Unterpunkt. */
mailPoolCount?: number;
/**
* Zahlen an den Unterpunkten. Was auf eine Entscheidung wartet oder sich
* ansammelt, soll man sehen, ohne die Ansicht zu oeffnen. 0 zeigt nichts an -
* eine Null waere nur Rauschen.
*/
counts?: Partial<Record<ViewState, number | null>>;
}
interface MenuItem {
@@ -185,7 +189,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
onViewChange,
isOpen,
onToggle,
mailPoolCount = 0,
counts = {},
}) => {
const { user, logout } = useAuth();
const isMobile = useIsMobile();
@@ -208,7 +212,10 @@ export const Sidebar: React.FC<SidebarProps> = ({
setOpenGroups((open) => (open.includes(active.item.name) ? open : [...open, active.item.name]));
}, [currentView]);
const renderItem = (item: MenuItem, options: { indented?: boolean; badge?: number } = {}) => {
const renderItem = (
item: MenuItem,
options: { indented?: boolean; badge?: number | null } = {}
) => {
const isActive = currentView === item.name;
return (
<button
@@ -324,10 +331,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
{showLabels && isGroupOpen && (
<div className="mt-1 space-y-1">
{children.map((child) =>
renderItem(child, {
indented: true,
badge: child.name === 'Aus Mails' ? mailPoolCount : undefined,
})
renderItem(child, { indented: true, badge: counts[child.name] ?? undefined })
)}
</div>
)}