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.
rappli/src/components/Positions.tsx

238 lines
7.3 KiB
TypeScript

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";
export const calculateAgileQuantity = (
hoursPerStoryPoint = 0,
riskFactor = 0,
minPoints = 0,
maxPoints = 0
) => {
if (minPoints > maxPoints) {
maxPoints = minPoints;
}
const minHours = minPoints * hoursPerStoryPoint;
const maxHours = maxPoints * hoursPerStoryPoint;
const minWeighted = new Big(-1).plus(riskFactor).abs().mul(minHours);
const maxWeighted = new Big(riskFactor).mul(maxHours);
const quantity = minWeighted.plus(maxWeighted).round().toNumber();
return quantity;
};
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;
const agileRiskFactor =
position.agileRiskFactor != null
? position.agileRiskFactor
: state.agileRiskFactor;
quantity = calculateAgileQuantity(
state.agileHoursPerStoryPoint,
agileRiskFactor,
min,
max
);
}
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 (
<Show when={positions()}>
<table class="table table-compact text-sm w-full mb-12">
<thead class="pt-9">
<tr>
<th class="!relative">Pos.</th>
<th class="w-full">Bezeichnung</th>
<th class="text-center">Menge</th>
<th class="text-right">Einzelpreis</th>
<th class="text-right">Gesamtpreis</th>
</tr>
</thead>
<tbody use:autoAnimate>
<For each={positions()}>
{(position, idx) => {
const hasTwoRows = createMemo(
() =>
!!position.description || position.fixedDiscountPrice != null
);
return (
<>
<tr class="tr-bg-transparent">
<th
rowSpan={hasTwoRows() ? 2 : 1}
class="!relative border-b-0 align-top"
>
{position.number || idx() + 1}
</th>
<td
classList={{
"align-top break-words whitespace-normal": true,
"border-b-0 pb-0": hasTwoRows(),
}}
>
{position.name}
</td>
<td
classList={{
"align-top text-center !whitespace-nowrap": true,
"border-b-0 pb-0": hasTwoRows(),
}}
>
{getQuantity(position, state)}
</td>
<td
classList={{
"align-top text-right !whitespace-nowrap": true,
"border-b-0 pb-0": hasTwoRows(),
}}
>
{formatAmount(
position.itemPrice != null
? position.itemPrice
: state.defaultItemPrice
)}{" "}
CHF
</td>
<td
classList={{
"align-top text-right !whitespace-nowrap": true,
"border-b-0 pb-0": hasTwoRows(),
"line-through": position.fixedDiscountPrice != null,
}}
>
{formatAmount(calculatePrice(position, state))} CHF
</td>
</tr>
<Show when={hasTwoRows()}>
<tr class="tr-bg-transparent">
<td class="align-top pt-1" colspan={3}>
<Show when={!!position.description}>
<Markdown
class="px-4 opacity-75 prose-sm"
value={position.description!}
/>
</Show>
</td>
<td class="align-top pt-1 text-right !whitespace-nowrap">
<Show when={position.fixedDiscountPrice != null}>
{formatAmount(position.fixedDiscountPrice!)} CHF
</Show>
</td>
</tr>
</Show>
</>
);
}}
</For>
<Show when={localState.vatRate > 0}>
<tr class="h-12">
<th class="!relative border-b-0"></th>
<td class="align-bottom">Summe</td>
<td></td>
<td></td>
<td class="text-right align-bottom !whitespace-nowrap">
{formatAmount(props.invoiceData.amountBeforeTax)} CHF
</td>
</tr>
<tr>
<th class="!relative border-b-0"></th>
<td>
Mehrwertsteuer {new Big(localState.vatRate).mul(100).toNumber()}
%
</td>
<td></td>
<td></td>
<td class="text-right !whitespace-nowrap">
{formatAmount(props.invoiceData.tax)} CHF
</td>
</tr>
</Show>
<tr class="font-bold h-12">
<th class="!relative"></th>
<td class="align-bottom">Gesamtbetrag</td>
<td></td>
<td></td>
<td class="align-bottom text-right !whitespace-nowrap">
{formatAmount(props.invoiceData.amount)} CHF
</td>
</tr>
</tbody>
</table>
</Show>
);
};
export default Positions;