From 93fe8cfd73db6f0a85b20906862ce8ca6d6d7b15 Mon Sep 17 00:00:00 2001
From: Sascha Wolf
Date: Wed, 9 Sep 2026 14:07:23 +0200
Subject: [PATCH] fix(opportunities): Berater als eigenes Feld statt nur in den
Notizen
Bei der Uebernahme aus der Akquise verschwand die wichtigste Angabe nach
dem Titel: fuer wen das Projekt laeuft. Die Opportunity hatte kein Feld
dafuer, also schrieb die Uebernahme den Namen in den Herkunftsvermerk der
Notizen - in Pipeline, Liste und Kacheln war er damit nicht zu sehen, und
suchen liess sich nur im Notiztext.
crm_opportunities.consultant traegt ihn jetzt selbst. Genau ein Name je
Opportunity: passt ein Fund zu drei Beratern, entstehen bei der
Uebernahme drei Opportunities. Sichtbar in Pipeline-Karte, Kachel,
Papierkorb und als sortierbare Spalte direkt hinter dem Titel; die Suche
fasst ihn mit ein, und im Dialog kommt die Auswahl aus derselben Liste
wie in der Akquise (Einstellungen -> Akquise), damit nicht "Nico" und
"Nico N." nebeneinander entstehen.
Bestehende Opportunities holt migrateOpportunityConsultant() beim Start
aus ihren Notizen nach - nur dort, wo die Spalte leer ist. Damit ist der
Lauf wiederholbar und ueberschreibt nichts, was seither von Hand gesetzt
wurde; ein Merker in crm_settings ist dafuer nicht noetig. Der Text in
den Notizen bleibt stehen: er ist der Herkunftsnachweis samt Anbieter,
Kanal und Link.
Der Namenszusatz im Titel entfaellt wieder. Er war gestern dazugekommen,
damit sich mehrere Opportunities aus einem Fund unterscheiden liessen -
mit einer eigenen Spalte ist er nur noch Rauschen.
Nebenbei: der Block, der won_at nachzieht, stand zweimal in schema.sql.
Idempotent und damit harmlos, aber eine Kopie - jetzt einmal.
Co-Authored-By: Claude Opus 5 (1M context)
---
docs/02-datenmodell.md | 10 ++++
docs/07-anleitung-benutzer.md | 37 ++++++++----
server/src/db.js | 37 ++++++++++++
server/src/routes/acquisitions.js | 32 +++++-----
server/src/routes/opportunities.js | 37 ++++++++++--
server/src/schema.sql | 20 +++++--
.../opportunities/Opportunities.tsx | 6 +-
.../opportunities/OpportunityGrid.tsx | 7 +++
.../opportunities/OpportunityList.tsx | 14 +++++
.../opportunities/OpportunityModal.tsx | 60 ++++++++++++++++++-
.../opportunities/OpportunityPipeline.tsx | 7 +++
.../opportunities/OpportunityTrash.tsx | 3 +
src/types.ts | 6 ++
13 files changed, 236 insertions(+), 40 deletions(-)
diff --git a/docs/02-datenmodell.md b/docs/02-datenmodell.md
index 97e4ffd..90a7b32 100644
--- a/docs/02-datenmodell.md
+++ b/docs/02-datenmodell.md
@@ -65,6 +65,7 @@ erDiagram
crm_opportunities {
int id PK
varchar title
+ varchar consultant "fuer welchen Berater"
enum stage "Neu .. Beauftragt | Absage"
tinyint probability "0-100"
decimal daily_rate
@@ -135,6 +136,15 @@ Acht Eigenheiten, die man kennen muss:
- **`avatar` ist eine Data-URL in der Datenbank.** Der Browser verkleinert jedes
Bild vor dem Upload auf 256 px (`src/lib/imageCompress.ts`), aus 2,9 MB werden
wenige Kilobyte.
+- **Der Berater steht an der Opportunity, nicht nur in ihren Notizen.** Bis zum
+ 09.09.2026 schrieb die Übernahme ihn in den Herkunftsvermerk, weil
+ `crm_opportunities` kein Feld dafür hatte - in Pipeline, Liste und Kacheln war
+ damit nicht zu sehen, für wen ein Projekt läuft. Jetzt trägt
+ `crm_opportunities.consultant` genau einen Namen; bei mehreren Beratern in der
+ Akquise entstehen mehrere Opportunities. Bestehende Zeilen füllt
+ `migrateOpportunityConsultant()` in `db.js` aus den Notizen nach - nur dort, wo
+ die Spalte leer ist, damit der Lauf wiederholbar bleibt und nichts
+ überschreibt.
- **Mehrere Berater stehen in einem Feld.** `crm_acquisitions.consultant`
nimmt eine kommagetrennte Liste auf (`VARCHAR(500)`), keine eigene
Zuordnungstabelle: die Namen sind reiner Text, die Auswahlliste in
diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md
index f66ac8a..b99b855 100644
--- a/docs/07-anleitung-benutzer.md
+++ b/docs/07-anleitung-benutzer.md
@@ -182,11 +182,10 @@ mehr im Auswahlfeld, es zeigt also immer nur, wer noch zu haben ist.
> **Was mehrere Berater bedeuten:** Beim Übernehmen entsteht **je Berater eine
> Opportunity** - jeder durchläuft seinen eigenen Vorgang, einer kann gewonnen
-> werden und ein anderer verloren. Ihre Titel bekommen den Namen in Klammern
-> angehängt, damit sie sich in Pipeline und Liste auseinanderhalten lassen; bei
-> einem einzigen Berater bleibt der Titel unverändert. Im Dashboard zählt der
-> Eintrag bei **jedem** der Berater - drei Berater sind drei versendete Profile,
-> also drei Bewerbungen.
+> werden und ein anderer verloren. Alle tragen denselben Titel und unterscheiden
+> sich durch die Spalte **Berater**, die in Pipeline, Liste und Kacheln steht.
+> Im Dashboard zählt der Eintrag bei **jedem** der Berater - drei Berater sind
+> drei versendete Profile, also drei Bewerbungen.
**Von** ist mit dem **angemeldeten Benutzer** vorbelegt - in der Regel trägt
jeder seine eigenen Funde ein. Trägt Markus etwas für Artur ein, wählt er Artur
@@ -302,14 +301,21 @@ im Archiv.
Der Knopf **Opportunity** legt sie sofort an: Phase *Erstgespräch Berater*, 10 % Wahrscheinlichkeit,
zuständig ist der angemeldete Benutzer. Mit übernommen werden Titel, Endkunde,
Beschreibung, Tagessatz, Laufzeit, Projektpreis, Zeitraum und nächster Schritt.
-Sind **mehrere Berater** hinterlegt, entsteht **je Berater eine Opportunity**
-mit denselben Projektdaten; ihr Titel trägt den Namen in Klammern. In den Notizen
-steht neben dem eigenen Berater auch, mit wem er sich das Projekt teilt
+Der **Berater** wandert in das gleichnamige Feld der Opportunity und steht damit
+in Pipeline, Liste und Kacheln - für wen ein Projekt läuft, ist neben dem Titel
+die wichtigste Angabe. Sind **mehrere Berater** hinterlegt, entsteht **je Berater
+eine Opportunity** mit denselben Projektdaten, jede mit ihrem eigenen Namen im
+Feld. In den Notizen steht zusätzlich, mit wem er sich das Projekt teilt
(*Auch beworben*).
-Anbieter, Von, Kanal, Berater und Link haben in der Opportunity kein Feld - sie
-landen als Herkunftsvermerk in den internen Notizen, damit nachvollziehbar
-bleibt, woher das Projekt kam.
+Anbieter, Von, Kanal und Link haben in der Opportunity kein Feld - sie landen als
+Herkunftsvermerk in den internen Notizen, damit nachvollziehbar bleibt, woher das
+Projekt kam.
+
+> **Opportunities von vor dem 09.09.2026:** Bis dahin hatte die Opportunity kein
+> Feld für den Berater, der Name stand nur im Herkunftsvermerk. Beim ersten Start
+> nach der Umstellung holt das CRM ihn von dort in das Feld - von Hand ist nichts
+> zu tun.
Ansprechpartner hängt man danach in der Opportunity dran; dort geht das auch für
Kontakte, die es noch nicht gibt.
@@ -375,6 +381,15 @@ Vorschau im Formular schreibt dazu, welche Grundlage gerade zählt
Tagessatzes den Projektpreis. Tagessatz und Laufzeit dürfen trotzdem gefüllt
bleiben, etwa zur eigenen Kalkulation.
+Das Feld **Berater** sagt, für wen das Projekt läuft. Die Auswahl ist dieselbe
+Liste wie in der Akquise, gepflegt unter *Einstellungen → Akquise* - damit hier
+nicht „Nico" und dort „Nico N." entsteht. Der Name steht auf der Kachel, in der
+Listenspalte *Berater* (sortierbar), auf der Pipeline-Karte und im Papierkorb; die
+Suche findet ihn ebenfalls. Genau **ein** Berater je Opportunity: passt ein Fund
+der Akquise zu dreien, entstehen bei der Übernahme drei Opportunities. Steht in
+einer alten Opportunity ein Name, den die Liste nicht mehr kennt, bleibt er
+wählbar und ist als *(nicht in der Liste)* gekennzeichnet.
+
Zu jeder Opportunity gehören beliebig viele **Ansprechpartner** aus den
Kontakten, jeder mit eigener Rolle („Entscheider", „Fachbereich", …). Fehlt
jemand noch im CRM, öffnet **Neuer Kontakt...** direkt neben der Auswahlliste
diff --git a/server/src/db.js b/server/src/db.js
index dcbc9f6..0e2e955 100644
--- a/server/src/db.js
+++ b/server/src/db.js
@@ -52,6 +52,7 @@ export async function initDatabase() {
await migrateOpportunityStages();
await migrateMailPool();
await widenAcquisitionConsultant();
+ await migrateOpportunityConsultant();
await seedAcquisitionOptions();
await dropProviderOptions();
await ensureAdminUser();
@@ -254,6 +255,42 @@ async function dropProviderOptions() {
* kurz ist, und ist damit bei jedem weiteren Start ein Blick in
* INFORMATION_SCHEMA und sonst nichts.
*/
+/*
+ * Berater aus den Notizen in die eigene Spalte holen - einmalig.
+ *
+ * Vor dem 09.09.2026 hatte die Opportunity kein Feld dafuer, und die Uebernahme
+ * aus der Akquise schrieb den Namen in die Notizen ("Berater: Nico"). Der Text
+ * bleibt dort stehen: er ist der Herkunftsnachweis samt Anbieter, Kanal und
+ * Link, und ihn herauszuschneiden waere ein Eingriff in gepflegte Notizen.
+ *
+ * Angefasst werden nur Zeilen, deren Spalte noch leer ist. Damit ist der Lauf
+ * gefahrlos wiederholbar und ueberschreibt nichts, was seither von Hand gesetzt
+ * wurde - deshalb braucht es hier auch keinen Merker in crm_settings.
+ */
+async function migrateOpportunityConsultant() {
+ const [rows] = await pool.query(
+ `SELECT id, notes FROM crm_opportunities
+ WHERE (consultant IS NULL OR consultant = '')
+ AND notes LIKE '%Berater: %'`
+ );
+ if (rows.length === 0) return;
+
+ let filled = 0;
+ for (const row of rows) {
+ // Die Zeile steht als eigene Zeile im Herkunftsblock; alles bis zum
+ // Zeilenende ist der Name.
+ const match = /^Berater: (.+)$/m.exec(String(row.notes ?? ''));
+ const name = match?.[1]?.trim().slice(0, 190);
+ if (!name) continue;
+ await pool.query('UPDATE crm_opportunities SET consultant = ? WHERE id = ?', [name, row.id]);
+ filled++;
+ }
+
+ if (filled > 0) {
+ console.log(`[Opportunities] Berater bei ${filled} Opportunity(s) aus den Notizen uebernommen.`);
+ }
+}
+
async function widenAcquisitionConsultant() {
const [rows] = await pool.query(
`SELECT CHARACTER_MAXIMUM_LENGTH AS length FROM INFORMATION_SCHEMA.COLUMNS
diff --git a/server/src/routes/acquisitions.js b/server/src/routes/acquisitions.js
index abbc459..ba9a787 100644
--- a/server/src/routes/acquisitions.js
+++ b/server/src/routes/acquisitions.js
@@ -1008,10 +1008,13 @@ router.post('/:id/opportunity', async (req, res, next) => {
/*
* Was in der Liste steht, aber in der Opportunity kein Feld hat (Anbieter,
- * Kanal, Absender, Link, Berater), wandert in die Notizen. Sonst wuesste
- * hinterher niemand mehr, woher das Projekt kam. In den Notizen steht der
- * Berater dieser Opportunity, nicht die ganze Liste - und dazu, mit wem er
- * sich das Projekt teilt.
+ * Kanal, Absender, Link), wandert in die Notizen. Sonst wuesste hinterher
+ * niemand mehr, woher das Projekt kam.
+ *
+ * Der Berater steht seit dem 09.09.2026 in einer eigenen Spalte und wird
+ * hier trotzdem noch genannt: die Notiz ist der Herkunftsnachweis und soll
+ * fuer sich lesbar bleiben. Dazu, mit wem er sich das Projekt teilt - das
+ * hat sonst keinen Platz.
*/
const buildNotes = (consultant) => {
const others = targets.filter((name) => name !== null && name !== consultant);
@@ -1037,25 +1040,24 @@ router.post('/:id/opportunity', async (req, res, next) => {
};
/*
- * Bei mehreren Beratern kommt der Name in den Titel: in Pipeline und Liste
- * standen sonst drei Zeilen mit identischem Text, und die Opportunity hat
- * kein eigenes Feld fuer den Berater. Bei einem einzigen Berater bleibt der
- * Titel unveraendert - das ist der Normalfall und braucht keinen Zusatz.
+ * Der Titel bleibt der Titel - auch bei drei Beratern. Bis der Berater eine
+ * eigene Spalte hatte, wurde sein Name angehaengt, damit sich die drei
+ * Opportunities in Pipeline und Liste unterscheiden liessen; jetzt steht er
+ * dort in einer eigenen Spalte und der Zusatz waere nur Rauschen.
*/
- const titleFor = (consultant) =>
- targets.length > 1 && consultant ? `${entry.title} (${consultant})`.slice(0, 190) : entry.title;
-
await conn.beginTransaction();
const opportunityIds = [];
for (const consultant of targets) {
const [created] = await conn.query(
`INSERT INTO crm_opportunities
- (title, company, description, stage, probability, daily_rate, duration_days,
- fixed_price, start_date, end_date, next_step, tags, notes, owner_id, created_by)
- VALUES (?, ?, ?, 'Erstgespraech Berater', 10, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ (title, company, consultant, description, stage, probability, daily_rate,
+ duration_days, fixed_price, start_date, end_date, next_step, tags, notes,
+ owner_id, created_by)
+ VALUES (?, ?, ?, ?, 'Erstgespraech Berater', 10, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
- titleFor(consultant),
+ entry.title,
entry.company,
+ consultant,
entry.description,
entry.daily_rate,
entry.duration_days,
diff --git a/server/src/routes/opportunities.js b/server/src/routes/opportunities.js
index 5c32dc2..345ad9f 100644
--- a/server/src/routes/opportunities.js
+++ b/server/src/routes/opportunities.js
@@ -18,7 +18,21 @@ const STAGES = new Set([
'Absage',
]);
-const TEXT_FIELDS = ['title', 'company', 'description', 'last_interaction_note', 'next_step', 'notes'];
+const TEXT_FIELDS = [
+ 'title',
+ 'company',
+ // Fuer welchen Berater das Projekt laeuft. Gepflegt wird die Auswahl unter
+ // Einstellungen -> Akquise; geprueft wird hier nur die Laenge, damit ein aus
+ // der Liste entfernter Name eine alte Opportunity nicht unaenderbar macht.
+ 'consultant',
+ 'description',
+ 'last_interaction_note',
+ 'next_step',
+ 'notes',
+];
+
+/** Laengengrenze der Spalte - abgewiesen statt stillschweigend abgeschnitten. */
+const CONSULTANT_MAX_LENGTH = 190;
const DATE_FIELDS = ['start_date', 'end_date', 'last_interaction_at'];
const NUMBER_FIELDS = ['daily_rate', 'duration_days', 'fixed_price'];
@@ -303,6 +317,13 @@ router.post('/', async (req, res, next) => {
return res.status(400).json({ error: 'Zuständiger Benutzer existiert nicht.' });
}
+ const consultant = emptyToNull(req.body?.consultant);
+ if (consultant !== null && consultant.length > CONSULTANT_MAX_LENGTH) {
+ return res
+ .status(400)
+ .json({ error: `Der Berater ist zu lang (max. ${CONSULTANT_MAX_LENGTH} Zeichen).` });
+ }
+
const contacts = await normalizeContacts(req.body?.contacts ?? []);
if (contacts.error) return res.status(400).json({ error: contacts.error });
@@ -313,13 +334,14 @@ router.post('/', async (req, res, next) => {
// Wer direkt in 'Beauftragt' anlegt, hat gerade gewonnen - dann steht das
// Datum sofort.
`INSERT INTO crm_opportunities
- (title, company, description, stage, won_at, probability, daily_rate, duration_days,
- fixed_price, start_date, end_date, last_interaction_at, last_interaction_note,
- next_step, tags, notes, owner_id, created_by)
- VALUES (?, ?, ?, ?, ${stage === 'Beauftragt' ? 'UTC_TIMESTAMP()' : 'NULL'}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ (title, company, consultant, description, stage, won_at, probability, daily_rate,
+ duration_days, fixed_price, start_date, end_date, last_interaction_at,
+ last_interaction_note, next_step, tags, notes, owner_id, created_by)
+ VALUES (?, ?, ?, ?, ?, ${stage === 'Beauftragt' ? 'UTC_TIMESTAMP()' : 'NULL'}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
title,
emptyToNull(req.body?.company),
+ consultant,
emptyToNull(req.body?.description),
stage,
Math.round(probability.value ?? 10),
@@ -381,6 +403,11 @@ router.patch('/:id', async (req, res, next) => {
if (field === 'title' && !value) {
return res.status(400).json({ error: 'Der Titel darf nicht leer sein.' });
}
+ if (field === 'consultant' && value !== null && value.length > CONSULTANT_MAX_LENGTH) {
+ return res
+ .status(400)
+ .json({ error: `Der Berater ist zu lang (max. ${CONSULTANT_MAX_LENGTH} Zeichen).` });
+ }
fields.push(`${field} = ?`);
values.push(value);
}
diff --git a/server/src/schema.sql b/server/src/schema.sql
index 44bcae0..22b4082 100644
--- a/server/src/schema.sql
+++ b/server/src/schema.sql
@@ -27,7 +27,7 @@ ALTER TABLE crm_users ADD COLUMN IF NOT EXISTS avatar LONGTEXT NULL AFTER color;
-- Persoenliche Darstellung. Sie haengt am Benutzer und nicht am Browser: wer
-- sich an einem anderen Rechner anmeldet, findet sein Design wieder, und zwei
-- Benutzer am selben Rechner kommen sich nicht in die Quere.
--- theme = Farbwelt ('perinco', 'pixel', 'outdoor'); NULL = 'perinco'
+-- theme = Farbwelt ('perinco', 'pixel', 'outdoor', 'cyberpunk'); NULL = 'perinco'
-- theme_mode = 'light' oder 'dark'; NULL = der Systemeinstellung folgen
-- Bewusst VARCHAR und kein ENUM: ein weiteres Theme soll eine Zeile im
-- Frontend sein und keine Datenbankmigration. Geprueft wird in userFields.js.
@@ -177,12 +177,20 @@ ALTER TABLE crm_opportunities ADD COLUMN IF NOT EXISTS won_at DATETIME NULL AFTE
ALTER TABLE crm_opportunities ADD INDEX IF NOT EXISTS idx_crm_opp_won (won_at);
/*
- * Zeitpunkt des Gewinns. Ohne ihn liesse sich "wie viele Projekte haben wir im
- * Mai gewonnen" nicht beantworten - die Phase allein sagt nur, wie es jetzt
- * steht. Gesetzt beim Wechsel nach 'Beauftragt', geleert beim Wechsel heraus.
+ * Fuer welchen Berater das Projekt laeuft.
+ *
+ * Bis zum 09.09.2026 stand der Berater nur in den Notizen: die Uebernahme aus
+ * der Akquise schrieb ihn dorthin, weil die Opportunity kein Feld dafuer hatte.
+ * In Pipeline, Liste und Kacheln war damit nicht zu sehen, fuer wen ein Projekt
+ * laeuft - neben dem Titel die wichtigste Angabe. Bestehende Opportunities aus
+ * der Akquise holt migrateOpportunityConsultant() in db.js aus ihren Notizen
+ * nach.
+ *
+ * Genau ein Name je Opportunity: passt ein Fund zu drei Beratern, entstehen bei
+ * der Uebernahme drei Opportunities. Deshalb 190 Zeichen wie die uebrigen
+ * Namensfelder und nicht 500 wie in der Akquise, wo eine Liste darin steht.
*/
-ALTER TABLE crm_opportunities ADD COLUMN IF NOT EXISTS won_at DATETIME NULL AFTER stage;
-ALTER TABLE crm_opportunities ADD INDEX IF NOT EXISTS idx_crm_opp_won (won_at);
+ALTER TABLE crm_opportunities ADD COLUMN IF NOT EXISTS consultant VARCHAR(190) NULL AFTER company;
ALTER TABLE crm_opportunities ADD COLUMN IF NOT EXISTS deleted_at DATETIME NULL AFTER created_by;
ALTER TABLE crm_opportunities ADD COLUMN IF NOT EXISTS deleted_by INT UNSIGNED NULL AFTER deleted_at;
diff --git a/src/components/opportunities/Opportunities.tsx b/src/components/opportunities/Opportunities.tsx
index 933416e..f3c7588 100644
--- a/src/components/opportunities/Opportunities.tsx
+++ b/src/components/opportunities/Opportunities.tsx
@@ -51,6 +51,7 @@ const VIEW_BUTTONS: { mode: OpportunityViewMode; label: string; icon: string }[]
/** Sortierbare Felder in der Reihenfolge des Auswahlmenues. */
const SORT_OPTIONS: OpportunitySortKey[] = [
'updated_at',
+ 'consultant',
'last_interaction',
'volume',
'daily_rate',
@@ -69,6 +70,8 @@ function sortValue(opportunity: Opportunity, key: OpportunitySortKey): string |
return opportunity.title.toLowerCase();
case 'company':
return opportunity.company?.toLowerCase() ?? null;
+ case 'consultant':
+ return opportunity.consultant?.toLowerCase() ?? null;
case 'stage':
return OPPORTUNITY_STAGES.indexOf(opportunity.stage);
case 'probability':
@@ -128,6 +131,7 @@ export const Opportunities: React.FC = ({
const haystack = [
opportunity.title,
opportunity.company,
+ opportunity.consultant,
opportunity.description,
opportunity.last_interaction_note,
opportunity.next_step,
@@ -223,7 +227,7 @@ export const Opportunities: React.FC = ({
type="search"
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
- placeholder="Titel, Firma, Ansprechpartner (Komma = UND)"
+ placeholder="Titel, Firma, Berater, Ansprechpartner (Komma = UND)"
className="w-full rounded-xl border border-ink-200 bg-surface py-2.5 pl-10 pr-4 text-sm text-ink-900 outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25"
/>
{opportunity.company || 'Ohne Firma'}
+ {/* Fuer wen das Projekt laeuft - hervorgehoben, weil es die
+ Kachel sonst nicht hergibt. */}
+ {opportunity.consultant && (
+
+ {opportunity.consultant}
+
+ )}
{/* Sichtbar, nicht erst beim Ueberfahren: ein Knopf, den man nur findet,
wenn man ihn schon kennt, ist keiner. */}
diff --git a/src/components/opportunities/OpportunityList.tsx b/src/components/opportunities/OpportunityList.tsx
index 5dd1fe0..308f30d 100644
--- a/src/components/opportunities/OpportunityList.tsx
+++ b/src/components/opportunities/OpportunityList.tsx
@@ -19,6 +19,7 @@ import {
export type OpportunitySortKey =
| 'title'
| 'company'
+ | 'consultant'
| 'stage'
| 'probability'
| 'volume'
@@ -37,6 +38,7 @@ export interface OpportunitySortConfig {
export const SORT_LABELS: Record = {
title: 'Titel',
company: 'Firma',
+ consultant: 'Berater',
stage: 'Phase',
probability: 'Chance',
volume: 'Volumen',
@@ -59,6 +61,9 @@ export const DESC_FIRST: OpportunitySortKey[] = [
const COLUMNS: { key: OpportunitySortKey | null; label: string; align?: 'right' }[] = [
{ key: 'title', label: 'Opportunity' },
+ // Direkt hinter dem Titel: fuer wen das Projekt laeuft, ist die Frage nach
+ // dem Was.
+ { key: 'consultant', label: 'Berater' },
{ key: 'stage', label: 'Phase' },
{ key: 'probability', label: 'Chance' },
{ key: 'volume', label: 'Volumen', align: 'right' },
@@ -159,6 +164,15 @@ export const OpportunityList: React.FC = ({
+