feat: implement invoice Positions component
parent
74739a2a7a
commit
b7b26b09d5
@ -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 (
|
||||
<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-all whitespace-normal": true,
|
||||
"border-b-0 pb-0": hasTwoRows(),
|
||||
}}
|
||||
>
|
||||
{position.name}
|
||||
</td>
|
||||
<td
|
||||
classList={{
|
||||
"align-top text-center": true,
|
||||
"border-b-0 pb-0": hasTwoRows(),
|
||||
}}
|
||||
>
|
||||
{getQuantity(position, state)}
|
||||
</td>
|
||||
<td
|
||||
classList={{
|
||||
"align-top text-right": true,
|
||||
"border-b-0 pb-0": hasTwoRows(),
|
||||
}}
|
||||
>
|
||||
{formatAmount(
|
||||
position.itemPrice != null
|
||||
? position.itemPrice
|
||||
: state.defaultItemPrice
|
||||
)}{" "}
|
||||
CHF
|
||||
</td>
|
||||
<td
|
||||
classList={{
|
||||
"align-top text-right ": 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">
|
||||
<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"></th>
|
||||
<td class="align-bottom">Summe</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right align-bottom">
|
||||
{formatAmount(props.invoiceData.amountBeforeTax)} CHF
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="!relative"></th>
|
||||
<td>
|
||||
Mehrwertsteuer {new Big(localState.vatRate).mul(100).toNumber()}
|
||||
%
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
{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">
|
||||
{formatAmount(props.invoiceData.amount)} CHF
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
export default Positions;
|
Loading…
Reference in New Issue