You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
2 years ago
|
import Big from "big.js";
|
||
|
import { Component, createMemo } from "solid-js";
|
||
|
import { createStore } from "solid-js/store";
|
||
|
import { TextInput } from "./Form";
|
||
|
import { calculateAgileQuantity } from "./Positions";
|
||
|
import { formatAmount } from "./SwissInvoice";
|
||
|
|
||
|
const AgileCalculator: Component = () => {
|
||
|
const [agileCalculator, setAgileCalculator] = createStore({
|
||
|
minPoints: 0,
|
||
|
maxPoints: 10,
|
||
|
risk: 70,
|
||
|
singlePrice: 10.0,
|
||
|
hoursPerPoint: 1,
|
||
|
});
|
||
|
const calculatorQuantity = createMemo(() =>
|
||
|
calculateAgileQuantity(
|
||
|
agileCalculator.hoursPerPoint,
|
||
|
new Big(agileCalculator.risk).div(100).toNumber(),
|
||
|
agileCalculator.minPoints,
|
||
|
agileCalculator.maxPoints
|
||
|
)
|
||
|
);
|
||
|
|
||
|
return (
|
||
|
<div class="border border-slate-500/40 flex flex-col gap-2 p-2 rounded mb-3">
|
||
|
<h3 class="mt-0">Rechnungsbeispiel</h3>
|
||
|
<TextInput
|
||
|
labelMinWidth="300px"
|
||
|
label="Risiko Faktor"
|
||
|
suffix="%"
|
||
|
step="10"
|
||
|
min="0"
|
||
|
max="100"
|
||
|
type="number"
|
||
|
value={agileCalculator.risk}
|
||
|
onInput={(e) =>
|
||
|
!Number.isNaN(e.currentTarget.valueAsNumber) &&
|
||
|
setAgileCalculator("risk", e.currentTarget.valueAsNumber)
|
||
|
}
|
||
|
/>
|
||
|
<TextInput
|
||
|
labelMinWidth="300px"
|
||
|
label="Stunden pro Story Point"
|
||
|
min="0"
|
||
|
type="number"
|
||
|
value={agileCalculator.hoursPerPoint}
|
||
|
onInput={(e) =>
|
||
|
!Number.isNaN(e.currentTarget.valueAsNumber) &&
|
||
|
setAgileCalculator("hoursPerPoint", e.currentTarget.valueAsNumber)
|
||
|
}
|
||
|
/>
|
||
|
<TextInput
|
||
|
labelMinWidth="300px"
|
||
|
label="Story Points Minimum"
|
||
|
min="0"
|
||
|
type="number"
|
||
|
value={agileCalculator.minPoints}
|
||
|
onInput={(e) =>
|
||
|
!Number.isNaN(e.currentTarget.valueAsNumber) &&
|
||
|
setAgileCalculator("minPoints", e.currentTarget.valueAsNumber)
|
||
|
}
|
||
|
/>
|
||
|
<TextInput
|
||
|
labelMinWidth="300px"
|
||
|
label="Story Points Maximum"
|
||
|
min="0"
|
||
|
type="number"
|
||
|
value={agileCalculator.maxPoints}
|
||
|
onInput={(e) =>
|
||
|
!Number.isNaN(e.currentTarget.valueAsNumber) &&
|
||
|
setAgileCalculator("maxPoints", e.currentTarget.valueAsNumber)
|
||
|
}
|
||
|
/>
|
||
|
<TextInput
|
||
|
labelMinWidth="300px"
|
||
|
label="Einzelpreis"
|
||
|
step="0.01"
|
||
|
suffix="CHF"
|
||
|
type="number"
|
||
|
value={agileCalculator.singlePrice}
|
||
|
onInput={(e) =>
|
||
|
!Number.isNaN(e.currentTarget.valueAsNumber) &&
|
||
|
setAgileCalculator("singlePrice", e.currentTarget.valueAsNumber)
|
||
|
}
|
||
|
/>
|
||
|
<div class="items-center grid grid-cols-[300px_1fr] gap-2">
|
||
|
<div class="p-1 font-mono text-sm text-right">
|
||
|
{"("}
|
||
|
<span title="Nichtrisiko Anteil" class="text-title-border">
|
||
|
{new Big(-100).plus(agileCalculator.risk).abs().toNumber()}%
|
||
|
</span>{" "}
|
||
|
*{" "}
|
||
|
<span title="Story Points Minimum" class="text-title-border">
|
||
|
{agileCalculator.minPoints} SP
|
||
|
</span>
|
||
|
{")"} + {"("}
|
||
|
<span title="Risiko Anteil" class="text-title-border">
|
||
|
{agileCalculator.risk}%
|
||
|
</span>{" "}
|
||
|
*{" "}
|
||
|
<span title="Story Points Maximum" class="text-title-border">
|
||
|
{agileCalculator.maxPoints > agileCalculator.minPoints
|
||
|
? agileCalculator.maxPoints
|
||
|
: agileCalculator.minPoints}{" "}
|
||
|
SP
|
||
|
</span>
|
||
|
{")"} =
|
||
|
</div>
|
||
|
<div class="p-1">
|
||
|
<div class="flex justify-between">
|
||
|
<span>Gewichtete Story Points:</span>
|
||
|
{new Big(calculatorQuantity())
|
||
|
.div(agileCalculator.hoursPerPoint)
|
||
|
.toNumber()}{" "}
|
||
|
SP
|
||
|
</div>
|
||
|
</div>
|
||
|
<div class="p-1 font-mono text-sm text-right">
|
||
|
<span title="Gewichtete Story Points" class="text-title-border">
|
||
|
{new Big(calculatorQuantity())
|
||
|
.div(agileCalculator.hoursPerPoint)
|
||
|
.toNumber()}{" "}
|
||
|
SP
|
||
|
</span>{" "}
|
||
|
*{" "}
|
||
|
<span title="Stunden pro Story Point" class="text-title-border">
|
||
|
{agileCalculator.hoursPerPoint} h
|
||
|
</span>{" "}
|
||
|
*{" "}
|
||
|
<span title="Einzelpreis" class="text-title-border">
|
||
|
{agileCalculator.singlePrice} CHF
|
||
|
</span>{" "}
|
||
|
=
|
||
|
</div>
|
||
|
<div class="p-1 font-bold">
|
||
|
<div class="flex justify-between">
|
||
|
<span>Gesamtpreis:</span>
|
||
|
{formatAmount(
|
||
|
calculatorQuantity() * agileCalculator.singlePrice
|
||
|
)}{" "}
|
||
|
CHF
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export default AgileCalculator;
|