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 '—';