summaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components')
-rw-r--r--packages/merchant-backoffice-ui/src/components/exception/login.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDate.tsx11
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx305
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx (renamed from packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx)97
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx31
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx81
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/index.tsx58
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx8
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx18
10 files changed, 266 insertions, 351 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx
index f2f94a7c5..4fa440fc7 100644
--- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx
+++ b/packages/merchant-backoffice-ui/src/components/exception/login.tsx
@@ -93,7 +93,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
<input
class="input"
type="password"
- placeholder={"set new access token"}
+ placeholder={"current access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
@@ -186,7 +186,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
<input
class="input"
type="password"
- placeholder={"set new access token"}
+ placeholder={"current access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
index 1f41c3564..a398629dc 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
@@ -20,16 +20,18 @@
*/
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { h, VNode } from "preact";
+import { ComponentChildren, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { DatePicker } from "../picker/DatePicker.js";
import { InputProps, useField } from "./useField.js";
+import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js";
export interface Props<T> extends InputProps<T> {
readonly?: boolean;
expand?: boolean;
//FIXME: create separated components InputDate and InputTimestamp
withTimestampSupport?: boolean;
+ side?: ComponentChildren;
}
export function InputDate<T>({
@@ -41,9 +43,11 @@ export function InputDate<T>({
tooltip,
expand,
withTimestampSupport,
+ side,
}: Props<keyof T>): VNode {
const [opened, setOpened] = useState(false);
const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
const { error, required, value, onChange } = useField<T>(name);
@@ -51,14 +55,14 @@ export function InputDate<T>({
if (!value) {
strValue = withTimestampSupport ? "unknown" : "";
} else if (value instanceof Date) {
- strValue = format(value, "yyyy/MM/dd");
+ strValue = format(value, dateFormatForSettings(settings));
} else if (value.t_s) {
strValue =
value.t_s === "never"
? withTimestampSupport
? "never"
: ""
- : format(new Date(value.t_s * 1000), "yyyy/MM/dd");
+ : format(new Date(value.t_s * 1000), dateFormatForSettings(settings));
}
return (
@@ -142,6 +146,7 @@ export function InputDate<T>({
</button>
</span>
)}
+ {side}
</div>
<DatePicker
opened={opened}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index 8d324660e..5cd69a0b3 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -18,9 +18,9 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-import { useCallback, useState } from "preact/hooks";
import { COUNTRY_TABLE } from "../../utils/constants.js";
import { undefinedIfEmpty } from "../../utils/table.js";
import { FormErrors, FormProvider } from "./FormProvider.js";
@@ -28,23 +28,23 @@ import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js";
-import { InputWithAddon } from "./InputWithAddon.js";
-import { MerchantBackend } from "../../declaration.js";
+import { useEffect, useState } from "preact/hooks";
export interface Props<T> extends InputProps<T> {
isValid?: (e: any) => boolean;
}
+// type Entity = PaytoUriGeneric
// 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;
+ path1?: string;
// path2 if the second field to be used, optional
path2?: string;
- // options of the payto uri
- options: {
+ // params of the payto uri
+ params: {
"receiver-name"?: string;
sender?: string;
message?: string;
@@ -52,13 +52,6 @@ type Entity = {
instruction?: string;
[name: string]: string | undefined;
};
- auth: {
- type: "unset" | "basic" | "none";
- url?: string;
- username?: string;
- password?: string;
- repeat?: string;
- };
};
function isEthereumAddress(address: string) {
@@ -171,14 +164,10 @@ const targets = [
"bitcoin",
"ethereum",
];
-const accountAuthType = ["none", "basic"];
const noTargetValue = targets[0];
-const defaultTarget: Partial<Entity> = {
+const defaultTarget: Entity = {
target: noTargetValue,
- options: {},
- auth: {
- type: "unset" as const,
- },
+ params: {},
};
export function InputPaytoForm<T>({
@@ -187,110 +176,91 @@ export function InputPaytoForm<T>({
label,
tooltip,
}: Props<keyof T>): VNode {
- const { value: paytos, onChange, required } = useField<T>(name);
-
- const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget);
+ const { value: initialValueStr, onChange } = useField<T>(name);
- 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 initialPayto = parsePaytoUri(initialValueStr ?? "")
+ const paths = !initialPayto ? [] : initialPayto.targetPath.split("/")
+ const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
+ const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
+ const initial: Entity = initialPayto === undefined ? defaultTarget : {
+ target: initialPayto.targetType,
+ params: initialPayto.params,
+ path1: initialPath1,
+ path2: initialPath2,
}
- const { i18n } = useTranslationContext();
+ const [value, setValue] = useState<Partial<Entity>>(initial)
- 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.href;
+ const { i18n } = useTranslationContext();
const errors: FormErrors<Entity> = {
target:
- value.target === noTargetValue && !paytos.length
+ value.target === noTargetValue
? i18n.str`required`
: undefined,
path1: !value.path1
? i18n.str`required`
: value.target === "iban"
- ? validateIBAN(value.path1, i18n)
- : value.target === "bitcoin"
- ? validateBitcoin(value.path1, i18n)
- : value.target === "ethereum"
- ? validateEthereum(value.path1, i18n)
- : undefined,
+ ? 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.str`required`
: undefined
: undefined,
- options: undefinedIfEmpty({
- "receiver-name": !value.options?.["receiver-name"]
+ params: undefinedIfEmpty({
+ "receiver-name": !value.params?.["receiver-name"]
? i18n.str`required`
: undefined,
}),
- auth: !value.auth
- ? undefined
- : undefinedIfEmpty({
- username:
- value.auth.type === "basic" && !value.auth.username
- ? i18n.str`required`
- : undefined,
- password:
- value.auth.type === "basic" && !value.auth.password
- ? i18n.str`required`
- : undefined,
- repeat:
- value.auth.type === "basic" && !value.auth.repeat
- ? i18n.str`required`
- : value.auth.repeat !== value.auth.password
- ? i18n.str`is not the same`
- : undefined,
- }),
};
const hasErrors = Object.keys(errors).some(
(k) => (errors as any)[k] !== undefined,
);
+ const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({
+ targetType: value.target,
+ targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""),
+ params: value.params ?? {} as any,
+ isKnown: false,
+ })
+ useEffect(() => {
+ onChange(str as any)
+ }, [str])
- const submit = useCallback((): void => {
- const accounts: MerchantBackend.Instances.MerchantBankAccount[] = paytos;
- const alreadyExists =
- accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
- if (!alreadyExists) {
- const newValue: MerchantBackend.Instances.MerchantBankAccount = {
- payto_uri: paytoURL,
- };
- if (value.auth) {
- if (value.auth.url) {
- newValue.credit_facade_url = value.auth.url;
- }
- if (value.auth.type === "none") {
- newValue.credit_facade_credentials = {
- type: "none",
- };
- }
- if (value.auth.type === "basic") {
- newValue.credit_facade_credentials = {
- type: "basic",
- username: value.auth.username ?? "",
- password: value.auth.password ?? "",
- };
- }
- }
- onChange([newValue, ...accounts] as any);
- }
- valueHandler(defaultTarget);
- }, [value]);
+ // const submit = useCallback((): void => {
+ // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos;
+ // // const alreadyExists =
+ // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
+ // // if (!alreadyExists) {
+ // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = {
+ // payto_uri: paytoURL,
+ // };
+ // if (value.auth) {
+ // if (value.auth.url) {
+ // newValue.credit_facade_url = value.auth.url;
+ // }
+ // if (value.auth.type === "none") {
+ // newValue.credit_facade_credentials = {
+ // type: "none",
+ // };
+ // }
+ // if (value.auth.type === "basic") {
+ // newValue.credit_facade_credentials = {
+ // type: "basic",
+ // username: value.auth.username ?? "",
+ // password: value.auth.password ?? "",
+ // };
+ // }
+ // }
+ // onChange(newValue as any);
+ // // }
+ // // valueHandler(defaultTarget);
+ // }, [value]);
//FIXME: translating plural singular
return (
@@ -299,11 +269,11 @@ export function InputPaytoForm<T>({
name="tax"
errors={errors}
object={value}
- valueHandler={valueHandler}
+ valueHandler={setValue}
>
<InputSelector<Entity>
name="target"
- label={i18n.str`Target type`}
+ label={i18n.str`Account type`}
tooltip={i18n.str`Method to use for wire transfer`}
values={targets}
toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)}
@@ -400,150 +370,15 @@ export function InputPaytoForm<T>({
{value.target !== noTargetValue && (
<Fragment>
<Input
- name="options.receiver-name"
+ name="params.receiver-name"
label={i18n.str`Name`}
tooltip={i18n.str`Bank account owner's name.`}
/>
- <InputWithAddon
- name="auth.url"
- label={i18n.str`Account info URL`}
- help="https://bank.com"
- expand
- tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
- />
- <InputSelector
- name="auth.type"
- label={i18n.str`Auth type`}
- tooltip={i18n.str`Choose the authentication type for the account info URL`}
- values={accountAuthType}
- toStr={(str) => {
- // if (str === "unset") {
- // return "Without change";
- // }
- if (str === "none") return "Without authentication";
- return "Username and password";
- }}
- />
- {value.auth?.type === "basic" ? (
- <Fragment>
- <Input
- name="auth.username"
- label={i18n.str`Username`}
- tooltip={i18n.str`Username to access the account information.`}
- />
- <Input
- name="auth.password"
- inputType="password"
- label={i18n.str`Password`}
- tooltip={i18n.str`Password to access the account information.`}
- />
- <Input
- name="auth.repeat"
- inputType="password"
- label={i18n.str`Repeat password`}
- />
- </Fragment>
- ) : undefined}
-
- {/* <InputWithAddon
- name="options.credit_credentials"
- label={i18n.str`Account info`}
- inputType={showKey ? "text" : "password"}
- help="From where the merchant can download information about incoming wire transfers to this account"
- expand
- tooltip={i18n.str`Useful to validate the purchase`}
- fromStr={(v) => v.toUpperCase()}
- addonAfter={
- <span class="icon">
- {showKey ? (
- <i class="mdi mdi-eye" />
- ) : (
- <i class="mdi mdi-eye-off" />
- )}
- </span>
- }
- side={
- <span style={{ display: "flex" }}>
- <button
- data-tooltip={
- showKey
- ? i18n.str`show secret key`
- : i18n.str`hide secret key`
- }
- class="button is-info mr-3"
- onClick={(e) => {
- setShowKey(!showKey);
- }}
- >
- {showKey ? (
- <i18n.Translate>hide</i18n.Translate>
- ) : (
- <i18n.Translate>show</i18n.Translate>
- )}
- </button>
- </span>
- }
- /> */}
</Fragment>
)}
- {/**
- * Show the values in the list
- */}
- <div class="field is-horizontal">
- <div class="field-label is-normal" />
- <div class="field-body" style={{ display: "block" }}>
- {paytos.map(
- (v: MerchantBackend.Instances.MerchantBankAccount, 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.payto_uri}
- </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.str`No accounts yet.`}
- {required && (
- <span class="icon has-text-danger is-right">
- <i class="mdi mdi-alert" />
- </span>
- )}
- </div>
- </div>
- {value.target !== noTargetValue && (
- <div class="buttons is-right mt-5">
- <button
- class="button is-info"
- data-tooltip={i18n.str`add tax to the tax list`}
- disabled={hasErrors}
- onClick={submit}
- >
- <i18n.Translate>Add</i18n.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/InputSearchOnList.tsx
index 1c1fcb907..be5800d14 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
@@ -22,32 +22,41 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import emptyImage from "../../assets/empty.png";
-import { MerchantBackend, WithId } from "../../declaration.js";
import { FormErrors, FormProvider } from "./FormProvider.js";
import { InputWithAddon } from "./InputWithAddon.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
-type Entity = MerchantBackend.Products.ProductDetail & WithId;
+type Entity = {
+ id: string,
+ description: string;
+ image?: string;
+ extra?: string;
+};
-export interface Props {
- selected?: Entity;
- onChange: (p?: Entity) => void;
- products: (MerchantBackend.Products.ProductDetail & WithId)[];
+export interface Props<T extends Entity> {
+ selected?: T;
+ onChange: (p?: T) => void;
+ label: TranslatedString;
+ list: T[];
+ withImage?: boolean;
}
-interface ProductSearch {
+interface Search {
name: string;
}
-export function InputSearchProduct({
+export function InputSearchOnList<T extends Entity>({
selected,
onChange,
- products,
-}: Props): VNode {
- const [prodForm, setProdName] = useState<Partial<ProductSearch>>({
+ label,
+ list,
+ withImage,
+}: Props<T>): VNode {
+ const [nameForm, setNameForm] = useState<Partial<Search>>({
name: "",
});
- const errors: FormErrors<ProductSearch> = {
+ const errors: FormErrors<Search> = {
name: undefined,
};
const { i18n } = useTranslationContext();
@@ -55,15 +64,17 @@ export function InputSearchProduct({
if (selected) {
return (
<article class="media">
- <figure class="media-left">
- <p class="image is-128x128">
- <img src={selected.image ? selected.image : emptyImage} />
- </p>
- </figure>
+ {withImage &&
+ <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">
- <i18n.Translate>Product id</i18n.Translate>: <b>{selected.id}</b>
+ <i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b>
</p>
<p>
<i18n.Translate>Description</i18n.Translate>:{" "}
@@ -84,15 +95,15 @@ export function InputSearchProduct({
}
return (
- <FormProvider<ProductSearch>
+ <FormProvider<Search>
errors={errors}
- object={prodForm}
- valueHandler={setProdName}
+ object={nameForm}
+ valueHandler={setNameForm}
>
- <InputWithAddon<ProductSearch>
+ <InputWithAddon<Search>
name="name"
- label={i18n.str`Product`}
- tooltip={i18n.str`search products by it's description or id`}
+ label={label}
+ tooltip={i18n.str`enter description or id`}
addonAfter={
<span class="icon">
<i class="mdi mdi-magnify" />
@@ -100,13 +111,14 @@ export function InputSearchProduct({
}
>
<div>
- <ProductList
- name={prodForm.name}
- list={products}
+ <DropdownList
+ name={nameForm.name}
+ list={list}
onSelect={(p) => {
- setProdName({ name: "" });
+ setNameForm({ name: "" });
onChange(p);
}}
+ withImage={!!withImage}
/>
</div>
</InputWithAddon>
@@ -114,13 +126,14 @@ export function InputSearchProduct({
);
}
-interface ProductListProps {
+interface DropdownListProps<T extends Entity> {
name?: string;
- onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void;
- list: (MerchantBackend.Products.ProductDetail & WithId)[];
+ onSelect: (p: T) => void;
+ list: T[];
+ withImage: boolean;
}
-function ProductList({ name, onSelect, list }: ProductListProps) {
+function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) {
const { i18n } = useTranslationContext();
if (!name) {
/* FIXME
@@ -149,7 +162,7 @@ function ProductList({ name, onSelect, list }: ProductListProps) {
{!filtered.length ? (
<div class="dropdown-item">
<i18n.Translate>
- no products found with that description
+ no match found with that description or id
</i18n.Translate>
</div>
) : (
@@ -161,18 +174,20 @@ function ProductList({ name, onSelect, list }: ProductListProps) {
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 }}
- />
+ {withImage &&
+ <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>
+ }
<div class="media-content">
<div class="content">
<p>
- <strong>{p.id}</strong> <small>{p.price}</small>
+ <strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined}
<br />
{p.description}
</p>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
index 61ddf3c84..f95dfcd05 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
@@ -56,7 +56,7 @@ export function InputToggle<T>({
return (
<div class="field is-horizontal">
<div class="field-label is-normal">
- <label class="label" style={{ width: 200 }}>
+ <label class="label" >
{label}
{tooltip && (
<span class="icon has-tooltip-right" data-tooltip={tooltip}>
@@ -65,7 +65,7 @@ export function InputToggle<T>({
)}
</label>
</div>
- <div class="field-body is-flex-grow-1">
+ <div class="field-body is-flex-grow-3">
<div class="field">
<p class={expand ? "control is-expanded" : "control"}>
<label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index 24380ce98..b75dc83b3 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -24,14 +24,13 @@ import { Fragment, h, VNode } from "preact";
import { useBackendContext } from "../../context/backend.js";
import { Entity } from "../../paths/admin/create/CreatePage.js";
import { Input } from "../form/Input.js";
-import { InputCurrency } from "../form/InputCurrency.js";
import { InputDuration } from "../form/InputDuration.js";
import { InputGroup } from "../form/InputGroup.js";
import { InputImage } from "../form/InputImage.js";
import { InputLocation } from "../form/InputLocation.js";
-import { InputPaytoForm } from "../form/InputPaytoForm.js";
-import { InputWithAddon } from "../form/InputWithAddon.js";
import { InputSelector } from "../form/InputSelector.js";
+import { InputToggle } from "../form/InputToggle.js";
+import { InputWithAddon } from "../form/InputWithAddon.js";
export function DefaultInstanceFormFields({
readonlyId,
@@ -85,28 +84,10 @@ export function DefaultInstanceFormFields({
tooltip={i18n.str`Logo image.`}
/>
- <InputPaytoForm<Entity>
- name="accounts"
- label={i18n.str`Bank account`}
- tooltip={i18n.str`URI specifying bank account for crediting revenue.`}
- />
-
- <InputCurrency<Entity>
- name="default_max_deposit_fee"
- label={i18n.str`Default max deposit fee`}
- tooltip={i18n.str`Maximum deposit fees this merchant is willing to pay per order by default.`}
- />
-
- <InputCurrency<Entity>
- name="default_max_wire_fee"
- label={i18n.str`Default max wire fee`}
- tooltip={i18n.str`Maximum wire fees this merchant is willing to pay per wire transfer by default.`}
- />
-
- <Input<Entity>
- name="default_wire_fee_amortization"
- label={i18n.str`Default wire fee amortization`}
- tooltip={i18n.str`Number of orders excess wire transfer fees will be divided by to compute per order surcharge.`}
+ <InputToggle<Entity>
+ name="use_stefan"
+ label={i18n.str`Pay transaction fee`}
+ tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`}
/>
<InputGroup
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index f3cf80b92..be2f8dde5 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -25,6 +25,7 @@ import { useBackendContext } from "../../context/backend.js";
import { useConfigContext } from "../../context/config.js";
import { useInstanceKYCDetails } from "../../hooks/instance.js";
import { LangSelector } from "./LangSelector.js";
+import { useCredentialsChecker } from "../../hooks/backend.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -36,6 +37,7 @@ interface Props {
instance: string;
admin?: boolean;
mimic?: boolean;
+ isPasswordOk: boolean;
}
export function Sidebar({
@@ -45,6 +47,7 @@ export function Sidebar({
onLogout,
admin,
mimic,
+ isPasswordOk
}: Props): VNode {
const config = useConfigContext();
const backend = useBackendContext();
@@ -53,7 +56,7 @@ export function Sidebar({
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
return (
- <aside class="aside is-placed-left is-expanded">
+ <aside class="aside is-placed-left is-expanded" style={{ overflowY: "scroll" }}>
{mobile && (
<div
class="footer"
@@ -78,10 +81,10 @@ export function Sidebar({
</div>
</div>
<div class="menu is-menu-main">
- {instance ? (
+ {isPasswordOk && instance ? (
<Fragment>
<ul class="menu-list">
- <li>
+ <li>
<a href={"/orders"} class="has-icon">
<span class="icon">
<i class="mdi mdi-cash-register" />
@@ -104,7 +107,7 @@ export function Sidebar({
<li>
<a href={"/transfers"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-bank" />
+ <i class="mdi mdi-arrow-left-right" />
</span>
<span class="menu-item-label">
<i18n.Translate>Transfers</i18n.Translate>
@@ -137,12 +140,22 @@ export function Sidebar({
</p>
<ul class="menu-list">
<li>
- <a href={"/update"} class="has-icon">
+ <a href={"/bank"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-square-edit-outline" />
+ <i class="mdi mdi-bank" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Bank account</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/validators"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-lock" />
</span>
<span class="menu-item-label">
- <i18n.Translate>Account</i18n.Translate>
+ <i18n.Translate>Validators</i18n.Translate>
</span>
</a>
</li>
@@ -164,6 +177,26 @@ export function Sidebar({
</span>
</a>
</li>
+ <li>
+ <a href={"/server"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-square-edit-outline" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Server</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/token"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-security" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Access token</i18n.Translate>
+ </span>
+ </a>
+ </li>
</ul>
</Fragment>
) : undefined}
@@ -174,12 +207,12 @@ export function Sidebar({
<li>
<a class="has-icon is-state-info is-hoverable"
onClick={(): void => onShowSettings()}
- >
+ >
<span class="icon">
<i class="mdi mdi-newspaper" />
</span>
<span class="menu-item-label">
- <i18n.Translate>Settings</i18n.Translate>
+ <i18n.Translate>Interface</i18n.Translate>
</span>
</a>
</li>
@@ -211,7 +244,7 @@ export function Sidebar({
</span>
</div>
</li>
- {admin && !mimic && (
+ {isPasswordOk && admin && !mimic && (
<Fragment>
<p class="menu-label">
<i18n.Translate>Instances</i18n.Translate>
@@ -238,19 +271,21 @@ export function Sidebar({
</li>
</Fragment>
)}
- <li>
- <a
- class="has-icon is-state-info is-hoverable"
- onClick={(): void => onLogout()}
- >
- <span class="icon">
- <i class="mdi mdi-logout default" />
- </span>
- <span class="menu-item-label">
- <i18n.Translate>Log out</i18n.Translate>
- </span>
- </a>
- </li>
+ {isPasswordOk &&
+ <li>
+ <a
+ class="has-icon is-state-info is-hoverable"
+ onClick={(): void => onLogout()}
+ >
+ <span class="icon">
+ <i class="mdi mdi-logout default" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Log out</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ }
</ul>
</div>
</aside>
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
index cdbae4ae0..cb318906f 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -24,7 +24,7 @@ import { Sidebar } from "./SideBar.js";
function getInstanceTitle(path: string, id: string): string {
switch (path) {
- case InstancePaths.update:
+ case InstancePaths.server:
return `${id}: Settings`;
case InstancePaths.order_list:
return `${id}: Orders`;
@@ -50,6 +50,12 @@ function getInstanceTitle(path: string, id: string): string {
return `${id}: New webhook`;
case InstancePaths.webhooks_update:
return `${id}: Update webhook`;
+ case InstancePaths.validators_list:
+ return `${id}: Validators`;
+ case InstancePaths.validators_new:
+ return `${id}: New validator`;
+ case InstancePaths.validators_update:
+ return `${id}: Update validators`;
case InstancePaths.templates_new:
return `${id}: New template`;
case InstancePaths.templates_update:
@@ -58,6 +64,10 @@ function getInstanceTitle(path: string, id: string): string {
return `${id}: Templates`;
case InstancePaths.templates_use:
return `${id}: Use template`;
+ case InstancePaths.settings:
+ return `${id}: Interface`;
+ case InstancePaths.settings:
+ return `${id}: Interface`;
default:
return "";
}
@@ -77,6 +87,7 @@ interface MenuProps {
onLogout?: () => void;
onShowSettings: () => void;
setInstanceName: (s: string) => void;
+ isPasswordOk: boolean;
}
function WithTitle({
@@ -100,14 +111,15 @@ export function Menu({
path,
admin,
setInstanceName,
+ isPasswordOk
}: MenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
const titleWithSubtitle = title
? title
: !admin
- ? getInstanceTitle(path, instance)
- : getAdminTitle(path, instance);
+ ? getInstanceTitle(path, instance)
+ : getAdminTitle(path, instance);
const adminInstance = instance === "default";
const mimic = admin && !adminInstance;
return (
@@ -129,14 +141,15 @@ export function Menu({
mimic={mimic}
instance={instance}
mobile={mobileOpen}
+ isPasswordOk={isPasswordOk}
/>
)}
{mimic && (
<nav class="level" style={{
zIndex: 100,
- position:"fixed",
- width:"50%",
+ position: "fixed",
+ width: "50%",
marginLeft: "20%"
}}>
<div class="level-item has-text-centered has-background-warning">
@@ -161,8 +174,9 @@ export function Menu({
interface NotYetReadyAppMenuProps {
title: string;
- onLogout?: () => void;
onShowSettings: () => void;
+ onLogout?: () => void;
+ isPasswordOk: boolean;
}
interface NotifProps {
@@ -181,8 +195,8 @@ export function NotificationCard({
n.type === "ERROR"
? "message is-danger"
: n.type === "WARN"
- ? "message is-warning"
- : "message is-info"
+ ? "message is-warning"
+ : "message is-info"
}
>
<div class="message-header">
@@ -201,10 +215,36 @@ export function NotificationCard({
);
}
+interface NotConnectedAppMenuProps {
+ title: string;
+}
+export function NotConnectedAppMenu({
+ title,
+}: NotConnectedAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ </div>
+ );
+}
+
export function NotYetReadyAppMenu({
onLogout,
onShowSettings,
title,
+ isPasswordOk
}: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
@@ -222,7 +262,7 @@ export function NotYetReadyAppMenu({
title={title}
/>
{onLogout && (
- <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} />
+ <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} />
)}
</div>
);
diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
index b2ec4dd11..377d9c1ba 100644
--- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
@@ -20,7 +20,7 @@ import { MerchantBackend, WithId } from "../../declaration.js";
import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js";
import { FormErrors, FormProvider } from "../form/FormProvider.js";
import { InputNumber } from "../form/InputNumber.js";
-import { InputSearchProduct } from "../form/InputSearchProduct.js";
+import { InputSearchOnList } from "../form/InputSearchOnList.js";
type Form = {
product: MerchantBackend.Products.ProductDetail & WithId;
@@ -95,10 +95,12 @@ export function InventoryProductForm({
return (
<FormProvider<Form> errors={errors} object={state} valueHandler={setState}>
- <InputSearchProduct
+ <InputSearchOnList
+ label={i18n.str`Search product`}
selected={state.product}
onChange={(p) => setState((v) => ({ ...v, product: p }))}
- products={inventory}
+ list={inventory}
+ withImage
/>
{state.product && (
<div class="columns mt-5">
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index 7956a9ea5..4cd90aa45 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -58,12 +58,12 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
!initial || initial.total_stock === -1
? undefined
: {
- current: initial.total_stock || 0,
- lost: initial.total_lost || 0,
- sold: initial.total_sold || 0,
- address: initial.address,
- nextRestock: initial.next_restock,
- },
+ current: initial.total_stock || 0,
+ lost: initial.total_lost || 0,
+ sold: initial.total_sold || 0,
+ address: initial.address,
+ nextRestock: initial.next_restock,
+ },
});
let errors: FormErrors<Entity> = {};
@@ -148,15 +148,17 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
name="minimum_age"
label={i18n.str`Age restricted`}
tooltip={i18n.str`is this product restricted for customer below certain age?`}
+ help={i18n.str`can be overriden by the order configuration`}
/>
<Input<Entity>
name="unit"
- label={i18n.str`Unit`}
+ label={i18n.str`Unit name`}
tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`}
+ help={i18n.str`exajmple: kg, items or liters`}
/>
<InputCurrency<Entity>
name="price"
- label={i18n.str`Price`}
+ label={i18n.str`Price per unit`}
tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`}
/>
<InputStock