diff --git a/src/components/Positions.tsx b/src/components/Positions.tsx new file mode 100644 index 0000000..0ee05c6 --- /dev/null +++ b/src/components/Positions.tsx @@ -0,0 +1,221 @@ +import { Component, createMemo, For, Show, useContext } from "solid-js"; +import { formatAmount, InvoiceData } from "./SwissInvoice"; +import { autoAnimate } from "~/directives/autoAnimate"; +import { + LocalStoreContext, + LocalStoreObject, + Position, + POSITION_TYPE_AGILE, + StoreContext, + StoreObject, +} from "~/stores"; +import Big from "big.js"; +import Markdown from "./Markdown"; + +const getQuantity = (position: Position, state: StoreObject) => { + let quantity = position.quantity; + + if (position.type === POSITION_TYPE_AGILE) { + const min = position.agilePointsMin || 0; + let max = position.agilePointsMax || 0; + if (min > max) { + max = min; + } + + const minHours = min * state.agileHoursPerStoryPoint; + const maxHours = max * state.agileHoursPerStoryPoint; + const agileRiskFactor = + position.agileRiskFactor != null + ? position.agileRiskFactor + : state.agileRiskFactor; + const normalizedRiskFactor = new Big(agileRiskFactor).mul(2).minus(1); + const minWeighted = new Big(1).minus(normalizedRiskFactor).mul(minHours); + const maxWeighted = new Big(1).plus(normalizedRiskFactor).mul(maxHours); + quantity = minWeighted.plus(maxWeighted).div(2).round().toNumber(); + } + + return quantity; +}; + +const calculatePrice = (position: Position, state: StoreObject) => { + const itemPrice = + position.itemPrice != null ? position.itemPrice : state.defaultItemPrice; + return new Big(itemPrice).mul(getQuantity(position, state)).toNumber(); +}; + +const calculatePriceAfterDiscount = ( + position: Position, + state: StoreObject +) => { + if (position.fixedDiscountPrice != null) { + return position.fixedDiscountPrice; + } + + return calculatePrice(position, state); +}; + +export const calculatePositionsTax = ( + positionsPrice: number, + localState: LocalStoreObject +) => { + return new Big(localState.vatRate).mul(positionsPrice).round(2, 0).toNumber(); +}; + +export const calculatePositionsPrice = ( + positions: Position[], + state: StoreObject +) => { + const result = positions + .reduce((acc, next) => { + if (!next.enabled) { + return acc; + } + + return acc.plus(calculatePriceAfterDiscount(next, state)); + }, new Big(0)) + .toNumber(); + + return result; +}; + +const Positions: Component<{ + positions: Position[]; + invoiceData: InvoiceData; +}> = (props) => { + const [state] = useContext(StoreContext)!; + const [localState] = useContext(LocalStoreContext)!; + + autoAnimate; + + const positions = createMemo(function () { + return props.positions.filter((p) => p.enabled); + }); + + return ( + + + + + + + + + + + + + + {(position, idx) => { + const hasTwoRows = createMemo( + () => + !!position.description || position.fixedDiscountPrice != null + ); + + return ( + <> + + + + + + + + + + + + + + + ); + }} + + 0}> + + + + + + + + + + + + + + + + + + + + + + + +
Pos.BezeichnungMengeEinzelpreisGesamtpreis
+ {position.number || idx() + 1} + + {position.name} + + {getQuantity(position, state)} + + {formatAmount( + position.itemPrice != null + ? position.itemPrice + : state.defaultItemPrice + )}{" "} + CHF + + {formatAmount(calculatePrice(position, state))} CHF +
+ + + + + + {formatAmount(position.fixedDiscountPrice!)} CHF + +
Summe + {formatAmount(props.invoiceData.amountBeforeTax)} CHF +
+ Mehrwertsteuer {new Big(localState.vatRate).mul(100).toNumber()} + % + + {formatAmount(props.invoiceData.tax)} CHF +
Gesamtbetrag + {formatAmount(props.invoiceData.amount)} CHF +
+
+ ); +}; + +export default Positions;