From 64dea0b100308b5b81263258218779206e9e50fc Mon Sep 17 00:00:00 2001 From: Sascha Wolf Date: Tue, 18 Aug 2026 16:17:53 +0200 Subject: [PATCH] feat(opportunities): gewichtetes Volumen entfernt So wird bei perinco nicht gerechnet: die Summe "Gewichtet" aenderte sich beim Schieben der Abschlusswahrscheinlichkeit und behauptete damit eine Groesse, die es im Haus nicht gibt. Weg sind die Kennzahl ueber der Liste, die Spalte samt Sortierung, die zweite Zahl in der Vorschau des Formulars und weightedVolume() selbst. Die Wahrscheinlichkeit bleibt an der Opportunity - Schieberegler im Formular, Prozent in Liste und Pipeline-Karte; sie geht nur in keine Summe mehr ein. Co-Authored-By: Claude Opus 5 (1M context) --- docs/07-anleitung-benutzer.md | 12 ++++++++---- docs/09-quellcode.md | 2 +- docs/10-glossar.md | 2 +- src/components/opportunities/Opportunities.tsx | 6 ------ src/components/opportunities/OpportunityList.tsx | 7 ------- src/components/opportunities/OpportunityModal.tsx | 15 +++------------ src/types.ts | 11 ++++++----- 7 files changed, 19 insertions(+), 36 deletions(-) diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md index 8e2c23c..4915edc 100644 --- a/docs/07-anleitung-benutzer.md +++ b/docs/07-anleitung-benutzer.md @@ -263,10 +263,14 @@ der Kopfzeile. > Gewonnen, Verloren) ließ nicht erkennen, in welcher der neuen Phasen ein > Projekt steckt. -**Volumen** rechnet die Anwendung selbst: Tagessatz x Personentage. Das -**gewichtete** Volumen multipliziert das noch mit der Abschlusswahrscheinlichkeit. -Beide Zahlen stehen live im Formular, auf jeder Karte, als Summe über der Liste, -je Pipeline-Spalte und in der Kopfzeile. +**Volumen** rechnet die Anwendung selbst: Tagessatz x Personentage. Die Zahl +steht live im Formular, auf jeder Karte, als Summe über der Liste, je +Pipeline-Spalte und in der Kopfzeile. + +Ein mit der **Abschlusswahrscheinlichkeit gewichtetes** Volumen gibt es nicht +mehr (entfernt am 18.08.2026) - so wird bei perinco nicht gerechnet. Die +Wahrscheinlichkeit selbst bleibt an der Opportunity und steht in Liste und +Pipeline; sie geht nur in keine Summe mehr ein. Nicht jedes Projekt rechnet nach Aufwand. Für Festpreise gibt es unter *Projektdaten* das Feld **Projektpreis**. Steht dort eine Zahl, gilt sie als diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md index 95cdfa8..6130082 100644 --- a/docs/09-quellcode.md +++ b/docs/09-quellcode.md @@ -301,7 +301,7 @@ nicht auseinanderlaufen: | `VIEW_TITLES`, `viewTitle()` | abweichende Beschriftung, wo der Ansichtsname nur intern eindeutig sein muss (`Opportunity-Papierkorb` → Papierkorb) | | `MAIL_LEAD_SOURCE`, `LeadOriginFilter` | Herkunft „Mail" in `Lead.source` und ihr Filter | | `USER_COLORS`, `userColor()` | Farbe gesetzt, sonst stabil aus der Benutzer-Id | -| `opportunityVolume()`, `weightedVolume()`, `formatEuro()` | Festpreis, sonst Tagessatz x Personentage; gewichtet; formatiert | +| `opportunityVolume()`, `formatEuro()` | Festpreis, sonst Tagessatz x Personentage; formatiert. Ein gewichtetes Volumen gibt es bewusst nicht | | `OPPORTUNITY_STAGES`, `OPEN_STAGES`, `LEAD_KINDS`, `CONNECTION_TYPES` | die Aufzählungen der Datenbank | | `ACQUISITION_OPTION_FIELDS`, `ACQUISITION_FIELD_LABELS`, `ACQUISITION_FIELD_HINTS` | die vier Auswahllisten der Akquise samt Beschriftung | | `eventColor()`, `leadFullName()`, `leadInitials()`, `contactFullName()` | Anzeigelogik | diff --git a/docs/10-glossar.md b/docs/10-glossar.md index 2ae19ed..3dd9f2c 100644 --- a/docs/10-glossar.md +++ b/docs/10-glossar.md @@ -37,7 +37,7 @@ Quelltext. | **Schwellenwert Anzeige** | `display_threshold` - Treffer darunter bleiben im Dashboard ausgeblendet. | | **Schwellenwert Benachrichtigung** | `match_threshold` - ab hier gibt es Signalton und E-Mail. | | **Verbindung** | Kante zwischen zwei Kontakten im Netzwerkdiagramm, mit Art und Stärke. Tabelle `crm_lead_connections`. | -| **Volumen** | Tagessatz x Personentage - oder der **Projektpreis**, wenn einer eingetragen ist. **Gewichtet** heißt zusätzlich mit der Abschlusswahrscheinlichkeit multipliziert. | +| **Volumen** | Tagessatz x Personentage - oder der **Projektpreis**, wenn einer eingetragen ist. Ein mit der Abschlusswahrscheinlichkeit gewichtetes Volumen gibt es nicht (seit 18.08.2026 entfernt); so wird bei perinco nicht gerechnet. | | **Whisper** | Das Spracherkennungsmodell für das Diktat. Läuft im Browser, nicht auf dem Server. | | **Zugriffsrichtlinie** | `ApplicationAccessPolicy` in Exchange Online. Begrenzt eine App-only-Berechtigung auf einzelne Postfächer - ohne sie gilt sie tenant-weit. | | **Zuständiger** / Owner | Benutzer, dem ein Kontakt oder eine Opportunity zugeordnet ist. Darf sie ändern, auch wenn er sie nicht angelegt hat. | diff --git a/src/components/opportunities/Opportunities.tsx b/src/components/opportunities/Opportunities.tsx index b92f32b..933416e 100644 --- a/src/components/opportunities/Opportunities.tsx +++ b/src/components/opportunities/Opportunities.tsx @@ -19,7 +19,6 @@ import { contactFullName, formatEuro, opportunityVolume, - weightedVolume, type AppUser, type Lead, type Opportunity, @@ -54,7 +53,6 @@ const SORT_OPTIONS: OpportunitySortKey[] = [ 'updated_at', 'last_interaction', 'volume', - 'weighted', 'daily_rate', 'duration', 'probability', @@ -77,8 +75,6 @@ function sortValue(opportunity: Opportunity, key: OpportunitySortKey): string | return opportunity.probability; case 'volume': return opportunityVolume(opportunity); - case 'weighted': - return weightedVolume(opportunity); case 'daily_rate': return opportunity.daily_rate; case 'duration': @@ -167,7 +163,6 @@ export const Opportunities: React.FC = ({ const totals = useMemo( () => ({ volume: filtered.reduce((sum, item) => sum + (opportunityVolume(item) ?? 0), 0), - weighted: filtered.reduce((sum, item) => sum + (weightedVolume(item) ?? 0), 0), open: filtered.filter((item) => OPEN_STAGES.includes(item.stage)).length, }), [filtered] @@ -346,7 +341,6 @@ export const Opportunities: React.FC = ({ { label: 'Opportunities', value: String(filtered.length) }, { label: 'davon offen', value: String(totals.open) }, { label: 'Volumen', value: formatEuro(totals.volume) }, - { label: 'Gewichtet', value: formatEuro(totals.weighted) }, ].map((metric) => (

diff --git a/src/components/opportunities/OpportunityList.tsx b/src/components/opportunities/OpportunityList.tsx index ce17dd7..5dd1fe0 100644 --- a/src/components/opportunities/OpportunityList.tsx +++ b/src/components/opportunities/OpportunityList.tsx @@ -12,7 +12,6 @@ import { contactFullName, formatEuro, opportunityVolume, - weightedVolume, type Opportunity, } from '../../types'; @@ -23,7 +22,6 @@ export type OpportunitySortKey = | 'stage' | 'probability' | 'volume' - | 'weighted' | 'daily_rate' | 'duration' | 'last_interaction' @@ -42,7 +40,6 @@ export const SORT_LABELS: Record = { stage: 'Phase', probability: 'Chance', volume: 'Volumen', - weighted: 'Gewichtet', daily_rate: 'Tagessatz', duration: 'Laufzeit', last_interaction: 'Letzte Interaktion', @@ -54,7 +51,6 @@ export const SORT_LABELS: Record = { export const DESC_FIRST: OpportunitySortKey[] = [ 'probability', 'volume', - 'weighted', 'daily_rate', 'duration', 'last_interaction', @@ -170,9 +166,6 @@ export const OpportunityList: React.FC = ({ {opportunity.probability}% -

- {formatEuro(weightedVolume(opportunity))} -

{formatEuro(opportunityVolume(opportunity))} diff --git a/src/components/opportunities/OpportunityModal.tsx b/src/components/opportunities/OpportunityModal.tsx index ed3ba4f..21b5308 100644 --- a/src/components/opportunities/OpportunityModal.tsx +++ b/src/components/opportunities/OpportunityModal.tsx @@ -111,7 +111,6 @@ export const OpportunityModal: React.FC = ({ // Festpreis sofort sichtbar ist. Ein gesetzter Projektpreis gewinnt - genau // wie in opportunityVolume(). const preview = useMemo(() => { - const probability = Number(form.probability) || 0; const toNumber = (value: string) => Number(value.replace(',', '.')); const fixed = toNumber(form.fixed_price); @@ -126,9 +125,9 @@ export const OpportunityModal: React.FC = ({ Number.isFinite(days); if (!hasFixed && !hasRate) return null; - const volume = hasFixed ? fixed : rate * days; - return { volume, weighted: (volume * probability) / 100, isFixed: hasFixed }; - }, [form.daily_rate, form.duration_days, form.fixed_price, form.probability]); + // Kein gewichtetes Volumen: die Wahrscheinlichkeit steht fuer sich. + return { volume: hasFixed ? fixed : rate * days, isFixed: hasFixed }; + }, [form.daily_rate, form.duration_days, form.fixed_price]); const addTags = () => { const parsed = tagInput @@ -418,14 +417,6 @@ export const OpportunityModal: React.FC = ({

{formatEuro(preview.volume)}

-
-

- Gewichtet ({form.probability} %) -

-

- {formatEuro(preview.weighted)} -

-
)} diff --git a/src/types.ts b/src/types.ts index 865d013..cb19bf2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -405,11 +405,12 @@ export function opportunityVolume(opportunity: Opportunity): number | null { return opportunity.daily_rate * opportunity.duration_days; } -/** Mit der Abschlusswahrscheinlichkeit gewichtetes Volumen. */ -export function weightedVolume(opportunity: Opportunity): number | null { - const volume = opportunityVolume(opportunity); - return volume === null ? null : (volume * opportunity.probability) / 100; -} +/* + * Ein mit der Abschlusswahrscheinlichkeit gewichtetes Volumen gibt es hier + * bewusst nicht (am 18.08.2026 entfernt): so wird bei perinco nicht gerechnet. + * Die Wahrscheinlichkeit selbst bleibt an der Opportunity - sie sagt etwas, ohne + * dass daraus eine Summe gebildet werden muss. + */ export function formatEuro(value: number | null): string { if (value === null) return '—';