You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rappli/src/components/Settings/Overlay.tsx

1081 lines
38 KiB
TypeScript

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,
NumberInput,
Select,
TextArea,
TextInput,
UnixDateInput,
} from "../Form";
import { autoAnimate } from "~/directives/autoAnimate";
import {
ADDRESS_LAYOUT_LEFT,
ADDRESS_LAYOUT_RIGHT,
CURRENT_VERSION,
LocalStoreContext,
localStoreSchema,
migrateInfoLog,
migrateLocalState,
migrateState,
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 DeleteIcon from "~icons/carbon/trash-can";
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="text"
label={isStructured() ? "Nummer" : "Linie 2"}
maxLength={isStructured() ? 16 : 70}
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);
});
const AccordionItemLabel: FlowComponent = (props) => (
<span class="overflow-hidden whitespace-nowrap hidden xxl:inline-flex group-hover:inline-flex group-focus-within:inline-flex gap-2">
{props.children}
</span>
);
const AccordionContent: FlowComponent = (props) => (
<div class="min-w-[300px]">{props.children}</div>
);
let logoInputEl: HTMLInputElement = undefined!;
return (
<>
<div class="group print:hidden hidden lg:grid bg-white z-50 h-full fixed xxl:h-auto xxl:relative left-0 xxl:w-[480px] grid-rows-[1fr_auto] gap-4 p-1 xxl:p-3 transition-all shadow hover:shadow-2xl focus-within:shadow-2xl outline outline-1 outline-slate-700/10 w-24 hover:w-[480px] focus-within:w-[480px]">
<div class="overflow-y-scroll">
<AccordionItem
item={0}
activeTitleColor="text-violet-600"
label={
<>
<ProjectIcon />
<AccordionItemLabel>Dokument</AccordionItemLabel>
<Show when={!documentDataForm.valid}>
<AccordionItemLabel>
<WarningIcon class="text-error" />
</AccordionItemLabel>
</Show>
</>
}
>
{/* TODO: Add option for item price decimals */}
<AccordionContent>
<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">
<Select
label="Typ neuer Positionen"
labelMinWidth={fullWidthLabelWidth}
title="Neue Positionen werden mit diesem Typ erzeugt."
value={state.defaultPositionType}
options={[
[POSITION_TYPE_QUANTITY, "Menge"],
[POSITION_TYPE_AGILE, "Agile"],
]}
onChange={(v) =>
setState("defaultPositionType", v as any)
}
/>
</div>
<div class="col-span-2">
<NumberInput
required
label="Standard Einzelpreis"
labelMinWidth={fullWidthLabelWidth}
value={state.defaultItemPrice}
onInput={(v) =>
v != null && setState("defaultItemPrice", v)
}
onBlur={resetInput(0)}
/>
</div>
<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>
</AccordionContent>
</AccordionItem>
<AccordionItem
item={1}
activeTitleColor="text-cyan-500"
label={
<>
<YouIcon />
<AccordionItemLabel>Deine Angaben</AccordionItemLabel>
<Show when={!yourDataForm.valid}>
<AccordionItemLabel>
<WarningIcon class="text-error" />
</AccordionItemLabel>
</Show>
</>
}
>
<AccordionContent>
<YourDataValidationContext>
<AccordionItemDivider>Bank Verbindung</AccordionItemDivider>
<AccordionItemGrid>
<div class="col-span-2">
<TextInput
required
label="Iban"
maxLength={50}
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>
Abweichende 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">
<NumberInput
required
label="MwST-Satz"
suffix="%"
value={new Big(localState.vatRate).mul(100).toNumber()}
onInput={(v) => {
v != null &&
setLocalState(
"vatRate",
new Big(v).div(100).toNumber()
);
}}
onBlur={resetInput(0)}
/>
</div>
</AccordionItemGrid>
<AccordionItemEnd />
</YourDataValidationContext>
</AccordionContent>
</AccordionItem>
<AccordionItem
item={2}
activeTitleColor="text-emerald-500"
label={
<>
<CustomerIcon />
<AccordionItemLabel>Kunde</AccordionItemLabel>
<Show when={!customerDataForm.valid}>
<AccordionItemLabel>
<WarningIcon class="text-error" />
</AccordionItemLabel>
</Show>
</>
}
>
<AccordionContent>
<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>
</AccordionContent>
</AccordionItem>
<AccordionItem
item={3}
activeTitleColor="text-orange-400"
label={
<>
<PositionsIcon />
<AccordionItemLabel>
Positionen <small>({state.positions.length})</small>
</AccordionItemLabel>
</>
}
>
<AccordionContent>
<PositionsSettings />
</AccordionContent>
</AccordionItem>
<AccordionItem
activeTitleColor="text-blue-500"
label={
<>
<DesignIcon />
<AccordionItemLabel>Design</AccordionItemLabel>
</>
}
item={4}
>
<AccordionContent>
<AccordionItemDivider>Corporate Design</AccordionItemDivider>
<AccordionItemGrid>
<div class="col-span-2">
<Select
label="Adress-Layout"
labelMinWidth={fullWidthLabelWidth}
options={[
[ADDRESS_LAYOUT_LEFT, "Adresse links"],
[ADDRESS_LAYOUT_RIGHT, "Adresse rechts"],
]}
value={localState.addressLayout}
onChange={(v) => setLocalState("addressLayout", v)}
/>
</div>
<div class="col-span-2">
<TextInput
ref={logoInputEl}
label="Logo"
labelMinWidth={fullWidthLabelWidth}
type="file"
class="file-input"
accept="image/png, image/jpeg, image/svg+xml"
suffix={
localState.logo ? (
<button
type="button"
class="btn btn-xs btn-error !rounded"
onClick={(e) => {
e.preventDefault();
setLocalState("logo", undefined);
logoInputEl.value = "";
}}
>
<DeleteIcon />
</button>
) : undefined
}
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>
<AccordionItemDivider>Sonstiges</AccordionItemDivider>
<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>
</AccordionContent>
</AccordionItem>
</div>
<div class="overflow-hidden transition-opacity opacity-90 group-hover:opacity-100 group-focus-within:opacity-100 xxl:opacity-100">
<AccordionContent>
<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;
}
const schema = z
.object({
state: storeSchema,
localState: localStoreSchema,
})
.collectErrors();
try {
let migrated = false;
const localStateVersion =
contentObject?.localState?.version;
const stateVersion = contentObject?.state?.version;
if (localStateVersion !== CURRENT_VERSION) {
migrateLocalState(contentObject?.localState);
migrated = true;
}
if (stateVersion !== CURRENT_VERSION) {
migrateState(contentObject?.state);
migrated = true;
}
if (migrated) {
migrateInfoLog(
localStateVersion < stateVersion
? localStateVersion
: stateVersion,
CURRENT_VERSION
);
}
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) {
setUiState("lastSaved", getUnixTime(new Date()));
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>
</AccordionContent>
</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;