aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/components/form
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-10-24 10:46:14 +0200
committerFlorian Dold <florian@dold.me>2022-10-24 10:46:14 +0200
commit3e060b80428943c6562250a6ff77eff10a0259b7 (patch)
treed08472bc5ca28621c62ac45b229207d8215a9ea7 /packages/merchant-backoffice-ui/src/components/form
parentfb52ced35ac872349b0e1062532313662552ff6c (diff)
downloadwallet-core-3e060b80428943c6562250a6ff77eff10a0259b7.tar.xz
repo: integrate packages from former merchant-backoffice.git
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components/form')
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx81
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/Input.tsx71
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputArray.tsx97
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx72
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx47
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDate.tsx159
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx172
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx66
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputImage.tsx95
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx43
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx42
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx39
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx392
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx139
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx55
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx119
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx86
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx162
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputStock.tsx171
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx97
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx77
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/TextField.tsx53
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/useField.tsx86
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx40
24 files changed, 2461 insertions, 0 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx
new file mode 100644
index 000000000..aef410ce7
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx
@@ -0,0 +1,81 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext, useMemo } from "preact/hooks";
+
+type Updater<S> = (value: ((prevState: S) => S) ) => void;
+
+export interface Props<T> {
+ object?: Partial<T>;
+ errors?: FormErrors<T>;
+ name?: string;
+ valueHandler: Updater<Partial<T>> | null;
+ children: ComponentChildren
+}
+
+const noUpdater: Updater<Partial<unknown>> = () => (s: unknown) => s
+
+export function FormProvider<T>({ object = {}, errors = {}, name = '', valueHandler, children }: Props<T>): VNode {
+ const initialObject = useMemo(() => object, []);
+ const value = useMemo<FormType<T>>(() => ({ errors, object, initialObject, valueHandler: valueHandler ? valueHandler : noUpdater, name, toStr: {}, fromStr: {} }), [errors, object, valueHandler]);
+
+ return <FormContext.Provider value={value}>
+ <form class="field" onSubmit={(e) => {
+ e.preventDefault();
+ // if (valueHandler) valueHandler(object);
+ }}>
+ {children}
+ </form>
+ </FormContext.Provider>;
+}
+
+export interface FormType<T> {
+ object: Partial<T>;
+ initialObject: Partial<T>;
+ errors: FormErrors<T>;
+ toStr: FormtoStr<T>;
+ name: string;
+ fromStr: FormfromStr<T>;
+ valueHandler: Updater<Partial<T>>;
+}
+
+const FormContext = createContext<FormType<unknown>>(null!)
+
+export function useFormContext<T>() {
+ return useContext<FormType<T>>(FormContext)
+}
+
+export type FormErrors<T> = {
+ [P in keyof T]?: string | FormErrors<T[P]>
+}
+
+export type FormtoStr<T> = {
+ [P in keyof T]?: ((f?: T[P]) => string)
+}
+
+export type FormfromStr<T> = {
+ [P in keyof T]?: ((f: string) => T[P])
+}
+
+export type FormUpdater<T> = {
+ [P in keyof T]?: (f: keyof T) => (v: T[P]) => void
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/Input.tsx b/packages/merchant-backoffice-ui/src/components/form/Input.tsx
new file mode 100644
index 000000000..9a9691e9b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/Input.tsx
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { ComponentChildren, h, VNode } from "preact";
+import { useField, InputProps } from "./useField";
+
+interface Props<T> extends InputProps<T> {
+ inputType?: 'text' | 'number' | 'multiline' | 'password';
+ expand?: boolean;
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+ inputExtra?: any,
+ side?: ComponentChildren;
+ children?: ComponentChildren;
+}
+
+const defaultToString = (f?: any): string => f || ''
+const defaultFromString = (v: string): any => v as any
+
+const TextInput = ({ inputType, error, ...rest }: any) => inputType === 'multiline' ?
+ <textarea {...rest} class={error ? "textarea is-danger" : "textarea"} rows="3" /> :
+ <input {...rest} class={error ? "input is-danger" : "input"} type={inputType} />;
+
+export function Input<T>({ name, readonly, placeholder, tooltip, label, expand, help, children, inputType, inputExtra, side, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+ return <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded has-icons-right" : "control has-icons-right"}>
+ <TextInput error={error} {...inputExtra}
+ inputType={inputType}
+ placeholder={placeholder} readonly={readonly}
+ name={String(name)} value={toStr(value)}
+ onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => onChange(fromStr(e.currentTarget.value))} />
+ {help}
+ {children}
+ { required && <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span> }
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {side}
+ </div>
+ </div>;
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx
new file mode 100644
index 000000000..984c6dc49
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx
@@ -0,0 +1,97 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Translate, useTranslator } from "../../i18n";
+import { InputProps, useField } from "./useField";
+
+export interface Props<T> extends InputProps<T> {
+ isValid?: (e: any) => boolean;
+ addonBefore?: string;
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+}
+
+const defaultToString = (f?: any): string => f || ''
+const defaultFromString = (v: string): any => v as any
+
+export function InputArray<T>({ name, readonly, placeholder, tooltip, label, help, addonBefore, isValid = () => true, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode {
+ const { error: formError, value, onChange, required } = useField<T>(name);
+ const [localError, setLocalError] = useState<string | null>(null)
+
+ const error = localError || formError
+
+ const array: any[] = (value ? value! : []) as any;
+ const [currentValue, setCurrentValue] = useState('');
+ const i18n = useTranslator();
+
+ return <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ {addonBefore && <div class="control">
+ <a class="button is-static">{addonBefore}</a>
+ </div>}
+ <p class="control is-expanded has-icons-right">
+ <input class={error ? "input is-danger" : "input"} type="text"
+ placeholder={placeholder} readonly={readonly} disabled={readonly}
+ name={String(name)} value={currentValue}
+ onChange={(e): void => setCurrentValue(e.currentTarget.value)} />
+ {required && <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>}
+ </p>
+ <p class="control">
+ <button class="button is-info has-tooltip-left" disabled={!currentValue} onClick={(): void => {
+ const v = fromStr(currentValue)
+ if (!isValid(v)) {
+ setLocalError(i18n`The value ${v} is invalid for a payment url`)
+ return;
+ }
+ setLocalError(null)
+ onChange([v, ...array] as any);
+ setCurrentValue('');
+ }} data-tooltip={i18n`add element to the list`}><Translate>add</Translate></button>
+ </p>
+ </div>
+ {help}
+ {error && <p class="help is-danger"> {error} </p>}
+ {array.map((v, i) => <div key={i} class="tags has-addons mt-3 mb-0">
+ <span class="tag is-medium is-info mb-0" style={{ maxWidth: '90%' }}>{v}</span>
+ <a class="tag is-medium is-danger is-delete mb-0" onClick={() => {
+ onChange(array.filter(f => f !== v) as any);
+ setCurrentValue(toStr(v));
+ }} />
+ </div>
+ )}
+ </div>
+
+ </div>
+ </div>;
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx b/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx
new file mode 100644
index 000000000..2771fe483
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx
@@ -0,0 +1,72 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField";
+
+interface Props<T> extends InputProps<T> {
+ name: T;
+ readonly?: boolean;
+ expand?: boolean;
+ threeState?: boolean;
+ toBoolean?: (v?: any) => boolean | undefined;
+ fromBoolean?: (s: boolean | undefined) => any;
+}
+
+const defaultToBoolean = (f?: any): boolean | undefined => f || ''
+const defaultFromBoolean = (v: boolean | undefined): any => v as any
+
+
+export function InputBoolean<T>({ name, readonly, placeholder, tooltip, label, help, threeState, expand, fromBoolean = defaultFromBoolean, toBoolean = defaultToBoolean }: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const onCheckboxClick = (): void => {
+ const c = toBoolean(value)
+ if (c === false && threeState) return onChange(undefined as any)
+ return onChange(fromBoolean(!c))
+ }
+
+ return <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ <label class="b-checkbox checkbox">
+ <input type="checkbox" class={toBoolean(value) === undefined ? "is-indeterminate" : ""}
+ checked={toBoolean(value)}
+ placeholder={placeholder} readonly={readonly}
+ name={String(name)} disabled={readonly}
+ onChange={onCheckboxClick} />
+ <span class="check" />
+ </label>
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>;
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
new file mode 100644
index 000000000..d3a46f483
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
@@ -0,0 +1,47 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { ComponentChildren, h } from "preact";
+import { useConfigContext } from "../../context/config";
+import { Amount } from "../../declaration";
+import { InputWithAddon } from "./InputWithAddon";
+import { InputProps } from "./useField";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ addonAfter?: ComponentChildren;
+ children?: ComponentChildren;
+ side?: ComponentChildren;
+}
+
+export function InputCurrency<T>({ name, readonly, label, placeholder, help, tooltip, expand, addonAfter, children, side }: Props<keyof T>) {
+ const config = useConfigContext()
+ return <InputWithAddon<T> name={name} readonly={readonly} addonBefore={config.currency}
+ side={side}
+ label={label} placeholder={placeholder} help={help} tooltip={tooltip}
+ addonAfter={addonAfter}
+ inputType='number' expand={expand}
+ toStr={(v?: Amount) => v?.split(':')[1] || ''}
+ fromStr={(v: string) => !v ? '' : `${config.currency}:${v}`}
+ inputExtra={{ min: 0 }}
+ children={children}
+ />
+}
+
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
new file mode 100644
index 000000000..77199527f
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
@@ -0,0 +1,159 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { format } from "date-fns";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Translate, useTranslator } from "../../i18n";
+import { DatePicker } from "../picker/DatePicker";
+import { InputProps, useField } from "./useField";
+
+export interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ //FIXME: create separated components InputDate and InputTimestamp
+ withTimestampSupport?: boolean;
+}
+
+export function InputDate<T>({
+ name,
+ readonly,
+ label,
+ placeholder,
+ help,
+ tooltip,
+ expand,
+ withTimestampSupport,
+}: Props<keyof T>): VNode {
+ const [opened, setOpened] = useState(false);
+ const i18n = useTranslator();
+
+ const { error, required, value, onChange } = useField<T>(name);
+
+ let strValue = "";
+ if (!value) {
+ strValue = withTimestampSupport ? "unknown" : "";
+ } else if (value instanceof Date) {
+ strValue = format(value, "yyyy/MM/dd");
+ } else if (value.t_s) {
+ strValue =
+ value.t_s === "never"
+ ? withTimestampSupport
+ ? "never"
+ : ""
+ : format(new Date(value.t_s * 1000), "yyyy/MM/dd");
+ }
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ <p
+ class={
+ expand
+ ? "control is-expanded has-icons-right"
+ : "control has-icons-right"
+ }
+ >
+ <input
+ class="input"
+ type="text"
+ readonly
+ value={strValue}
+ placeholder={placeholder}
+ onClick={() => {
+ if (!readonly) setOpened(true);
+ }}
+ />
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {help}
+ </p>
+ <div
+ class="control"
+ onClick={() => {
+ if (!readonly) setOpened(true);
+ }}
+ >
+ <a class="button is-static">
+ <span class="icon">
+ <i class="mdi mdi-calendar" />
+ </span>
+ </a>
+ </div>
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+
+ {!readonly && (
+ <span
+ data-tooltip={
+ withTimestampSupport
+ ? i18n`change value to unknown date`
+ : i18n`change value to empty`
+ }
+ >
+ <button
+ class="button is-info mr-3"
+ onClick={() => onChange(undefined as any)}
+ >
+ <Translate>clear</Translate>
+ </button>
+ </span>
+ )}
+ {withTimestampSupport && (
+ <span data-tooltip={i18n`change value to never`}>
+ <button
+ class="button is-info"
+ onClick={() => onChange({ t_s: "never" } as any)}
+ >
+ <Translate>never</Translate>
+ </button>
+ </span>
+ )}
+ </div>
+ <DatePicker
+ opened={opened}
+ closeFunction={() => setOpened(false)}
+ dateReceiver={(d) => {
+ if (withTimestampSupport) {
+ onChange({ t_s: d.getTime() / 1000 } as any);
+ } else {
+ onChange(d as any);
+ }
+ }}
+ />
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
new file mode 100644
index 000000000..d5c208e25
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx
@@ -0,0 +1,172 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { intervalToDuration, formatDuration } from "date-fns";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Translate, useTranslator } from "../../i18n";
+import { SimpleModal } from "../modal";
+import { DurationPicker } from "../picker/DurationPicker";
+import { InputProps, useField } from "./useField";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ readonly?: boolean;
+ withForever?: boolean;
+}
+
+export function InputDuration<T>({
+ name,
+ expand,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ readonly,
+ withForever,
+}: Props<keyof T>): VNode {
+ const [opened, setOpened] = useState(false);
+ const i18n = useTranslator();
+
+ const { error, required, value, onChange } = useField<T>(name);
+ let strValue = "";
+ if (!value) {
+ strValue = "";
+ } else if (value.d_us === "forever") {
+ strValue = i18n`forever`;
+ } else {
+ strValue = formatDuration(
+ intervalToDuration({ start: 0, end: value.d_us / 1000 }),
+ {
+ locale: {
+ formatDistance: (name, value) => {
+ switch (name) {
+ case "xMonths":
+ return i18n`${value}M`;
+ case "xYears":
+ return i18n`${value}Y`;
+ case "xDays":
+ return i18n`${value}d`;
+ case "xHours":
+ return i18n`${value}h`;
+ case "xMinutes":
+ return i18n`${value}min`;
+ case "xSeconds":
+ return i18n`${value}sec`;
+ }
+ },
+ localize: {
+ day: () => "s",
+ month: () => "m",
+ ordinalNumber: () => "th",
+ dayPeriod: () => "p",
+ quarter: () => "w",
+ era: () => "e",
+ },
+ },
+ }
+ );
+ }
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ <p class={expand ? "control is-expanded " : "control "}>
+ <input
+ class="input"
+ type="text"
+ readonly
+ value={strValue}
+ placeholder={placeholder}
+ onClick={() => {
+ if (!readonly) setOpened(true);
+ }}
+ />
+ {required && (
+ <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>
+ )}
+ {help}
+ </p>
+ <div
+ class="control"
+ onClick={() => {
+ if (!readonly) setOpened(true);
+ }}
+ >
+ <a class="button is-static">
+ <span class="icon">
+ <i class="mdi mdi-clock" />
+ </span>
+ </a>
+ </div>
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {withForever && (
+ <span data-tooltip={i18n`change value to never`}>
+ <button
+ class="button is-info mr-3"
+ onClick={() => onChange({ d_us: "forever" } as any)}
+ >
+ <Translate>forever</Translate>
+ </button>
+ </span>
+ )}
+ {!readonly && (
+ <span data-tooltip={i18n`change value to empty`}>
+ <button
+ class="button is-info "
+ onClick={() => onChange(undefined as any)}
+ >
+ <Translate>clear</Translate>
+ </button>
+ </span>
+ )}
+ </div>
+ {opened && (
+ <SimpleModal onCancel={() => setOpened(false)}>
+ <DurationPicker
+ days
+ hours
+ minutes
+ value={!value || value.d_us === "forever" ? 0 : value.d_us}
+ onChange={(v) => {
+ onChange({ d_us: v } as any);
+ }}
+ />
+ </SimpleModal>
+ )}
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx b/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx
new file mode 100644
index 000000000..8af9c7d96
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx
@@ -0,0 +1,66 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { ComponentChildren, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useGroupField } from "./useGroupField";
+
+export interface Props<T> {
+ name: T;
+ children: ComponentChildren;
+ label: ComponentChildren;
+ tooltip?: ComponentChildren;
+ alternative?: ComponentChildren;
+ fixed?: boolean;
+ initialActive?: boolean;
+}
+
+export function InputGroup<T>({ name, label, children, tooltip, alternative, fixed, initialActive }: Props<keyof T>): VNode {
+ const [active, setActive] = useState(initialActive || fixed);
+ const group = useGroupField<T>(name);
+
+ return <div class="card">
+ <header class="card-header">
+ <p class="card-header-title">
+ {label}
+ {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ {group?.hasError && <span class="icon has-text-danger" data-tooltip={tooltip}>
+ <i class="mdi mdi-alert" />
+ </span>}
+ </p>
+ { !fixed && <button class="card-header-icon" aria-label="more options" onClick={(): void => setActive(!active)}>
+ <span class="icon">
+ {active ?
+ <i class="mdi mdi-arrow-up" /> :
+ <i class="mdi mdi-arrow-down" />}
+ </span>
+ </button> }
+ </header>
+ {active ? <div class="card-content">
+ {children}
+ </div> : (
+ alternative ? <div class="card-content">
+ {alternative}
+ </div> : undefined
+ )}
+ </div>;
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
new file mode 100644
index 000000000..6cc9b9dcc
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx
@@ -0,0 +1,95 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { ComponentChildren, h, VNode } from "preact";
+import { useRef, useState } from "preact/hooks";
+import emptyImage from "../../assets/empty.png";
+import { Translate } from "../../i18n";
+import { MAX_IMAGE_SIZE as MAX_IMAGE_UPLOAD_SIZE } from "../../utils/constants";
+import { InputProps, useField } from "./useField";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ addonAfter?: ComponentChildren;
+ children?: ComponentChildren;
+}
+
+export function InputImage<T>({ name, readonly, placeholder, tooltip, label, help, children, expand }: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const image = useRef<HTMLInputElement>(null)
+
+ const [sizeError, setSizeError] = useState(false)
+
+ return <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ {value &&
+ <img src={value} style={{ width: 200, height: 200 }} onClick={() => image.current?.click()} />
+ }
+ <input
+ ref={image} style={{ display: 'none' }}
+ type="file" name={String(name)}
+ placeholder={placeholder} readonly={readonly}
+ onChange={e => {
+ const f: FileList | null = e.currentTarget.files
+ if (!f || f.length != 1) {
+ return onChange(undefined!)
+ }
+ if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
+ setSizeError(true)
+ return onChange(undefined!)
+ }
+ setSizeError(false)
+ return f[0].arrayBuffer().then(b => {
+ const b64 = btoa(
+ new Uint8Array(b)
+ .reduce((data, byte) => data + String.fromCharCode(byte), '')
+ )
+ return onChange(`data:${f[0].type};base64,${b64}` as any)
+ })
+ }} />
+ {help}
+ {children}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ {sizeError && <p class="help is-danger">
+ <Translate>Image should be smaller than 1 MB</Translate>
+ </p>}
+ {!value &&
+ <button class="button" onClick={() => image.current?.click()} ><Translate>Add</Translate></button>
+ }
+ {value &&
+ <button class="button" onClick={() => onChange(undefined!)} ><Translate>Remove</Translate></button>
+ }
+ </div>
+ </div>
+ </div>
+}
+
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx b/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx
new file mode 100644
index 000000000..12755f47a
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { Fragment, h } from "preact";
+import { useTranslator } from "../../i18n";
+import { Input } from "./Input";
+
+export function InputLocation({name}:{name:string}) {
+ const i18n = useTranslator()
+ return <>
+ <Input name={`${name}.country`} label={i18n`Country`} />
+ <Input name={`${name}.address_lines`} inputType="multiline"
+ label={i18n`Address`}
+ toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')}
+ fromStr={(v: string) => v.split('\n')}
+ />
+ <Input name={`${name}.building_number`} label={i18n`Building number`} />
+ <Input name={`${name}.building_name`} label={i18n`Building name`} />
+ <Input name={`${name}.street`} label={i18n`Street`} />
+ <Input name={`${name}.post_code`} label={i18n`Post code`} />
+ <Input name={`${name}.town_location`} label={i18n`Town location`} />
+ <Input name={`${name}.town`} label={i18n`Town`} />
+ <Input name={`${name}.district`} label={i18n`District`} />
+ <Input name={`${name}.country_subdivision`} label={i18n`Country subdivision`} />
+ </>
+} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx
new file mode 100644
index 000000000..046cda59e
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx
@@ -0,0 +1,42 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { ComponentChildren, h } from "preact";
+import { InputWithAddon } from "./InputWithAddon";
+import { InputProps } from "./useField";
+
+export interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ side?: ComponentChildren;
+ children?: ComponentChildren;
+}
+
+export function InputNumber<T>({ name, readonly, placeholder, tooltip, label, help, expand, children, side }: Props<keyof T>) {
+ return <InputWithAddon<T> name={name} readonly={readonly}
+ fromStr={(v) => !v ? undefined : parseInt(v, 10) } toStr={(v) => `${v}`}
+ inputType='number' expand={expand}
+ label={label} placeholder={placeholder} help={help} tooltip={tooltip}
+ inputExtra={{ min: 0 }}
+ children={children}
+ side={side}
+ />
+}
+
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx
new file mode 100644
index 000000000..44252317e
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx
@@ -0,0 +1,39 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { h, VNode } from "preact";
+import { InputArray } from "./InputArray";
+import { PAYTO_REGEX } from "../../utils/constants";
+import { InputProps } from "./useField";
+
+export type Props<T> = InputProps<T>;
+
+const PAYTO_START_REGEX = /^payto:\/\//
+
+export function InputPayto<T>({ name, readonly, placeholder, tooltip, label, help }: Props<keyof T>): VNode {
+ return <InputArray<T> name={name} readonly={readonly}
+ addonBefore="payto://"
+ label={label} placeholder={placeholder} help={help} tooltip={tooltip}
+ isValid={(v) => v && PAYTO_REGEX.test(v) }
+ toStr={(v?: string) => !v ? '': v.replace(PAYTO_START_REGEX, '')}
+ fromStr={(v: string) => `payto://${v}` }
+ />
+}
+
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
new file mode 100644
index 000000000..9cfef07cf
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -0,0 +1,392 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode, Fragment } from "preact";
+import { useCallback, useState } from "preact/hooks";
+import { Translate, Translator, useTranslator } from "../../i18n";
+import { COUNTRY_TABLE } from "../../utils/constants";
+import { FormErrors, FormProvider } from "./FormProvider";
+import { Input } from "./Input";
+import { InputGroup } from "./InputGroup";
+import { InputSelector } from "./InputSelector";
+import { InputProps, useField } from "./useField";
+
+export interface Props<T> extends InputProps<T> {
+ isValid?: (e: any) => boolean;
+}
+
+// https://datatracker.ietf.org/doc/html/rfc8905
+type Entity = {
+ // iban, bitcoin, x-taler-bank. it defined the format
+ target: string;
+ // path1 if the first field to be used
+ path1: string;
+ // path2 if the second field to be used, optional
+ path2?: string;
+ // options of the payto uri
+ options: {
+ "receiver-name"?: string;
+ sender?: string;
+ message?: string;
+ amount?: string;
+ instruction?: string;
+ [name: string]: string | undefined;
+ };
+};
+
+function isEthereumAddress(address: string) {
+ if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
+ return false;
+ } else if (
+ /^(0x|0X)?[0-9a-f]{40}$/.test(address) ||
+ /^(0x|0X)?[0-9A-F]{40}$/.test(address)
+ ) {
+ return true;
+ }
+ return checkAddressChecksum(address);
+}
+
+function checkAddressChecksum(address: string) {
+ //TODO implement ethereum checksum
+ return true;
+}
+
+function validateBitcoin(addr: string, i18n: Translator): string | undefined {
+ try {
+ const valid = /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/.test(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n`This is not a valid bitcoin address.`;
+}
+
+function validateEthereum(addr: string, i18n: Translator): string | undefined {
+ try {
+ const valid = isEthereumAddress(addr);
+ if (valid) return undefined;
+ } catch (e) {
+ console.log(e);
+ }
+ return i18n`This is not a valid Ethereum address.`;
+}
+
+/**
+ * An IBAN is validated by converting it into an integer and performing a
+ * basic mod-97 operation (as described in ISO 7064) on it.
+ * If the IBAN is valid, the remainder equals 1.
+ *
+ * The algorithm of IBAN validation is as follows:
+ * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid
+ * 2.- Move the four initial characters to the end of the string
+ * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35
+ * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97
+ *
+ * If the remainder is 1, the check digit test is passed and the IBAN might be valid.
+ *
+ */
+function validateIBAN(iban: string, i18n: Translator): string | undefined {
+ // Check total length
+ if (iban.length < 4)
+ return i18n`IBAN numbers usually have more that 4 digits`;
+ if (iban.length > 34)
+ return i18n`IBAN numbers usually have less that 34 digits`;
+
+ const A_code = "A".charCodeAt(0);
+ const Z_code = "Z".charCodeAt(0);
+ const IBAN = iban.toUpperCase();
+ // check supported country
+ const code = IBAN.substr(0, 2);
+ const found = code in COUNTRY_TABLE;
+ if (!found) return i18n`IBAN country code not found`;
+
+ // 2.- Move the four initial characters to the end of the string
+ const step2 = IBAN.substr(4) + iban.substr(0, 4);
+ const step3 = Array.from(step2)
+ .map((letter) => {
+ const code = letter.charCodeAt(0);
+ if (code < A_code || code > Z_code) return letter;
+ return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`;
+ })
+ .join("");
+
+ function calculate_iban_checksum(str: string): number {
+ const numberStr = str.substr(0, 5);
+ const rest = str.substr(5);
+ const number = parseInt(numberStr, 10);
+ const result = number % 97;
+ if (rest.length > 0) {
+ return calculate_iban_checksum(`${result}${rest}`);
+ }
+ return result;
+ }
+
+ const checksum = calculate_iban_checksum(step3);
+ if (checksum !== 1) return i18n`IBAN number is not valid, checksum is wrong`;
+ return undefined;
+}
+
+// const targets = ['ach', 'bic', 'iban', 'upi', 'bitcoin', 'ilp', 'void', 'x-taler-bank']
+const targets = [
+ "Choose one...",
+ "iban",
+ "x-taler-bank",
+ "bitcoin",
+ "ethereum",
+];
+const noTargetValue = targets[0];
+const defaultTarget = { target: noTargetValue, options: {} };
+
+function undefinedIfEmpty<T>(obj: T): T | undefined {
+ return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
+ ? obj
+ : undefined;
+}
+
+export function InputPaytoForm<T>({
+ name,
+ readonly,
+ label,
+ tooltip,
+}: Props<keyof T>): VNode {
+ const { value: paytos, onChange } = useField<T>(name);
+
+ const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget);
+
+ let payToPath;
+ if (value.target === "iban" && value.path1) {
+ payToPath = `/${value.path1.toUpperCase()}`;
+ } else if (value.path1) {
+ if (value.path2) {
+ payToPath = `/${value.path1}/${value.path2}`;
+ } else {
+ payToPath = `/${value.path1}`;
+ }
+ }
+ const i18n = useTranslator();
+
+ const ops = value.options!;
+ const url = tryUrl(`payto://${value.target}${payToPath}`);
+ if (url) {
+ Object.keys(ops).forEach((opt_key) => {
+ const opt_value = ops[opt_key];
+ if (opt_value) url.searchParams.set(opt_key, opt_value);
+ });
+ }
+ const paytoURL = !url ? "" : url.toString();
+
+ const errors: FormErrors<Entity> = {
+ target: value.target === noTargetValue ? i18n`required` : undefined,
+ path1: !value.path1
+ ? i18n`required`
+ : value.target === "iban"
+ ? validateIBAN(value.path1, i18n)
+ : value.target === "bitcoin"
+ ? validateBitcoin(value.path1, i18n)
+ : value.target === "ethereum"
+ ? validateEthereum(value.path1, i18n)
+ : undefined,
+ path2:
+ value.target === "x-taler-bank"
+ ? !value.path2
+ ? i18n`required`
+ : undefined
+ : undefined,
+ options: undefinedIfEmpty({
+ "receiver-name": !value.options?.["receiver-name"]
+ ? i18n`required`
+ : undefined,
+ }),
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined
+ );
+
+ const submit = useCallback((): void => {
+ const alreadyExists =
+ paytos.findIndex((x: string) => x === paytoURL) !== -1;
+ if (!alreadyExists) {
+ onChange([paytoURL, ...paytos] as any);
+ }
+ valueHandler(defaultTarget);
+ }, [value]);
+
+ //FIXME: translating plural singular
+ return (
+ <InputGroup name="payto" label={label} fixed tooltip={tooltip}>
+ <FormProvider<Entity>
+ name="tax"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <InputSelector<Entity>
+ name="target"
+ label={i18n`Target type`}
+ tooltip={i18n`Method to use for wire transfer`}
+ values={targets}
+ toStr={(v) => (v === noTargetValue ? i18n`Choose one...` : v)}
+ />
+
+ {value.target === "ach" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Routing`}
+ tooltip={i18n`Routing number.`}
+ />
+ <Input<Entity>
+ name="path2"
+ label={i18n`Account`}
+ tooltip={i18n`Account number.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "bic" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Code`}
+ tooltip={i18n`Business Identifier Code.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "iban" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Account`}
+ tooltip={i18n`Bank Account Number.`}
+ inputExtra={{ style: { textTransform: "uppercase" } }}
+ />
+ </Fragment>
+ )}
+ {value.target === "upi" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Account`}
+ tooltip={i18n`Unified Payment Interface.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "bitcoin" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Address`}
+ tooltip={i18n`Bitcoin protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "ethereum" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Address`}
+ tooltip={i18n`Ethereum protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "ilp" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Address`}
+ tooltip={i18n`Interledger protocol.`}
+ />
+ </Fragment>
+ )}
+ {value.target === "void" && <Fragment />}
+ {value.target === "x-taler-bank" && (
+ <Fragment>
+ <Input<Entity>
+ name="path1"
+ label={i18n`Host`}
+ tooltip={i18n`Bank host.`}
+ />
+ <Input<Entity>
+ name="path2"
+ label={i18n`Account`}
+ tooltip={i18n`Bank account.`}
+ />
+ </Fragment>
+ )}
+
+ {value.target !== noTargetValue && (
+ <Input
+ name="options.receiver-name"
+ label={i18n`Name`}
+ tooltip={i18n`Bank account owner's name.`}
+ />
+ )}
+
+ <div class="field is-horizontal">
+ <div class="field-label is-normal" />
+ <div class="field-body" style={{ display: "block" }}>
+ {paytos.map((v: any, i: number) => (
+ <div
+ key={i}
+ class="tags has-addons mt-3 mb-0 mr-3"
+ style={{ flexWrap: "nowrap" }}
+ >
+ <span
+ class="tag is-medium is-info mb-0"
+ style={{ maxWidth: "90%" }}
+ >
+ {v}
+ </span>
+ <a
+ class="tag is-medium is-danger is-delete mb-0"
+ onClick={() => {
+ onChange(paytos.filter((f: any) => f !== v) as any);
+ }}
+ />
+ </div>
+ ))}
+ {!paytos.length && i18n`No accounts yet.`}
+ </div>
+ </div>
+
+ {value.target !== noTargetValue && (
+ <div class="buttons is-right mt-5">
+ <button
+ class="button is-info"
+ data-tooltip={i18n`add tax to the tax list`}
+ disabled={hasErrors}
+ onClick={submit}
+ >
+ <Translate>Add</Translate>
+ </button>
+ </div>
+ )}
+ </FormProvider>
+ </InputGroup>
+ );
+}
+
+function tryUrl(s: string): URL | undefined {
+ try {
+ return new URL(s);
+ } catch (e) {
+ return undefined;
+ }
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx
new file mode 100644
index 000000000..51f84fd12
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx
@@ -0,0 +1,139 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import emptyImage from "../../assets/empty.png";
+import { MerchantBackend, WithId } from "../../declaration";
+import { useInstanceProducts } from "../../hooks/product";
+import { Translate, useTranslator } from "../../i18n";
+import { FormErrors, FormProvider } from "./FormProvider";
+import { InputWithAddon } from "./InputWithAddon";
+
+type Entity = MerchantBackend.Products.ProductDetail & WithId
+
+export interface Props {
+ selected?: Entity;
+ onChange: (p?: Entity) => void;
+ products: (MerchantBackend.Products.ProductDetail & WithId)[],
+}
+
+interface ProductSearch {
+ name: string;
+}
+
+export function InputSearchProduct({ selected, onChange, products }: Props): VNode {
+ const [prodForm, setProdName] = useState<Partial<ProductSearch>>({ name: '' })
+
+ const errors: FormErrors<ProductSearch> = {
+ name: undefined
+ }
+ const i18n = useTranslator()
+
+
+ if (selected) {
+ return <article class="media">
+ <figure class="media-left">
+ <p class="image is-128x128">
+ <img src={selected.image ? selected.image : emptyImage} />
+ </p>
+ </figure>
+ <div class="media-content">
+ <div class="content">
+ <p class="media-meta"><Translate>Product id</Translate>: <b>{selected.id}</b></p>
+ <p><Translate>Description</Translate>: {selected.description}</p>
+ <div class="buttons is-right mt-5">
+ <button class="button is-info" onClick={() => onChange(undefined)}>clear</button>
+ </div>
+ </div>
+ </div>
+ </article>
+ }
+
+ return <FormProvider<ProductSearch> errors={errors} object={prodForm} valueHandler={setProdName} >
+
+ <InputWithAddon<ProductSearch>
+ name="name"
+ label={i18n`Product`}
+ tooltip={i18n`search products by it's description or id`}
+ addonAfter={<span class="icon" ><i class="mdi mdi-magnify" /></span>}
+ >
+ <div>
+ <ProductList
+ name={prodForm.name}
+ list={products}
+ onSelect={(p) => {
+ setProdName({ name: '' })
+ onChange(p)
+ }}
+ />
+ </div>
+ </InputWithAddon>
+
+ </FormProvider>
+
+}
+
+interface ProductListProps {
+ name?: string;
+ onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void;
+ list: (MerchantBackend.Products.ProductDetail & WithId)[]
+}
+
+function ProductList({ name, onSelect, list }: ProductListProps) {
+ if (!name) {
+ /* FIXME
+ this BR is added to occupy the space that will be added when the
+ dropdown appears
+ */
+ return <div ><br /></div>
+ }
+ const filtered = list.filter(p => p.id.includes(name) || p.description.includes(name))
+
+ return <div class="dropdown is-active">
+ <div class="dropdown-menu" id="dropdown-menu" role="menu" style={{ minWidth: '20rem' }}>
+ <div class="dropdown-content">
+ {!filtered.length ?
+ <div class="dropdown-item" >
+ <Translate>no products found with that description</Translate>
+ </div> :
+ filtered.map(p => (
+ <div key={p.id} class="dropdown-item" onClick={() => onSelect(p)} style={{ cursor: 'pointer' }}>
+ <article class="media">
+ <div class="media-left">
+ <div class="image" style={{ minWidth: 64 }}><img src={p.image ? p.image : emptyImage} style={{ width: 64, height: 64 }} /></div>
+ </div>
+ <div class="media-content">
+ <div class="content">
+ <p>
+ <strong>{p.id}</strong> <small>{p.price}</small>
+ <br />
+ {p.description}
+ </p>
+ </div>
+ </div>
+ </article>
+ </div>
+ ))
+ }
+ </div>
+ </div>
+ </div>
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx
new file mode 100644
index 000000000..1990eeeae
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx
@@ -0,0 +1,55 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { h, VNode } from 'preact';
+import { useState } from 'preact/hooks';
+import { FormProvider } from "./FormProvider";
+import { InputSecured } from './InputSecured';
+
+export default {
+ title: 'Components/Form/InputSecured',
+ component: InputSecured,
+};
+
+type T = { auth_token: string | null }
+
+export const InitialValueEmpty = (): VNode => {
+ const [state, setState] = useState<Partial<T>>({ auth_token: '' })
+ return <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
+ Initial value: ''
+ <InputSecured<T> name="auth_token" label="Access token" />
+ </FormProvider>
+}
+
+export const InitialValueToken = (): VNode => {
+ const [state, setState] = useState<Partial<T>>({ auth_token: 'token' })
+ return <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
+ <InputSecured<T> name="auth_token" label="Access token" />
+ </FormProvider>
+}
+
+export const InitialValueNull = (): VNode => {
+ const [state, setState] = useState<Partial<T>>({ auth_token: null })
+ return <FormProvider<T> object={state} errors={{}} valueHandler={setState}>
+ Initial value: ''
+ <InputSecured<T> name="auth_token" label="Access token" />
+ </FormProvider>
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx
new file mode 100644
index 000000000..c9b0f43b9
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx
@@ -0,0 +1,119 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Translate, useTranslator } from "../../i18n";
+import { InputProps, useField } from "./useField";
+
+export type Props<T> = InputProps<T>;
+
+const TokenStatus = ({ prev, post }: any) => {
+ if ((prev === undefined || prev === null) && (post === undefined || post === null))
+ return null
+ return (prev === post) ? null : (
+ post === null ?
+ <span class="tag is-danger is-align-self-center ml-2"><Translate>Deleting</Translate></span> :
+ <span class="tag is-warning is-align-self-center ml-2"><Translate>Changing</Translate></span>
+ )
+}
+
+export function InputSecured<T>({ name, readonly, placeholder, tooltip, label, help }: Props<keyof T>): VNode {
+ const { error, value, initial, onChange, toStr, fromStr } = useField<T>(name);
+
+ const [active, setActive] = useState(false);
+ const [newValue, setNuewValue] = useState("")
+
+ const i18n = useTranslator()
+
+ return <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ {!active ?
+ <Fragment>
+ <div class="field has-addons">
+ <button class="button"
+ onClick={(): void => { setActive(!active); }} >
+ <div class="icon is-left"><i class="mdi mdi-lock-reset" /></div>
+ <span><Translate>Manage access token</Translate></span>
+ </button>
+ <TokenStatus prev={initial} post={value} />
+ </div>
+ </Fragment> :
+ <Fragment>
+ <div class="field has-addons">
+ <div class="control">
+ <a class="button is-static">secret-token:</a>
+ </div>
+ <div class="control is-expanded">
+ <input class="input" type="text"
+ placeholder={placeholder} readonly={readonly || !active}
+ disabled={readonly || !active}
+ name={String(name)} value={newValue}
+ onInput={(e): void => {
+ setNuewValue(e.currentTarget.value)
+ }} />
+ {help}
+ </div>
+ <div class="control">
+ <button class="button is-info" disabled={fromStr(newValue) === value} onClick={(): void => { onChange(fromStr(newValue)); setActive(!active); setNuewValue(""); }} >
+ <div class="icon is-left"><i class="mdi mdi-lock-outline" /></div>
+ <span><Translate>Update</Translate></span>
+ </button>
+ </div>
+ </div>
+ </Fragment>
+ }
+ {error ? <p class="help is-danger">{error}</p> : null}
+ </div>
+ </div>
+ {active &&
+ <div class="field is-horizontal">
+ <div class="field-body is-flex-grow-3">
+ <div class="level" style={{ width: '100%' }}>
+ <div class="level-right is-flex-grow-1">
+ <div class="level-item">
+ <button class="button is-danger" disabled={null === value || undefined === value} onClick={(): void => { onChange(null!); setActive(!active); setNuewValue(""); }} >
+ <div class="icon is-left"><i class="mdi mdi-lock-open-variant" /></div>
+ <span><Translate>Remove</Translate></span>
+ </button>
+ </div>
+ <div class="level-item">
+ <button class="button " onClick={(): void => { onChange(initial!); setActive(!active); setNuewValue(""); }} >
+ <div class="icon is-left"><i class="mdi mdi-lock-open-variant" /></div>
+ <span><Translate>Cancel</Translate></span>
+ </button>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ }
+ </Fragment >;
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
new file mode 100644
index 000000000..86f4de756
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
@@ -0,0 +1,86 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { h, VNode } from "preact";
+import { InputProps, useField } from "./useField";
+
+interface Props<T> extends InputProps<T> {
+ readonly?: boolean;
+ expand?: boolean;
+ values: string[];
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+}
+
+const defaultToString = (f?: any): string => f || "";
+const defaultFromString = (v: string): any => v as any;
+
+export function InputSelector<T>({
+ name,
+ readonly,
+ expand,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ values,
+ toStr = defaultToString,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded select" : "control select"}>
+ <select
+ class={error ? "select is-danger" : "select"}
+ name={String(name)}
+ disabled={readonly}
+ readonly={readonly}
+ onChange={(e) => {
+ onChange(e.currentTarget.value as any);
+ }}
+ >
+ {placeholder && <option>{placeholder}</option>}
+ {values.map((v, i) => (
+ <option key={i} value={v} selected={value === v}>
+ {toStr(v)}
+ </option>
+ ))}
+ </select>
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx
new file mode 100644
index 000000000..63c7e4131
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx
@@ -0,0 +1,162 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { addDays } from "date-fns";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { FormProvider } from "./FormProvider";
+import { InputStock, Stock } from "./InputStock";
+
+export default {
+ title: "Components/Form/InputStock",
+ component: InputStock,
+};
+
+type T = { stock?: Stock };
+
+export const CreateStockEmpty = () => {
+ const [state, setState] = useState<Partial<T>>({});
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const CreateStockUnknownRestock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 10,
+ lost: 0,
+ sold: 0,
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const CreateStockNoRestock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 10,
+ lost: 0,
+ sold: 0,
+ nextRestock: { t_s: "never" },
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const CreateStockWithRestock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 15,
+ lost: 0,
+ sold: 0,
+ nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 },
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const UpdatingProductWithManagedStock = () => {
+ const [state, setState] = useState<Partial<T>>({
+ stock: {
+ current: 100,
+ lost: 0,
+ sold: 0,
+ nextRestock: { t_s: addDays(new Date(), 1).getTime() / 1000 },
+ },
+ });
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" alreadyExist />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
+
+export const UpdatingProductWithInfiniteStock = () => {
+ const [state, setState] = useState<Partial<T>>({});
+ return (
+ <FormProvider<T>
+ name="product"
+ object={state}
+ errors={{}}
+ valueHandler={setState}
+ >
+ <InputStock<T> name="stock" label="Stock" alreadyExist />
+ <div>
+ <pre>{JSON.stringify(state, undefined, 2)}</pre>
+ </div>
+ </FormProvider>
+ );
+};
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
new file mode 100644
index 000000000..158f44192
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
@@ -0,0 +1,171 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { Fragment, h } from "preact";
+import { MerchantBackend, Timestamp } from "../../declaration";
+import { InputProps, useField } from "./useField";
+import { FormProvider, FormErrors } from "./FormProvider";
+import { useLayoutEffect, useState } from "preact/hooks";
+import { Input } from "./Input";
+import { InputGroup } from "./InputGroup";
+import { InputNumber } from "./InputNumber";
+import { InputDate } from "./InputDate";
+import { Translate, useTranslator } from "../../i18n";
+import { InputLocation } from "./InputLocation";
+
+export interface Props<T> extends InputProps<T> {
+ alreadyExist?: boolean;
+}
+
+
+type Entity = Stock
+
+export interface Stock {
+ current: number;
+ lost: number;
+ sold: number;
+ address?: MerchantBackend.Location;
+ nextRestock?: Timestamp;
+}
+
+interface StockDelta {
+ incoming: number;
+ lost: number;
+}
+
+
+export function InputStock<T>({ name, tooltip, label, alreadyExist }: Props<keyof T>) {
+ const { error, value, onChange } = useField<T>(name);
+
+ const [errors, setErrors] = useState<FormErrors<Entity>>({})
+
+ const [formValue, valueHandler] = useState<Partial<Entity>>(value)
+ const [addedStock, setAddedStock] = useState<StockDelta>({ incoming: 0, lost: 0 })
+ const i18n = useTranslator()
+
+
+ useLayoutEffect(() => {
+ if (!formValue) {
+ onChange(undefined as any)
+ } else {
+ onChange({
+ ...formValue,
+ current: (formValue?.current || 0) + addedStock.incoming,
+ lost: (formValue?.lost || 0) + addedStock.lost
+ } as any)
+ }
+ }, [formValue, addedStock])
+
+ if (!formValue) {
+ return <Fragment>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field has-addons">
+ {!alreadyExist ?
+ <button class="button"
+ data-tooltip={i18n`click here to configure the stock of the product, leave it as is and the backend will not control stock`}
+ onClick={(): void => { valueHandler({ current: 0, lost: 0, sold: 0 } as Stock as any); }} >
+ <span><Translate>Manage stock</Translate></span>
+ </button> : <button class="button"
+ data-tooltip={i18n`this product has been configured without stock control`}
+ disabled >
+ <span><Translate>Infinite</Translate></span>
+ </button>
+ }
+ </div>
+ </div>
+ </div>
+ </Fragment >
+ }
+
+ const currentStock = (formValue.current || 0) - (formValue.lost || 0) - (formValue.sold || 0)
+
+ const stockAddedErrors: FormErrors<typeof addedStock> = {
+ lost: currentStock + addedStock.incoming < addedStock.lost ?
+ i18n`lost cannot be greater than current and incoming (max ${currentStock + addedStock.incoming})`
+ : undefined
+ }
+
+ // const stockUpdateDescription = stockAddedErrors.lost ? '' : (
+ // !!addedStock.incoming || !!addedStock.lost ?
+ // i18n`current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` :
+ // i18n`current stock will stay at ${currentStock}`
+ // )
+
+ return <Fragment>
+ <div class="card">
+ <header class="card-header">
+ <p class="card-header-title">
+ {label}
+ {tooltip && <span class="icon" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </p>
+ </header>
+ <div class="card-content">
+ <FormProvider<Entity> name="stock" errors={errors} object={formValue} valueHandler={valueHandler}>
+ {alreadyExist ? <Fragment>
+
+ <FormProvider name="added" errors={stockAddedErrors} object={addedStock} valueHandler={setAddedStock as any}>
+ <InputNumber name="incoming" label={i18n`Incoming`} />
+ <InputNumber name="lost" label={i18n`Lost`} />
+ </FormProvider>
+
+ {/* <div class="field is-horizontal">
+ <div class="field-label is-normal" />
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ {stockUpdateDescription}
+ </div>
+ </div>
+ </div> */}
+
+ </Fragment> : <InputNumber<Entity> name="current"
+ label={i18n`Current`}
+ side={
+ <button class="button is-danger"
+ data-tooltip={i18n`remove stock control for this product`}
+ onClick={(): void => { valueHandler(undefined as any) }} >
+ <span><Translate>without stock</Translate></span>
+ </button>
+ }
+ />}
+
+ <InputDate<Entity> name="nextRestock" label={i18n`Next restock`} withTimestampSupport />
+
+ <InputGroup<Entity> name="address" label={i18n`Delivery address`}>
+ <InputLocation name="address" />
+ </InputGroup>
+ </FormProvider>
+ </div>
+ </div>
+ </Fragment>
+}
+ // (
+
+
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx
new file mode 100644
index 000000000..507a61242
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx
@@ -0,0 +1,97 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { h, VNode } from "preact";
+import { useCallback, useState } from "preact/hooks";
+import * as yup from 'yup';
+import { MerchantBackend } from "../../declaration";
+import { Translate, useTranslator } from "../../i18n";
+import { TaxSchema as schema } from '../../schemas';
+import { FormErrors, FormProvider } from "./FormProvider";
+import { Input } from "./Input";
+import { InputGroup } from "./InputGroup";
+import { InputProps, useField } from "./useField";
+
+export interface Props<T> extends InputProps<T> {
+ isValid?: (e: any) => boolean;
+}
+
+type Entity = MerchantBackend.Tax
+export function InputTaxes<T>({ name, readonly, label }: Props<keyof T>): VNode {
+ const { value: taxes, onChange, } = useField<T>(name);
+
+ const [value, valueHandler] = useState<Partial<Entity>>({})
+ // const [errors, setErrors] = useState<FormErrors<Entity>>({})
+
+ let errors: FormErrors<Entity> = {}
+
+ try {
+ schema.validateSync(value, { abortEarly: false })
+ } catch (err) {
+ if (err instanceof yup.ValidationError) {
+ const yupErrors = err.inner as yup.ValidationError[]
+ errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {})
+ }
+ }
+ const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined)
+
+ const submit = useCallback((): void => {
+ onChange([value as any, ...taxes] as any)
+ valueHandler({})
+ }, [value])
+
+ const i18n = useTranslator()
+
+ //FIXME: translating plural singular
+ return (
+ <InputGroup name="tax" label={label} alternative={taxes.length > 0 && <p>This product has {taxes.length} applicable taxes configured.</p>}>
+ <FormProvider<Entity> name="tax" errors={errors} object={value} valueHandler={valueHandler} >
+
+ <div class="field is-horizontal">
+ <div class="field-label is-normal" />
+ <div class="field-body" style={{ display: 'block' }}>
+ {taxes.map((v: any, i: number) => <div key={i} class="tags has-addons mt-3 mb-0 mr-3" style={{ flexWrap: 'nowrap' }}>
+ <span class="tag is-medium is-info mb-0" style={{ maxWidth: '90%' }}><b>{v.tax}</b>: {v.name}</span>
+ <a class="tag is-medium is-danger is-delete mb-0" onClick={() => {
+ onChange(taxes.filter((f: any) => f !== v) as any);
+ valueHandler(v);
+ }} />
+ </div>
+ )}
+ {!taxes.length && i18n`No taxes configured for this product.`}
+ </div>
+ </div>
+
+ <Input<Entity> name="tax" label={i18n`Amount`} tooltip={i18n`Taxes can be in currencies that differ from the main currency used by the merchant.`}>
+ <Translate>Enter currency and value separated with a colon, e.g. "USD:2.3".</Translate>
+ </Input>
+
+ <Input<Entity> name="name" label={i18n`Description`} tooltip={i18n`Legal name of the tax, e.g. VAT or import duties.`} />
+
+ <div class="buttons is-right mt-5">
+ <button class="button is-info"
+ data-tooltip={i18n`add tax to the tax list`}
+ disabled={hasErrors}
+ onClick={submit}><Translate>Add</Translate></button>
+ </div>
+ </FormProvider>
+ </InputGroup>
+ )
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
new file mode 100644
index 000000000..a16ebc2e9
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
@@ -0,0 +1,77 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { ComponentChildren, h, VNode } from "preact";
+import { InputProps, useField } from "./useField";
+
+export interface Props<T> extends InputProps<T> {
+ expand?: boolean;
+ inputType?: 'text' | 'number';
+ addonBefore?: ComponentChildren;
+ addonAfter?: ComponentChildren;
+ toStr?: (v?: any) => string;
+ fromStr?: (s: string) => any;
+ inputExtra?: any,
+ children?: ComponentChildren,
+ side?: ComponentChildren;
+}
+
+const defaultToString = (f?: any): string => f || ''
+const defaultFromString = (v: string): any => v as any
+
+export function InputWithAddon<T>({ name, readonly, addonBefore, children, expand, label, placeholder, help, tooltip, inputType, inputExtra, side, addonAfter, toStr = defaultToString, fromStr = defaultFromString }: Props<keyof T>): VNode {
+ const { error, value, onChange, required } = useField<T>(name);
+
+ return <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <div class="field has-addons">
+ {addonBefore && <div class="control">
+ <a class="button is-static">{addonBefore}</a>
+ </div>}
+ <p class={`control${expand ? " is-expanded" :""}${required ? " has-icons-right" : ''}`}>
+ <input {...(inputExtra || {})} class={error ? "input is-danger" : "input"} type={inputType}
+ placeholder={placeholder} readonly={readonly}
+ name={String(name)} value={toStr(value)}
+ onChange={(e): void => onChange(fromStr(e.currentTarget.value))} />
+ {required && <span class="icon has-text-danger is-right">
+ <i class="mdi mdi-alert" />
+ </span>}
+ {help}
+ {children}
+ </p>
+ {addonAfter && <div class="control">
+ <a class="button is-static">{addonAfter}</a>
+ </div>}
+ </div>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {side}
+ </div>
+ </div>;
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/TextField.tsx b/packages/merchant-backoffice-ui/src/components/form/TextField.tsx
new file mode 100644
index 000000000..2579a27b2
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/TextField.tsx
@@ -0,0 +1,53 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { ComponentChildren, h, VNode } from "preact";
+import { useField, InputProps } from "./useField";
+
+interface Props<T> extends InputProps<T> {
+ inputType?: 'text' | 'number' | 'multiline' | 'password';
+ expand?: boolean;
+ side?: ComponentChildren;
+ children: ComponentChildren;
+}
+
+export function TextField<T>({ name, tooltip, label, expand, help, children, side}: Props<keyof T>): VNode {
+ const { error } = useField<T>(name);
+ return <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ {label}
+ {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class={expand ? "control is-expanded has-icons-right" : "control has-icons-right"}>
+ {children}
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ {side}
+ </div>
+ </div>;
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/useField.tsx b/packages/merchant-backoffice-ui/src/components/form/useField.tsx
new file mode 100644
index 000000000..8479d7ad8
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/useField.tsx
@@ -0,0 +1,86 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { ComponentChildren, VNode } from "preact";
+import { useFormContext } from "./FormProvider";
+
+interface Use<V> {
+ error?: string;
+ required: boolean;
+ value: any;
+ initial: any;
+ onChange: (v: V) => void;
+ toStr: (f: V | undefined) => string;
+ fromStr: (v: string) => V
+}
+
+export function useField<T>(name: keyof T): Use<T[typeof name]> {
+ const { errors, object, initialObject, toStr, fromStr, valueHandler } = useFormContext<T>()
+ type P = typeof name
+ type V = T[P]
+
+ const updateField = (field: P) => (value: V): void => {
+ return valueHandler((prev) => {
+ return setValueDeeper(prev, String(field).split('.'), value)
+ })
+ }
+
+ const defaultToString = ((f?: V): string => String(!f ? '' : f))
+ const defaultFromString = ((v: string): V => v as any)
+ const value = readField(object, String(name))
+ const initial = readField(initialObject, String(name))
+ const isDirty = value !== initial
+ const hasError = readField(errors, String(name))
+ return {
+ error: isDirty ? hasError : undefined,
+ required: !isDirty && hasError,
+ value,
+ initial,
+ onChange: updateField(name) as any,
+ toStr: toStr[name] ? toStr[name]! : defaultToString,
+ fromStr: fromStr[name] ? fromStr[name]! : defaultFromString,
+ }
+}
+/**
+ * read the field of an object an support accessing it using '.'
+ *
+ * @param object
+ * @param name
+ * @returns
+ */
+const readField = (object: any, name: string) => {
+ return name.split('.').reduce((prev, current) => prev && prev[current], object)
+}
+
+const setValueDeeper = (object: any, names: string[], value: any): any => {
+ if (names.length === 0) return value
+ const [head, ...rest] = names
+ return { ...object, [head]: setValueDeeper(object[head] || {}, rest, value) }
+}
+
+export interface InputProps<T> {
+ name: T;
+ label: ComponentChildren;
+ placeholder?: string;
+ tooltip?: ComponentChildren;
+ readonly?: boolean;
+ help?: ComponentChildren;
+} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx b/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx
new file mode 100644
index 000000000..a73f464a1
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx
@@ -0,0 +1,40 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { useFormContext } from "./FormProvider";
+
+interface Use {
+ hasError?: boolean;
+}
+
+export function useGroupField<T>(name: keyof T): Use {
+ const f = useFormContext<T>();
+ if (!f)
+ return {};
+
+ return {
+ hasError: readField(f.errors, String(name))
+ };
+}
+
+const readField = (object: any, name: string) => {
+ return name.split('.').reduce((prev, current) => prev && prev[current], object)
+}