feat: implement index route
parent
0c0ef7fe99
commit
c0c298df83
@ -0,0 +1,358 @@
|
||||
import { Link, Params } from "solid-app-router";
|
||||
|
||||
import {
|
||||
Component,
|
||||
createMemo,
|
||||
JSX,
|
||||
Show,
|
||||
FlowComponent,
|
||||
mergeProps,
|
||||
ParentComponent,
|
||||
createSignal,
|
||||
onMount,
|
||||
} from "solid-js";
|
||||
import SwissInvoice, { InvoiceData } from "~/components/SwissInvoice";
|
||||
import { Style, Title } from "solid-meta";
|
||||
import Page from "~/components/Page";
|
||||
// import HelpIcon from "~icons/carbon/help-filled";
|
||||
import LufraiLogoWww from "~icons/custom/lufrai-logo-www";
|
||||
import AppIcon from "~icons/custom/icon";
|
||||
|
||||
import { getLine1, getLine2, isStructuredAddress } from "~/components/Address";
|
||||
import Positions, {
|
||||
calculatePositionsPrice,
|
||||
calculatePositionsTax,
|
||||
} from "~/components/Positions";
|
||||
import SettingsOverlay from "~/components/Settings/Overlay";
|
||||
import {
|
||||
createLocalStore,
|
||||
createStore,
|
||||
createUiStore,
|
||||
LocalStoreContext,
|
||||
PRINT_TYPE_CONFIRMATION,
|
||||
PRINT_TYPE_INVOICE,
|
||||
PRINT_TYPE_OFFER,
|
||||
StoreContext,
|
||||
UiStoreContext,
|
||||
} from "~/stores";
|
||||
import Big from "big.js";
|
||||
import { getDisplayDateFromUnix, roundToStep } from "~/util";
|
||||
import WelcomeModal from "~/components/WelcomeModal";
|
||||
import Markdown from "~/components/Markdown";
|
||||
|
||||
export default function Home() {
|
||||
const store = createStore();
|
||||
const [state, setState] = store;
|
||||
const localStore = createLocalStore();
|
||||
const [localState, setLocalState] = localStore;
|
||||
const uiStore = createUiStore();
|
||||
const [uiState, setUiState] = uiStore;
|
||||
|
||||
const invoiceData = createMemo((): InvoiceData => {
|
||||
const amountBeforeTax = calculatePositionsPrice(state.positions, state);
|
||||
const tax = calculatePositionsTax(amountBeforeTax, localState);
|
||||
const totalAmount = new Big(amountBeforeTax).plus(tax).toNumber();
|
||||
const totalAmountRounded = roundToStep(totalAmount, 0.05);
|
||||
|
||||
const creditor = localState.creditor;
|
||||
|
||||
return {
|
||||
iban: localState.iban || "",
|
||||
amount: totalAmountRounded,
|
||||
amountBeforeTax,
|
||||
tax,
|
||||
currency: "CHF",
|
||||
message: state.invoice.message,
|
||||
reference: state.invoice.reference, //generate("12345 12345 12345 12345 1"),
|
||||
creditor: {
|
||||
type: isStructuredAddress(creditor) ? "S" : "K",
|
||||
name: creditor.name,
|
||||
city: creditor.city,
|
||||
zip: creditor.zip || 8500,
|
||||
country: "CH",
|
||||
line1: creditor.line1,
|
||||
line2: creditor.line2,
|
||||
},
|
||||
debtor: state.customer.debtorAddress.name
|
||||
? {
|
||||
type: isStructuredAddress(state.customer.debtorAddress) ? "S" : "K",
|
||||
name: state.customer.debtorAddress.name,
|
||||
city: state.customer.debtorAddress.city,
|
||||
zip: state.customer.debtorAddress.zip,
|
||||
country: "CH",
|
||||
line1: state.customer.debtorAddress.line1,
|
||||
line2: state.customer.debtorAddress.line2,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const InnerPadding: FlowComponent = (props) => (
|
||||
<div classList={{ "px-16": state.fullWidthInvoice }}>{props.children}</div>
|
||||
);
|
||||
|
||||
const PageHeader: Component = () => {
|
||||
const RightItem: ParentComponent<
|
||||
{ label: string; value?: any } & JSX.HTMLAttributes<HTMLDivElement>
|
||||
> = (p) => {
|
||||
const props = mergeProps({ show: true }, p);
|
||||
|
||||
return (
|
||||
<Show when={!!props.value}>
|
||||
<div class={props.class}>{props.label}</div>
|
||||
<div class={"text-right " + props.class}>
|
||||
{props.children || props.value}
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
const address = createMemo(() => {
|
||||
const value =
|
||||
(localState.useCustomAddress && localState.customAddress
|
||||
? localState.customAddress
|
||||
: localState.creditor) || invoiceData().creditor;
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
const customerAddress = createMemo(() => {
|
||||
return state.useCustomerAlternativeAddress
|
||||
? state.customer.alternativeAddress
|
||||
: state.customer.debtorAddress;
|
||||
});
|
||||
|
||||
const titleMemo = createMemo(
|
||||
() =>
|
||||
(state.positions.length > 0 && `(${state.positions.length}) `) +
|
||||
"Räppli" +
|
||||
(state.project.projectNumber.length
|
||||
? ` - ${state.project.projectNumber}`
|
||||
: "")
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="break-all whitespace-normal">
|
||||
<Title>{titleMemo()}</Title>
|
||||
<Show when={localState.logo}>
|
||||
<div class="flex justify-end items-center h-20 mb-5">
|
||||
<img
|
||||
classList={{
|
||||
"max-h-full max-w-[50%] w-auto": true,
|
||||
"h-full": localState.logo?.type.startsWith("image/svg"),
|
||||
"h-auto": !localState.logo?.type.startsWith("image/svg"),
|
||||
}}
|
||||
width={localState.logo?.width}
|
||||
height={localState.logo?.height}
|
||||
src={localState.logo?.url}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="text-xs mb-2">
|
||||
{address().name
|
||||
? [address().name, getLine1(address()), getLine2(address())]
|
||||
.filter((x) => x != "")
|
||||
.join(" · ")
|
||||
: ""}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-[30%] mb-10">
|
||||
<div class="leading-snug">
|
||||
<div>{customerAddress().name}</div>
|
||||
<div>{getLine1(customerAddress())}</div>
|
||||
<div>{getLine2(customerAddress())}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-4 text-sm">
|
||||
<RightItem
|
||||
class="font-bold"
|
||||
label="Projekt Nr."
|
||||
value={state.project.projectNumber}
|
||||
/>
|
||||
<RightItem
|
||||
class="font-bold"
|
||||
label="Bestellungs Nr."
|
||||
value={state.project.orderNumber}
|
||||
/>
|
||||
<RightItem
|
||||
value={getDisplayDateFromUnix(state.project.date)}
|
||||
label="Datum"
|
||||
/>
|
||||
<RightItem
|
||||
label="Lieferungs Nr."
|
||||
value={state.project.deliveryNumber}
|
||||
/>
|
||||
<RightItem
|
||||
label="Lieferdatum"
|
||||
value={
|
||||
state.project.deliveryDate != null &&
|
||||
getDisplayDateFromUnix(state.project.deliveryDate)
|
||||
}
|
||||
/>
|
||||
<RightItem
|
||||
label="Kunden Nr."
|
||||
value={state.customer.customerNumber}
|
||||
/>
|
||||
<RightItem label="Ihre MwST-Nr." value={state.customer.vatNumber} />
|
||||
<Show
|
||||
when={
|
||||
localState.contact.name ||
|
||||
localState.contact.phone ||
|
||||
localState.contact.email
|
||||
}
|
||||
>
|
||||
<hr class="col-span-2 my-2" />
|
||||
<RightItem
|
||||
value={localState.contact.name}
|
||||
label="Ansprechpartner"
|
||||
/>
|
||||
<RightItem value={localState.contact.phone} label="Telefon" />
|
||||
<RightItem
|
||||
value={localState.contact.email}
|
||||
label="E-Mail Adresse"
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={localState.vatNumber}>
|
||||
<div class="col-span-2 h-4"></div>
|
||||
<RightItem label="MwST-Nr." value={localState.vatNumber} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PrintPreview: Component = () => {
|
||||
const Title: FlowComponent = (props) => (
|
||||
<div class="text-4xl mb-8 font-semibold">{props.children}</div>
|
||||
);
|
||||
|
||||
const PositionsWithData = () => (
|
||||
<Positions positions={state.positions} invoiceData={invoiceData()} />
|
||||
);
|
||||
|
||||
const Preface = () => (
|
||||
<Show when={state.project.preface}>
|
||||
<Markdown class="mb-7" value={state.project.preface!} />
|
||||
</Show>
|
||||
);
|
||||
|
||||
const Conclusion = () => (
|
||||
<Show when={state.project.conclusion}>
|
||||
<Markdown class="mb-7" value={state.project.conclusion!} />
|
||||
</Show>
|
||||
);
|
||||
|
||||
const LufraiWatermark = () => (
|
||||
<Show when={localState.showLufraiWatermark}>
|
||||
<div class="text-xs mb-10 font-medium flex justify-center items-center">
|
||||
<a
|
||||
aria-disabled="true"
|
||||
class="transition text-lufrai-primary-light hover:text-lufrai-primary fill-current hover:scale-110 flex items-center gap-2"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://lufrai.org"
|
||||
>
|
||||
Powered by <LufraiLogoWww class="w-auto h-7" />
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Style type="text/css">{`
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 11mm ${state.fullWidthInvoice ? 0 : 4}rem 11mm ${
|
||||
state.fullWidthInvoice ? 0 : 4
|
||||
}rem;
|
||||
}
|
||||
|
||||
.swissinvoice {
|
||||
font-family: Liberation Sans, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
@media print {
|
||||
html {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
`}</Style>
|
||||
<Show when={uiState.printType === PRINT_TYPE_OFFER}>
|
||||
<Page>
|
||||
<InnerPadding>
|
||||
<PageHeader />
|
||||
<Title>Offerte</Title>
|
||||
<Preface />
|
||||
<PositionsWithData />
|
||||
<Conclusion />
|
||||
<LufraiWatermark />
|
||||
</InnerPadding>
|
||||
</Page>
|
||||
</Show>
|
||||
<Show when={uiState.printType === PRINT_TYPE_CONFIRMATION}>
|
||||
<Page>
|
||||
<InnerPadding>
|
||||
<PageHeader />
|
||||
<Title>Auftragsbestätigung</Title>
|
||||
<Preface />
|
||||
<PositionsWithData />
|
||||
<Conclusion />
|
||||
<LufraiWatermark />
|
||||
</InnerPadding>
|
||||
</Page>
|
||||
</Show>
|
||||
<Show when={uiState.printType === PRINT_TYPE_INVOICE}>
|
||||
<Page>
|
||||
<InnerPadding>
|
||||
<PageHeader />
|
||||
<Title>Rechnung</Title>
|
||||
<Preface />
|
||||
<PositionsWithData />
|
||||
<Show when={localState.paymentTerms}>
|
||||
<p class="mb-12 text-sm">
|
||||
Zahlungsbedingungen: {localState.paymentTerms}
|
||||
</p>
|
||||
</Show>
|
||||
<Conclusion />
|
||||
<LufraiWatermark />
|
||||
</InnerPadding>
|
||||
<SwissInvoice value={invoiceData()} />
|
||||
</Page>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const [pulsingLogo, setPulsingLogo] = createSignal(false);
|
||||
onMount(function () {
|
||||
setPulsingLogo(true);
|
||||
setTimeout(function () {
|
||||
setPulsingLogo(false);
|
||||
}, 4000);
|
||||
});
|
||||
|
||||
return (
|
||||
<UiStoreContext.Provider value={uiStore}>
|
||||
<LocalStoreContext.Provider value={localStore}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<div class="block xxl:flex xxl:h-screen bg-slate-200 print:bg-transparent items-stretch">
|
||||
<div
|
||||
onClick={() => setLocalState("showWelcome", true)}
|
||||
classList={{
|
||||
"animate-pulse": pulsingLogo() && !localState.showWelcome,
|
||||
"print:hidden hover:!opacity-100 fixed z-10 right-2 top-0 text-slate-600 m-4 transition-all duration-75 hover:text-swiss-red fill-current cursor-pointer hover:scale-110 text-6xl drop-shadow-md":
|
||||
true,
|
||||
}}
|
||||
>
|
||||
<AppIcon class="text-height" />
|
||||
</div>
|
||||
<SettingsOverlay />
|
||||
<WelcomeModal />
|
||||
<PrintPreview />
|
||||
</div>
|
||||
</StoreContext.Provider>
|
||||
</LocalStoreContext.Provider>
|
||||
</UiStoreContext.Provider>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue