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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4635b9a64b
commit
64dea0b100
@@ -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<OpportunitiesProps> = ({
|
||||
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<OpportunitiesProps> = ({
|
||||
{ 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) => (
|
||||
<div key={metric.label}>
|
||||
<p className="text-[10px] font-extrabold uppercase tracking-wider text-ink-400">
|
||||
|
||||
@@ -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<OpportunitySortKey, string> = {
|
||||
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<OpportunitySortKey, string> = {
|
||||
export const DESC_FIRST: OpportunitySortKey[] = [
|
||||
'probability',
|
||||
'volume',
|
||||
'weighted',
|
||||
'daily_rate',
|
||||
'duration',
|
||||
'last_interaction',
|
||||
@@ -170,9 +166,6 @@ export const OpportunityList: React.FC<OpportunityListProps> = ({
|
||||
<span className="text-sm font-bold tabular-nums text-ink-700">
|
||||
{opportunity.probability}%
|
||||
</span>
|
||||
<p className="whitespace-nowrap text-[11px] text-ink-400">
|
||||
{formatEuro(weightedVolume(opportunity))}
|
||||
</p>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-3 text-right text-sm font-bold text-ink-800">
|
||||
{formatEuro(opportunityVolume(opportunity))}
|
||||
|
||||
@@ -111,7 +111,6 @@ export const OpportunityModal: React.FC<OpportunityModalProps> = ({
|
||||
// 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<OpportunityModalProps> = ({
|
||||
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<OpportunityModalProps> = ({
|
||||
</p>
|
||||
<p className="text-lg font-extrabold text-ink-900">{formatEuro(preview.volume)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-extrabold uppercase tracking-wider text-ink-400">
|
||||
Gewichtet ({form.probability} %)
|
||||
</p>
|
||||
<p className="text-lg font-extrabold text-brand-700">
|
||||
{formatEuro(preview.weighted)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
+6
-5
@@ -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 '—';
|
||||
|
||||
Reference in New Issue
Block a user