feat: implement index route

master
Katja Lutz 2 years ago
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…
Cancel
Save