diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components')
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/exception/login.tsx | 4 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/form/InputDate.tsx | 11 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx | 305 | ||||
-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.tsx | 4 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx | 31 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx | 81 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/menu/index.tsx | 58 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx | 8 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx | 18 |
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 |