import {
Component ,
createMemo ,
JSX ,
Show ,
FlowComponent ,
mergeProps ,
ParentComponent ,
createSignal ,
onMount ,
} from "solid-js" ;
import SwissInvoice , { InvoiceData } from "~/components/SwissInvoice" ;
import { Meta , Style , Title } from "solid-meta" ;
import Page from "~/components/Page" ;
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 {
ADDRESS_LAYOUT_LEFT ,
ADDRESS_LAYOUT_RIGHT ,
createLocalStore ,
createStore ,
createUiStore ,
LocalStoreContext ,
printTypeTitles ,
PRINT_TYPE_CONFIRMATION ,
PRINT_TYPE_INVOICE ,
PRINT_TYPE_OFFER ,
StoreContext ,
UiStoreContext ,
} from "~/stores" ;
import Big from "big.js" ;
import { getDisplayDateFromUnix , getHost , roundToStep } from "~/util" ;
import WelcomeModal , { description } from "~/components/WelcomeModal" ;
import Markdown from "~/components/Markdown" ;
// TODO: This should be a direct import of "shareon/css", but "shareon/css" doesnt work yet because solid-start / vite do not properly detect that as css
import "../shareon.css" ;
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 ;
onMount ( function ( ) {
! import . meta . env . SSR && import ( "shareon" ) . then ( ( m ) = > m . init ( ) ) ;
} ) ;
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 = { {
"mx-16" : true ,
"print:mx-0" : ! state . fullWidthInvoice ,
"print:mx-[11mm]" : 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 > 0
? ` ${ state . project . projectNumber } - `
: "" ) +
printTypeTitles [ uiState . printType ]
) ;
const externalTitle = "Räppli - Web App für Schweizerische Rechnungen" ;
return (
< div class = "break-words whitespace-normal" >
< Title > { import . meta . env . SSR ? externalTitle : titleMemo ( ) } < / Title >
< h1 class = "hidden" > { externalTitle } < / h1 >
< Meta name = "description" content = { description } / >
< Meta property = "og:description" content = { description } / >
< Meta property = "og:image" content = { ` ${ getHost ( ) } /social.png ` } / >
< Meta property = "og:title" content = { externalTitle } / >
< Meta property = "og:url" content = { getHost ( ) } / >
< Meta name = "twitter:card" content = "summary_large_image" / >
< Meta name = "twitter:site" content = "@katy_wings" / >
< div
classList = { {
"flex justify-start items-center print:mb-7 print:h-28" : true ,
"h-20 mb-7" : ! ! localState . logo ,
"justify-start" : localState . addressLayout == ADDRESS_LAYOUT_RIGHT ,
"justify-end" : localState . addressLayout == ADDRESS_LAYOUT_LEFT ,
} }
>
< Show when = { localState . logo } >
< 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 }
/ >
< / Show >
< / div >
< div class = "print:min-h-[12rem] mb-10" >
< div class = "grid grid-cols-2 gap-x-[15%]" >
< div
classList = { {
"order-last" : localState . addressLayout == ADDRESS_LAYOUT_RIGHT ,
} }
>
< div class = "text-sm mb-3" >
{ address ( ) . name
? [ address ( ) . name , getLine1 ( address ( ) ) , getLine2 ( address ( ) ) ]
. filter ( ( x ) = > x != "" )
. join ( " · " )
: "" }
< / div >
< div class = "text-lg leading-snug" >
< div > { customerAddress ( ) . name } < / div >
< div > { getLine1 ( customerAddress ( ) ) } < / div >
< div > { getLine2 ( customerAddress ( ) ) } < / div >
< / div >
< / 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 >
< / 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-10" 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 leading-none 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 : 25mm $ { state . fullWidthInvoice ? 0 : 11 } mm 11 mm $ {
state . fullWidthInvoice ? 0 : 11
} mm ;
}
. swissinvoice {
font - family : Liberation Sans , Helvetica , Arial , sans - serif ;
}
@media print {
html {
font - size : 11px ;
}
}
` }</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 >
< div
classList = { {
"mx-16 print:mx-0" : true ,
"mx-0" : state . fullWidthInvoice ,
} }
>
< SwissInvoice value = { invoiceData ( ) } / >
< / div >
< / 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 print:block 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 >
) ;
}