import {
batch ,
Component ,
FlowComponent ,
For ,
Show ,
useContext ,
JSX ,
startTransition ,
createMemo ,
onMount ,
onCleanup ,
} from "solid-js" ;
import { createStore , reconcile , unwrap } from "solid-js/store" ;
import { format , fromUnixTime , getUnixTime } from "date-fns" ;
import z from "myzod" ;
import Big from "big.js" ;
import { generate } from "node-iso11649" ;
import { customAlphabet } from "nanoid" ;
import createAccordion from "../Accordion" ;
import { Checkbox , TextArea , TextInput , UnixDateInput } from "../Form" ;
import { autoAnimate } from "~/directives/autoAnimate" ;
import {
LocalStoreContext ,
localStoreSchema ,
POSITION_TYPE_AGILE ,
POSITION_TYPE_QUANTITY ,
PrintType ,
printTypeTitles ,
PRINT_TYPE_CONFIRMATION ,
PRINT_TYPE_INVOICE ,
PRINT_TYPE_OFFER ,
StoreContext ,
storeSchema ,
UiStoreContext ,
} from "~/stores" ;
import { AddressData , isStructuredAddress } from "../Address" ;
import PositionsIcon from "~icons/carbon/show-data-cards" ;
import YouIcon from "~icons/carbon/face-wink" ;
import DesignIcon from "~icons/carbon/paint-brush" ;
import PrinterIcon from "~icons/carbon/printer" ;
import ProjectIcon from "~icons/carbon/product" ;
import DownloadIcon from "~icons/carbon/download" ;
import LoadIcon from "~icons/carbon/folder" ;
import LoadingSpinnerIcon from "~icons/icomoon-free/spinner9" ;
import ErrorIcon from "~icons/carbon/error" ;
import SuccessIcon from "~icons/carbon/checkmark-filled" ;
import CustomerIcon from "~icons/carbon/friendship" ;
import WarningIcon from "~icons/carbon/warning-alt-filled" ;
import GenerateIcon from "~icons/carbon/chemistry" ;
import { saveFile , selectLocalFiles , uploadFile } from "~/client/filesystem" ;
import { resetInput , sleep } from "~/util" ;
import { PositionsSettings } from "./Positions" ;
import Modal , { ModalCloseButton } from "../Modal" ;
import { createValidation } from "~/hooks/validation" ;
import { MarkdownHelpLabel } from "../Markdown" ;
const AccordionItemGrid : FlowComponent = ( props ) = > {
return (
< div class = "grid grid-cols-2 gap-3 gap-x-1 pb-3" > { props . children } < / div >
) ;
} ;
const AccordionItemEnd : Component = ( ) = > {
return < div class = "h-1" / > ;
} ;
const AccordionItemDivider : FlowComponent = ( props ) = > {
return < div class = "divider" > { props . children } < / div > ;
} ;
const SettingsOverlay : Component = ( ) = > {
const [ state , setState ] = useContext ( StoreContext ) ! ;
const [ localState , setLocalState ] = useContext ( LocalStoreContext ) ! ;
const [ loadModal , setLoadModal ] = createStore ( {
open : false ,
loading : false ,
errors : null as null | {
message? : JSX.Element ;
parseErrors : { path : string ; message : string } [ ] ;
} ,
} ) ;
const [ uiState , setUiState ] = useContext ( UiStoreContext ) ! ;
const [ AccordionItem ] = createAccordion ( null ) ;
autoAnimate ;
const [ DocumentValidationContext , documentDataForm ] = createValidation ( ) ;
const [ YourDataValidationContext , yourDataForm ] = createValidation ( ) ;
const [ CustomerValidationContext , customerDataForm ] = createValidation ( ) ;
const AddressInputs : Component < {
namePrefix? : string ;
nameRequired? : boolean ;
setter : ( name : string , value : any ) = > void ;
address : ( ) = > AddressData ;
} > = ( props ) = > {
const isStructured = createMemo ( ( ) = > isStructuredAddress ( props . address ( ) ) ) ;
const withPrefix = ( name : string ) = >
createMemo (
( ) = > ` ${ props . namePrefix && props . namePrefix + "_" } ${ name } `
) ( ) ;
return (
< >
< div class = "col-span-2" >
< TextInput
name = { withPrefix ( "name" ) }
label = "Name"
maxLength = { 70 }
required = { props . nameRequired }
value = { props . address ( ) . name }
onInput = { ( evt ) = > props . setter ( "name" , evt . currentTarget . value ) }
/ >
< / div >
< Show when = { props . nameRequired || props . address ( ) . name } >
< TextInput
name = { withPrefix ( "line1" ) }
label = { isStructured ( ) ? "Strasse" : "Linie 1" }
maxLength = { 70 }
required
value = { props . address ( ) . line1 }
onInput = { ( evt ) = > props . setter ( "line1" , evt . currentTarget . value ) }
/ >
< TextInput
name = { withPrefix ( "line2" ) }
type = { isStructured ( ) ? "number" : "text" }
label = { isStructured ( ) ? "Nummer" : "Linie 2" }
maxLength = { isStructured ( ) ? undefined : 70 }
max = { isStructured ( ) ? 9999999999999999 : undefined }
min = { isStructured ( ) ? 0 : undefined }
required
value = { props . address ( ) . line2 }
onInput = { ( evt ) = > props . setter ( "line2" , evt . currentTarget . value ) }
/ >
< TextInput
name = { withPrefix ( "zip" ) }
label = "Plz"
type = "number"
maxLength = { 16 }
min = "0"
value = { props . address ( ) . zip }
onInput = { ( evt ) = >
props . setter (
"zip" ,
parseInt ( evt . currentTarget . value ) || undefined
)
}
/ >
< TextInput
name = { withPrefix ( "city" ) }
label = "Ort"
value = { props . address ( ) . city }
onInput = { ( evt ) = > props . setter ( "city" , evt . currentTarget . value ) }
/ >
< / Show >
< / >
) ;
} ;
const createCustomerAddressSetter = ( alternative = false ) = > {
const addressField = alternative ? "alternativeAddress" : "debtorAddress" ;
return ( name : any , value : any ) = > {
setState ( "customer" , addressField , name , value ) ;
} ;
} ;
const contactSetter = ( name : any , value : any ) = > {
setLocalState ( "contact" , name , value ) ;
} ;
const fullWidthLabelWidth = "50%" ;
const FullWidthAccordionInput : Component < Parameters < typeof TextInput > [ 0 ] > = (
props
) = > (
< div class = "col-span-2" >
< TextInput labelMinWidth = { fullWidthLabelWidth } { ...props } / >
< / div >
) ;
const saveProject = ( ) = > {
const fileContent = JSON . stringify (
{
state : unwrap ( state ) ,
localState : unwrap ( localState ) ,
} ,
null ,
" "
) ;
saveFile (
` rappli- ${
state . project . projectNumber . length
? state . project . projectNumber . replaceAll ( " " , "-" ) + "-"
: ""
} $ { format ( fromUnixTime ( state . project . date ) , "yyyy-MM-dd" ) } . json ` ,
"application/json" ,
fileContent
) ;
setUiState ( "lastSaved" , getUnixTime ( new Date ( ) ) ) ;
} ;
const saveOnCtrlS = ( e : KeyboardEvent ) = > {
if ( e . ctrlKey && e . key === "s" ) {
e . preventDefault ( ) ;
saveProject ( ) ;
}
} ;
onMount ( function ( ) {
document . addEventListener ( "keydown" , saveOnCtrlS ) ;
} ) ;
onCleanup ( function ( ) {
document . removeEventListener ( "keydown" , saveOnCtrlS ) ;
} ) ;
return (
< >
< div class = "print:hidden hidden xl:grid bg-white z-50 h-full fixed xxl:h-auto xxl:relative left-0 w-[480px] grid-rows-[1fr_auto] gap-4 p-3 transition-all shadow-none hover:shadow-2xl focus-within:shadow-2xl -translate-x-72 opacity-70 hover:opacity-100 focus-within:opacity-100 xxl:opacity-100 xxl:-translate-x-0 hover:translate-x-0 focus-within:translate-x-0 " >
< div class = "overflow-y-scroll" >
< AccordionItem
item = { 0 }
activeTitleColor = "text-violet-600"
label = {
< >
< ProjectIcon / >
Dokument
< Show when = { ! documentDataForm . valid } >
< WarningIcon class = "text-error" / >
< / Show >
< / >
}
>
{ /* TODO: Add option for item price decimals */ }
< DocumentValidationContext >
< AccordionItemGrid >
< FullWidthAccordionInput
label = "Projekt Nr."
value = { state . project . projectNumber }
onInput = { ( evt ) = >
setState (
"project" ,
"projectNumber" ,
evt . currentTarget . value
)
}
/ >
< FullWidthAccordionInput
label = "Bestellungs Nr."
value = { state . project . orderNumber }
onInput = { ( evt ) = >
setState ( "project" , "orderNumber" , evt . currentTarget . value )
}
/ >
< div class = "col-span-2" >
< UnixDateInput
required
labelMinWidth = { fullWidthLabelWidth }
label = "Datum"
value = { state . project . date }
onInput = { ( v : any ) = > setState ( "project" , "date" , v ) }
/ >
< / div >
< FullWidthAccordionInput
label = "Lieferungs Nr."
value = { state . project . deliveryNumber }
onInput = { ( evt ) = >
setState (
"project" ,
"deliveryNumber" ,
evt . currentTarget . value
)
}
/ >
< div class = "col-span-2" >
< UnixDateInput
labelMinWidth = { fullWidthLabelWidth }
label = "Lieferdatum"
value = { state . project . deliveryDate }
onInput = { ( v : any ) = > setState ( "project" , "deliveryDate" , v ) }
/ >
< / div >
< div class = "col-span-2" >
< div class = "input-group input-group-sm" >
< span style = { { "min-width" : fullWidthLabelWidth } } >
Typ neuer Positionen
< / span >
< select
title = "Neue Positionen werden mit diesem Typen erzeugt."
class = "flex-1 select select-sm select-bordered"
onChange = { ( e ) = >
setState (
"defaultPositionType" ,
e . currentTarget . value as any
)
}
>
< For
each = { [
[ POSITION_TYPE_QUANTITY , "Menge" ] ,
[ POSITION_TYPE_AGILE , "Agile" ] ,
] }
>
{ ( [ type , label ] ) = > (
< option
selected = { type === state . defaultPositionType }
value = { type }
>
{ label }
< / option >
) }
< / For >
< / select >
< / div >
< / div >
< FullWidthAccordionInput
required
type = "number"
label = "Standard Einzelpreis"
value = { state . defaultItemPrice }
onInput = { ( evt ) = >
setState (
"defaultItemPrice" ,
parseFloat ( evt . currentTarget . value ) || 0
)
}
/ >
< div class = "col-span-2" >
< TextArea
label = "Einleitung"
labelSuffixJsx = { < MarkdownHelpLabel / > }
value = { state . project . preface }
onInput = { ( evt ) = > {
setState ( "project" , "preface" , evt . currentTarget . value ) ;
} }
/ >
< / div >
< div class = "col-span-2" >
< TextArea
label = "Schlussbemerkung"
labelSuffixJsx = { < MarkdownHelpLabel / > }
value = { state . project . conclusion }
onInput = { ( evt ) = >
setState ( "project" , "conclusion" , evt . currentTarget . value )
}
/ >
< / div >
< / AccordionItemGrid >
< AccordionItemEnd / >
< AccordionItemDivider > QR Rechnung < / AccordionItemDivider >
< AccordionItemGrid >
< FullWidthAccordionInput
vertical = { true }
label = "Referenz"
value = { state . invoice . reference }
onInput = { ( evt ) = >
setState ( "invoice" , "reference" , evt . currentTarget . value )
}
/ >
< div class = "col-span-2" >
< button
class = "btn btn-xs btn-block btn-accent gap-2"
onClick = { ( ) = > {
let value = state . invoice . reference ;
if ( value . startsWith ( "RF" ) ) {
return ;
}
if ( value === "" ) {
value = customAlphabet ( "1234567890" , 21 ) ( ) + "" ;
}
setState ( "invoice" , "reference" , generate ( value ) ) ;
} }
>
< GenerateIcon / > Kreditor Referenz generieren
< / button >
< / div >
< div class = "col-span-2" >
< TextInput
vertical = { true }
maxLength = { 140 }
label = "Zusätzliche Informationen"
value = { state . invoice . message }
onInput = { ( evt ) = >
setState ( "invoice" , "message" , evt . currentTarget . value )
}
/ >
< / div >
< / AccordionItemGrid >
< AccordionItemEnd / >
< AccordionItemDivider > Agile < / AccordionItemDivider >
< AccordionItemGrid >
< div class = "col-span-2" >
< TextInput
required
title = {
'Agile Positionen werden mit Story Points "geschätzt". Was ist der kleinst mögliche Aufwand einer einzelnen Agilen Position?'
}
type = "number"
label = "Stunden pro Story Point"
onInput = { ( evt ) = >
evt . currentTarget . value !== "" &&
setState (
"agileHoursPerStoryPoint" ,
parseFloat ( evt . currentTarget . value )
)
}
onBlur = { resetInput ( 0 ) }
value = { state . agileHoursPerStoryPoint }
/ >
< / div >
< div class = "col-span-2" >
< div class = "form-control" >
< label class = "label gap-8" >
< span class = "label-text font-bold" > Risiko Faktor < / span >
< div class = "flex-1" >
< input
class = "range range-xs hover:range-accent"
required
type = "range"
title = "Wie hoch ist das Risiko, dass etwas schief geht? Dieser Wert dient zur Gewichtung des Mittelwerts zwischen den minimalen und maximalen Story Points pro Position. Bei einem Risiko von 0% werden nur die minimalen Story Points der Positionen berücksichtigt, bei einem Risiko von 100% nur die Maximalen. (0 - 100%)"
value = { state . agileRiskFactor }
min = "0.0"
max = "1.0"
step = "0.1"
onInput = { ( evt ) = >
evt . currentTarget . value !== "" &&
setState (
"agileRiskFactor" ,
parseFloat ( evt . currentTarget . value )
)
}
onBlur = { resetInput ( 0 ) }
/ >
< div class = "w-full flex justify-between text-xs px-2 relative" >
< span >
< div class = "absolute left-0" > Klein < / div > & nbsp ;
< / span >
< span > | < / span >
< span > | < / span >
< span > | < / span >
< span > | < / span >
< span > | < / span >
< span > | < / span >
< span > | < / span >
< span > | < / span >
< span > | < / span >
< span >
< div class = "absolute text-right right-0" > Gross < / div >
& nbsp ;
< / span >
< / div >
< / div >
< / label >
< / div >
< / div >
< / AccordionItemGrid >
< / DocumentValidationContext >
< / AccordionItem >
< AccordionItem
item = { 1 }
activeTitleColor = "text-cyan-500"
label = {
< >
< YouIcon / >
Deine Angaben
< Show when = { ! yourDataForm . valid } >
< WarningIcon class = "text-error" / >
< / Show >
< / >
}
>
< YourDataValidationContext >
< AccordionItemDivider > Bank Verbindung < / AccordionItemDivider >
< AccordionItemGrid >
< div class = "col-span-2" >
< TextInput
required
label = "Iban"
value = { localState . iban }
onInput = { ( evt ) = >
setLocalState ( "iban" , evt . currentTarget . value )
}
/ >
< / div >
< AddressInputs
namePrefix = "creditor"
nameRequired = { true }
setter = { ( name , value ) = > {
setLocalState ( "creditor" , name as any , value ) ;
} }
address = { ( ) = > localState . creditor }
/ >
< / AccordionItemGrid >
< AccordionItemEnd / >
< AccordionItemDivider >
Abweichene Adresse { " " }
< input
class = "checkbox"
type = "checkbox"
checked = { localState . useCustomAddress }
onChange = { ( evt ) = >
setLocalState ( "useCustomAddress" , evt . currentTarget . checked )
}
/ >
< / AccordionItemDivider >
< div use : autoAnimate >
< Show when = { localState . useCustomAddress } >
< AccordionItemGrid >
< AddressInputs
namePrefix = "customAddress"
nameRequired = { true }
setter = { ( name , value ) = > {
setLocalState ( "customAddress" , name as any , value ) ;
} }
address = { ( ) = > localState . customAddress }
/ >
< / AccordionItemGrid >
< / Show >
< / div >
< AccordionItemEnd / >
< AccordionItemDivider > Ansprechpartner < / AccordionItemDivider >
< AccordionItemGrid >
< div class = "col-span-2" >
< TextInput
label = "Name"
value = { localState . contact ? . name }
onInput = { ( evt ) = >
contactSetter ( "name" , evt . currentTarget . value )
}
/ >
< / div >
< TextInput
label = "Telefon"
value = { localState . contact ? . phone }
onInput = { ( evt ) = >
contactSetter ( "phone" , evt . currentTarget . value )
}
/ >
< TextInput
label = "E-Mail"
value = { localState . contact ? . email }
type = "email"
onInput = { ( evt ) = >
contactSetter ( "email" , evt . currentTarget . value )
}
/ >
< / AccordionItemGrid >
< AccordionItemEnd / >
< AccordionItemDivider > Andere Angaben < / AccordionItemDivider >
< AccordionItemGrid >
< FullWidthAccordionInput
vertical = { true }
label = "Zahlungsbedingungen"
value = { localState . paymentTerms }
onInput = { ( evt ) = >
setLocalState ( "paymentTerms" , evt . currentTarget . value )
}
/ >
< div class = "col-span-2" >
< TextInput
label = "MwST-Nr."
value = { localState . vatNumber }
onInput = { ( evt ) = >
setLocalState ( "vatNumber" , evt . currentTarget . value )
}
/ >
< / div >
< div class = "col-span-2" >
< TextInput
required
label = "MwST-Satz"
type = "number"
step = "0.1"
suffix = "%"
value = { new Big ( localState . vatRate ) . mul ( 100 ) . toNumber ( ) }
onInput = { ( evt ) = > {
evt . currentTarget . value !== "" &&
setLocalState (
"vatRate" ,
new Big ( evt . currentTarget . value ) . div ( 100 ) . toNumber ( )
) ;
} }
onBlur = { resetInput ( 0 ) }
/ >
< / div >
< / AccordionItemGrid >
< AccordionItemEnd / >
< AccordionItemDivider > Corporate Design < / AccordionItemDivider >
< AccordionItemGrid >
< div class = "col-span-2" >
< TextInput
label = "Logo"
type = "file"
class = "file-input"
accept = "image/png, image/jpeg, image/svg+xml"
onInput = { async ( evt ) = > {
if ( ! evt . currentTarget . files ) {
return ;
}
const file = evt . currentTarget . files [ 0 ] ;
if ( ! file ) {
return ;
}
const content = await uploadFile ( file , "dataUrl" ) ;
if ( ! content ) {
return ;
}
const image = document . createElement ( "img" ) ;
image . src = content ;
image . onload = function ( ) {
setLocalState ( "logo" , {
width : image.width ,
height : image.height ,
type : file . type ,
url : content ,
} ) ;
} ;
} }
/ >
< / div >
< / AccordionItemGrid >
< / YourDataValidationContext >
< / AccordionItem >
< AccordionItem
item = { 2 }
activeTitleColor = "text-emerald-500"
label = {
< >
< CustomerIcon / > Kunde
< Show when = { ! customerDataForm . valid } >
< WarningIcon class = "text-error" / >
< / Show >
< / >
}
>
< CustomerValidationContext >
< AccordionItemDivider > Bank Verbindung < / AccordionItemDivider >
< AccordionItemGrid >
< AddressInputs
namePrefix = "debtor"
setter = { createCustomerAddressSetter ( ) }
address = { ( ) = > state . customer . debtorAddress }
/ >
< / AccordionItemGrid >
< AccordionItemEnd / >
< AccordionItemDivider >
Abweichende Adresse { " " }
< input
class = "checkbox"
type = "checkbox"
checked = { state . useCustomerAlternativeAddress }
onChange = { ( evt ) = >
setState (
"useCustomerAlternativeAddress" ,
evt . currentTarget . checked
)
}
/ >
< / AccordionItemDivider >
< div use : autoAnimate >
< Show when = { state . useCustomerAlternativeAddress } >
< AccordionItemGrid >
< AddressInputs
setter = { createCustomerAddressSetter ( true ) }
address = { ( ) = > state . customer . alternativeAddress }
/ >
< / AccordionItemGrid >
< / Show >
< / div >
< AccordionItemEnd / >
< AccordionItemDivider > Andere Angaben < / AccordionItemDivider >
< AccordionItemGrid >
< FullWidthAccordionInput
label = "Kunden Nr."
value = { state . customer . customerNumber }
onInput = { ( evt ) = >
setState (
"customer" ,
"customerNumber" ,
evt . currentTarget . value
)
}
/ >
< FullWidthAccordionInput
label = "MwST-Nr."
value = { state . customer . vatNumber }
onInput = { ( evt ) = >
setState ( "customer" , "vatNumber" , evt . currentTarget . value )
}
/ >
< / AccordionItemGrid >
< / CustomerValidationContext >
< / AccordionItem >
< AccordionItem
item = { 3 }
activeTitleColor = "text-orange-400"
label = {
< >
< PositionsIcon / >
Positionen
< small > ( { state . positions . length } ) < / small >
< / >
}
>
< PositionsSettings / >
< / AccordionItem >
< AccordionItem
activeTitleColor = "text-blue-500"
label = {
< >
< DesignIcon / >
Design
< / >
}
item = { 4 }
>
< AccordionItemGrid >
< div class = "col-span-2" >
< Checkbox
checked = { localState . showLufraiWatermark }
onChange = { ( evt ) = >
setLocalState (
"showLufraiWatermark" ,
evt . currentTarget . checked
)
}
>
Lufrai Wasserzeichen anzeigen
< / Checkbox >
< / div >
< div class = "col-span-2" >
< Checkbox
checked = { state . fullWidthInvoice }
onChange = { ( evt ) = >
setState ( "fullWidthInvoice" , evt . currentTarget . checked )
}
>
Abstandlose QR Rechnung
< / Checkbox >
< / div >
< / AccordionItemGrid >
< / AccordionItem >
< / div >
< div >
< div class = "grid grid-cols-2 gap-2 mb-4" >
< button
class = "btn btn-sm btn-accent shadow-md gap-2"
onClick = { async ( ) = > {
const files = await selectLocalFiles ( [
".json" ,
"application/json" ,
] ) ;
if ( ! files [ 0 ] ) {
return ;
}
setLoadModal ( "loading" , true ) ;
setLoadModal ( "errors" , null ) ;
await sleep ( 200 ) ;
setLoadModal ( "open" , true ) ;
const load = async ( ) = > {
const results = await Promise . all ( [
uploadFile ( files [ 0 ] ) ,
sleep ( 600 ) ,
] ) ;
const content = results [ 0 ] ;
if ( ! content ) {
setLoadModal ( "errors" , {
message :
"Das Dokument ist leer! Bitte lade ein korrektes Dokument hoch." ,
} ) ;
return ;
}
let contentObject : any ;
try {
contentObject = JSON . parse ( content ) ;
} catch ( err ) {
setLoadModal ( "errors" , {
message : "Das Dokument hat kein gültiges Format." ,
} ) ;
return ;
}
// TODO: Run migrations
const schema = z
. object ( {
state : storeSchema ,
localState : localStoreSchema ,
} )
. collectErrors ( ) ;
try {
schema . parse ( contentObject ) ;
} catch ( e : any ) {
const message = "Das Dokument hat kein gültiges Format." ;
let parseErrors = [ { path : "." , message : e.message } ] ;
if ( e . collectedErrors ) {
parseErrors = Object . values ( e . collectedErrors ) . map (
( error : any ) = > {
return {
path : error.path.join ( "." ) ,
message : error.message ,
} ;
}
) ;
}
setLoadModal ( "errors" , {
message ,
parseErrors ,
} ) ;
return ;
}
await startTransition ( function ( ) {
batch ( ( ) = > {
setState ( reconcile ( contentObject . state ) ) ;
setLocalState ( reconcile ( contentObject . localState ) ) ;
} ) ;
} ) ;
} ;
const results = await Promise . all ( [ sleep ( 1200 ) , load ( ) ] ) ;
setLoadModal ( "loading" , false ) ;
if ( ! loadModal . errors ) {
await sleep ( 1100 ) ;
setLoadModal ( "open" , false ) ;
}
} }
>
< LoadIcon / > Laden
< / button >
< button
class = "btn btn-sm btn-accent shadow-md gap-2"
onClick = { saveProject }
>
< DownloadIcon / > Speichern
< / button >
< / div >
< div class = "form-control w-full" >
< div class = "input-group input-group-sm" >
< select
class = "w-1/2 select select-sm select-bordered shadow"
onChange = { ( evt ) = > {
setUiState ( "printType" , evt . currentTarget . value as any ) ;
evt . currentTarget . blur ( ) ;
} }
>
< For
each = {
[
PRINT_TYPE_OFFER ,
PRINT_TYPE_CONFIRMATION ,
PRINT_TYPE_INVOICE ,
] as PrintType [ ]
}
>
{ ( type ) = > (
< option value = { type } selected = { type === uiState . printType } >
{ printTypeTitles [ type ] }
< / option >
) }
< / For >
< / select >
< button
class = "btn btn-sm btn-primary flex-1 gap-2 shadow-md"
onClick = { ( ) = > window . print ( ) }
>
< PrinterIcon / > Drucken
< / button >
< / div >
< / div >
< / div >
< / div >
< Show when = { true } >
< Modal open = { loadModal . open } >
< Show when = { ! loadModal . loading && loadModal . errors } >
< ModalCloseButton
onClick = { ( ) = > {
setLoadModal ( "open" , false ) ;
} }
/ >
< / Show >
< div use : autoAnimate class = "flex flex-col items-center" >
< Show when = { ! loadModal . loading } >
< Show when = { loadModal . errors } >
< div class = "flex items-center gap-2 text-xl text-error mb-10" >
< ErrorIcon / >
< div class = "font-black" >
Dokument konnte nicht geladen werden
< / div >
< / div >
< Show when = { loadModal . errors ? . message } >
< div class = "text-5xl font-light text-error-content text-center" >
{ loadModal . errors ? . message }
< / div >
< / Show >
< Show when = { loadModal . errors ? . parseErrors } >
< table class = "mt-6 w-full table table-compact" >
< thead >
< tr >
< th > < / th >
< th > JSON - Pfad < / th >
< th > Fehler < / th >
< / tr >
< / thead >
< tbody >
< For each = { loadModal . errors ? . parseErrors } >
{ ( error , idx ) = > (
< tr >
< th > { idx ( ) + 1 } < / th >
< td > { error . path } < / td >
< td > { error . message } < / td >
< / tr >
) }
< / For >
< / tbody >
< / table >
< / Show >
< button
class = "mt-8 btn"
onClick = { ( ) = > {
setLoadModal ( "open" , false ) ;
setLoadModal ( "errors" , null ) ;
} }
>
Schliessen
< / button >
< / Show >
< Show when = { ! loadModal . errors } >
< div class = "flex justify-center" >
< div class = "animate-pulse text-7xl text-success" >
< SuccessIcon / >
< / div >
< / div >
< / Show >
< / Show >
< Show when = { loadModal . loading } >
< div class = "flex justify-center" >
< div class = "animate-spin text-7xl text-primary" >
< LoadingSpinnerIcon / >
< / div >
< / div >
< / Show >
< / div >
< / Modal >
< / Show >
< / >
) ;
} ;
export default SettingsOverlay ;