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.
238 lines
7.3 KiB
TypeScript
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;
|