diff options
author | Sebastian <sebasjm@gmail.com> | 2021-04-20 19:14:57 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-04-20 19:15:02 -0300 |
commit | e9482a5c90ee6cfbe647b50520716ed5ea46a944 (patch) | |
tree | a65c640c99bf9abd4a5ccebc033e813ff16249db | |
parent | 14b76f2c318bf483bd7534c8761aec720d067532 (diff) | |
download | merchant-backoffice-e9482a5c90ee6cfbe647b50520716ed5ea46a944.tar.gz merchant-backoffice-e9482a5c90ee6cfbe647b50520716ed5ea46a944.tar.bz2 merchant-backoffice-e9482a5c90ee6cfbe647b50520716ed5ea46a944.zip |
product stock management
37 files changed, 797 insertions, 215 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 51e0f45..5d7b60d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,20 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - validate everything using onChange - feature: input as date format - - implement better error handling (improve creation of duplicated instances) - replace Yup and type definition with a taler-library for the purpose (first wait Florian to refactor wallet core) - add more doc style comments - configure eslint - configure prettier - prune scss styles to reduce size - - some way to copy the url of a created instance - fix mobile: some things are still on the left - edit button to go to instance settings - check if there is a way to remove auto async for /routes /components/{async,routes} so it can be turned on when building non-single-bundle - - product detail: we could have some button that brings us to the detailed screen for the product - - input number - - navigation to another instance should not do full refresh - cleanup instance and token management, because code is a mess and can be refactored ## [Unreleased] @@ -100,4 +100,3 @@ Result will be placed at `packages/frontend/single/index.html` * Date-fns: library for manipulating javascript date * messageformat: ICU MessageFormat for Javascript, with support for .po and .mo files - diff --git a/packages/frontend/src/components/form/Field.tsx b/packages/frontend/src/components/form/Field.tsx index 53ef1e6..8d643cf 100644 --- a/packages/frontend/src/components/form/Field.tsx +++ b/packages/frontend/src/components/form/Field.tsx @@ -27,6 +27,7 @@ export interface FormType<T> { initial: Partial<T>; errors: FormErrors<T>; toStr: FormtoStr<T>; + name: string; fromStr: FormfromStr<T>; valueHandler: StateUpdater<Partial<T>>; } @@ -57,15 +58,17 @@ export type FormUpdater<T> = { interface ProviderProps<T> { object?: Partial<T>; errors?: FormErrors<T>; + name?: string; valueHandler: StateUpdater<Partial<T>>; children: ComponentChildren } -export function FormProvider<T>({ object = {}, errors = {}, valueHandler, children }: ProviderProps<T>) { +export function FormProvider<T>({ object = {}, errors = {}, name = '', valueHandler, children }: ProviderProps<T>) { const initial = useMemo(() => object,[]) - const value = useMemo<FormType<T>>(() => ({errors, object, initial, valueHandler, toStr: {}, fromStr: {}}), [errors, object, valueHandler]) + const value = useMemo<FormType<T>>(() => ({errors, object, initial, valueHandler, name, toStr: {}, fromStr: {}}), [errors, object, valueHandler]) + return <FormContext.Provider value={value}> - <form onSubmit={(e) => { + <form class="field" onSubmit={(e) => { e.preventDefault() valueHandler(object) }}> @@ -92,7 +95,7 @@ const setValueDeeper = (object: any, names: string[], value: any): any => { } export function useField<T>(name: keyof T) { - const { errors, object, initial, toStr, fromStr, valueHandler } = useContext<FormType<T>>(FormContext) + const { errors, object, initial, name: formName, toStr, fromStr, valueHandler } = useContext<FormType<T>>(FormContext) type P = typeof name type V = T[P] @@ -108,6 +111,7 @@ export function useField<T>(name: keyof T) { return { error: errors[name], value: readField(object, String(name)), + formName, initial: initial[name], onChange: updateField(name), toStr: toStr[name] ? toStr[name]! : defaultToString, diff --git a/packages/frontend/src/components/form/Input.tsx b/packages/frontend/src/components/form/Input.tsx index e7af633..8b7cbc8 100644 --- a/packages/frontend/src/components/form/Input.tsx +++ b/packages/frontend/src/components/form/Input.tsx @@ -30,26 +30,27 @@ interface Props<T> { 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" /> : +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, expand, children, inputType, inputExtra, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { - const { error, value, onChange } = useField<T>(name); +export function Input<T>({ name, readonly, expand, children, inputType, inputExtra, side, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { + const { error, value, onChange, formName } = useField<T>(name); - const placeholder = useMessage(`fields.instance.${name}.placeholder`); - const tooltip = useMessage(`fields.instance.${name}.tooltip`); + const placeholder = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.placeholder`); + const tooltip = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.tooltip`); return <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> - <Message id={`fields.instance.${name}.label`} /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.label`} /> {tooltip && <span class="icon" data-tooltip={tooltip}> <i class="mdi mdi-information" /> </span>} @@ -61,15 +62,16 @@ export function Input<T>({ name, readonly, expand, children, inputType, inputExt <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))} /> - <Message id={`fields.instance.${name}.help`}> </Message> + name={String(name)} value={toStr(value)} + onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => onChange(fromStr(e.currentTarget.value))} /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.help`}> </Message> {children} </p> {error ? <p class="help is-danger"> <Message id={`validation.${error.type}`} fields={error.params}>{error.message} </Message> </p> : null} </div> + {side} </div> </div>; } diff --git a/packages/frontend/src/components/form/InputArray.tsx b/packages/frontend/src/components/form/InputArray.tsx index 80feec8..f09faa3 100644 --- a/packages/frontend/src/components/form/InputArray.tsx +++ b/packages/frontend/src/components/form/InputArray.tsx @@ -36,10 +36,10 @@ const defaultToString = (f?: any): string => f || '' const defaultFromString = (v: string): any => v as any export function InputArray<T>({ name, readonly, addonBefore, isValid = () => true, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { - const { error: formError, value, onChange } = useField<T>(name); + const { error: formError, value, onChange, formName } = useField<T>(name); const [localError, setLocalError] = useState<ValidationError | null>(null) - const placeholder = useMessage(`fields.instance.${name}.placeholder`); - const tooltip = useMessage(`fields.instance.${name}.tooltip`); + const placeholder = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.placeholder`); + const tooltip = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.tooltip`); const error = formError || localError @@ -50,7 +50,7 @@ export function InputArray<T>({ name, readonly, addonBefore, isValid = () => tru return <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> - <Message id={`fields.instance.${name}.label`} /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.label`} /> {tooltip && <span class="icon" data-tooltip={tooltip}> <i class="mdi mdi-information" /> </span>} @@ -67,7 +67,7 @@ export function InputArray<T>({ name, readonly, addonBefore, isValid = () => tru placeholder={placeholder} readonly={readonly} disabled={readonly} name={String(name)} value={currentValue} onChange={(e): void => setCurrentValue(e.currentTarget.value)} /> - <Message id={`fields.instance.${name}.help`}> </Message> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.help`}> </Message> </p> <p class="control"> <button class="button is-info" onClick={(): void => { diff --git a/packages/frontend/src/components/form/InputBoolean.tsx b/packages/frontend/src/components/form/InputBoolean.tsx index 3a4e36b..0bdbb7b 100644 --- a/packages/frontend/src/components/form/InputBoolean.tsx +++ b/packages/frontend/src/components/form/InputBoolean.tsx @@ -37,10 +37,10 @@ const defaultFromBoolean = (v: boolean | undefined): any => v as any export function InputBoolean<T>({ name, readonly, threeState, expand, fromBoolean = defaultFromBoolean, toBoolean = defaultToBoolean }: Props<keyof T>): VNode { - const { error, value, onChange } = useField<T>(name); + const { error, value, onChange, formName } = useField<T>(name); - const placeholder = useMessage(`fields.instance.${name}.placeholder`); - const tooltip = useMessage(`fields.instance.${name}.tooltip`); + const placeholder = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.placeholder`); + const tooltip = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.tooltip`); const onCheckboxClick = (): void => { const c = toBoolean(value) @@ -51,7 +51,7 @@ export function InputBoolean<T>({ name, readonly, threeState, expand, fromBoolea return <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> - <Message id={`fields.instance.${name}.label`} /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.label`} /> {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}> <i class="mdi mdi-information" /> </span>} @@ -69,7 +69,7 @@ export function InputBoolean<T>({ name, readonly, threeState, expand, fromBoolea <span class="check" /> </label> - <Message id={`fields.instance.${name}.help`}> </Message> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.help`}> </Message> </p> {error ? <p class="help is-danger"> <Message id={`validation.${error.type}`} fields={error.params}>{error.message} </Message> diff --git a/packages/frontend/src/components/form/InputCurrency.tsx b/packages/frontend/src/components/form/InputCurrency.tsx index f932d34..18f0b2b 100644 --- a/packages/frontend/src/components/form/InputCurrency.tsx +++ b/packages/frontend/src/components/form/InputCurrency.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ import { ComponentChildren, h } from "preact"; +import { useConfigContext } from "../../context/backend"; import { Amount } from "../../declaration"; import { InputWithAddon } from "./InputWithAddon"; @@ -26,17 +27,19 @@ export interface Props<T> { name: keyof T; readonly?: boolean; expand?: boolean; - currency: string; addonAfter?: ComponentChildren; children?: ComponentChildren; + side?: ComponentChildren; } -export function InputCurrency<T>({ name, readonly, expand, currency, addonAfter, children }: Props<T>) { - return <InputWithAddon<T> name={name} readonly={readonly} addonBefore={currency} +export function InputCurrency<T>({ name, readonly, expand, addonAfter, children, side }: Props<T>) { + const config = useConfigContext() + return <InputWithAddon<T> name={name} readonly={readonly} addonBefore={config.currency} + side={side} addonAfter={addonAfter} inputType='number' expand={expand} toStr={(v?: Amount) => v?.split(':')[1] || ''} - fromStr={(v: string) => !v ? '' : `${currency}:${v}`} + fromStr={(v: string) => !v ? '' : `${config.currency}:${v}`} inputExtra={{ min: 0 }} children={children} /> diff --git a/packages/frontend/src/components/form/InputDate.tsx b/packages/frontend/src/components/form/InputDate.tsx index ce077e7..a6ee828 100644 --- a/packages/frontend/src/components/form/InputDate.tsx +++ b/packages/frontend/src/components/form/InputDate.tsx @@ -31,19 +31,34 @@ export interface Props<T> { name: keyof T; readonly?: boolean; expand?: boolean; + //FIXME: create separated components InputDate and InputTimestamp + withTimestampSupport?: boolean; } -export function InputDate<T>({ name, readonly, expand }: Props<T>) { +export function InputDate<T>({ name, readonly, expand, withTimestampSupport }: Props<T>) { const [opened, setOpened] = useState(false) - const { error, value, onChange } = useField<T>(name); + const [editing, setEditing] = useState(false) - const placeholder = useMessage(`fields.instance.${name}.placeholder`); - const tooltip = useMessage(`fields.instance.${name}.tooltip`); + const { error, value, onChange, formName } = useField<T>(name); + + const placeholder = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.placeholder`); + const tooltip = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.tooltip`); + + let strValue = '' + if (!value) { + strValue = withTimestampSupport ? 'unknown' : '' + } else if (value instanceof Date) { + strValue = format(value, 'yyyy/MM/dd HH:mm:ss') + } else if (value.t_ms) { + strValue = value.t_ms === 'never' ? + (withTimestampSupport ? 'never' : '') : + format(new Date(value.t_ms), 'yyyy/MM/dd HH:mm:ss') + } return <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> - <Message id={`fields.instance.${name}.label`} /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.label`} /> {tooltip && <span class="icon" data-tooltip={tooltip}> <i class="mdi mdi-information" /> </span>} @@ -53,12 +68,12 @@ export function InputDate<T>({ name, readonly, expand }: Props<T>) { <div class="field"> <div class="field has-addons"> <p class={expand ? "control is-expanded" : "control"}> - <input class="input" type="text" - readonly value={!value ? '' : format(value, 'yyyy/MM/dd HH:mm:ss')} - placeholder="pick a date" + <input class="input" type="text" + readonly value={strValue} + placeholder="pick a date" onClick={() => setOpened(true)} - /> - <Message id={`fields.instance.${name}.help`}> </Message> + /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.help`}> </Message> </p> <div class="control" onClick={() => setOpened(true)}> <a class="button is-static" > @@ -68,11 +83,22 @@ export function InputDate<T>({ name, readonly, expand }: Props<T>) { </div> {error ? <p class="help is-danger"><Message id={`validation.${error.type}`} fields={error.params}>{error.message}</Message></p> : null} </div> + + <button class="button is-info mr-3" onClick={() => onChange(undefined as any)} >clear</button> + {withTimestampSupport && + <button class="button is-info" onClick={() => onChange({ t_ms: 'never' } as any)}>never</button> + } </div> <DatePicker opened={opened} closeFunction={() => setOpened(false)} - dateReceiver={(d) => onChange(d as any)} + dateReceiver={(d) => { + if (withTimestampSupport) { + onChange({t_ms: d.getTime()} as any) + } else { + onChange(d as any) + } + }} /> </div>; } diff --git a/packages/frontend/src/components/form/InputDuration.tsx b/packages/frontend/src/components/form/InputDuration.tsx index 18b55a3..aa349ce 100644 --- a/packages/frontend/src/components/form/InputDuration.tsx +++ b/packages/frontend/src/components/form/InputDuration.tsx @@ -34,8 +34,8 @@ export function InputDuration<T>({ name, expand, readonly }: Props<T>) { const { value } = useField<T>(name); return <InputWithAddon<T> name={name} readonly={readonly} addonAfter={readableDuration(value as any)} expand={expand} - toStr={(v?: RelativeTime) => `${(v && v.d_ms !== "forever" && v.d_ms ? v.d_ms / 1000 : '')}`} - fromStr={(v: string) => ({ d_ms: (parseInt(v, 10) * 1000) || undefined })} + toStr={(v?: RelativeTime) => `${(v && v.d_ms !== "forever" && v.d_ms ? v.d_ms : '')}`} + fromStr={(v: string) => ({ d_ms: (parseInt(v, 10)) || undefined })} /> } diff --git a/packages/frontend/src/components/form/InputGroup.tsx b/packages/frontend/src/components/form/InputGroup.tsx index e80ef66..3208285 100644 --- a/packages/frontend/src/components/form/InputGroup.tsx +++ b/packages/frontend/src/components/form/InputGroup.tsx @@ -37,7 +37,7 @@ export function InputGroup<T>({ name, description, children, alternative}: Props return <div class="card"> <header class="card-header"> <p class={ !group?.hasError ? "card-header-title" : "card-header-title has-text-danger"}> - { description ? description : <Message id={`fields.instance.${String(name)}.label`} /> } + { description ? description : <Message id={`fields.groups.${String(name)}.label`} /> } </p> <button class="card-header-icon" aria-label="more options" onClick={(): void => setActive(!active)}> <span class="icon"> diff --git a/packages/frontend/src/components/form/InputImage.tsx b/packages/frontend/src/components/form/InputImage.tsx index 8227f3b..153cf3d 100644 --- a/packages/frontend/src/components/form/InputImage.tsx +++ b/packages/frontend/src/components/form/InputImage.tsx @@ -22,7 +22,7 @@ import { ComponentChildren, Fragment, h } from "preact"; import { useField } from "./Field"; import emptyImage from "../../assets/empty.png"; import { Message, useMessage } from "preact-messages"; -import { useRef } from "preact/hooks"; +import { useRef, useState } from "preact/hooks"; export interface Props<T> { name: keyof T; @@ -33,16 +33,18 @@ export interface Props<T> { } export function InputImage<T>({ name, readonly, children, expand }: Props<T>) { - const { error, value, onChange } = useField<T>(name); + const { error, value, onChange, formName } = useField<T>(name); - const placeholder = useMessage(`fields.instance.${name}.placeholder`); - const tooltip = useMessage(`fields.instance.${name}.tooltip`); + const placeholder = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.placeholder`); + const tooltip = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.tooltip`); 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"> - <Message id={`fields.instance.${name}.label`} /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.label`} /> {tooltip && <span class="icon" data-tooltip={tooltip}> <i class="mdi mdi-information" /> </span>} @@ -58,7 +60,14 @@ export function InputImage<T>({ name, readonly, children, expand }: Props<T>) { placeholder={placeholder} readonly={readonly} onChange={e => { const f: FileList | null = e.currentTarget.files - if (!f || f.length != 1 || f[0].size > 10000000) return onChange(emptyImage) + if (!f || f.length != 1) { + return onChange(emptyImage) + } + if (f[0].size > 1024*1024) { + setSizeError(true) + return onChange(emptyImage) + } + setSizeError(false) f[0].arrayBuffer().then(b => { const b64 = btoa( new Uint8Array(b) @@ -67,12 +76,15 @@ export function InputImage<T>({ name, readonly, children, expand }: Props<T>) { onChange(`data:${f[0].type};base64,${b64}` as any) }) }} /> - <Message id={`fields.instance.${name}.help`}> </Message> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.help`}> </Message> {children} </p> {error ? <p class="help is-danger"> <Message id={`validation.${error.type}`} fields={error.params}>{error.message} </Message> </p> : null} + {sizeError ? <p class="help is-danger"> + <Message id={`validation.imageSizeLimit`} /> + </p> : null} </div> </div> </div> diff --git a/packages/frontend/src/components/form/InputNumber.tsx b/packages/frontend/src/components/form/InputNumber.tsx new file mode 100644 index 0000000..6362f11 --- /dev/null +++ b/packages/frontend/src/components/form/InputNumber.tsx @@ -0,0 +1,44 @@ +/* + 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/backend"; +import { Amount } from "../../declaration"; +import { Input } from "./Input"; +import { InputWithAddon } from "./InputWithAddon"; + +export interface Props<T> { + name: keyof T; + readonly?: boolean; + expand?: boolean; + side?: ComponentChildren; + children?: ComponentChildren; +} + +export function InputNumber<T>({ name, readonly, expand, children, side }: Props<T>) { + return <InputWithAddon<T> name={name} readonly={readonly} + fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v} + inputType='number' expand={expand} + inputExtra={{ min: 0 }} + children={children} + side={side} + /> +} + diff --git a/packages/frontend/src/components/form/InputSecured.tsx b/packages/frontend/src/components/form/InputSecured.tsx index b29ea4c..d5e36c7 100644 --- a/packages/frontend/src/components/form/InputSecured.tsx +++ b/packages/frontend/src/components/form/InputSecured.tsx @@ -39,10 +39,10 @@ const TokenStatus = ({ prev, post }: any) => { } export function InputSecured<T>({ name, readonly }: Props<T>): VNode { - const { error, value, initial, onChange, toStr, fromStr } = useField<T>(name); + const { error, value, initial, onChange, toStr, fromStr, formName } = useField<T>(name); - const placeholder = useMessage(`fields.instance.${name}.placeholder`, {}); - const tooltip = useMessage(`fields.instance.${name}.tooltip`, {}); + const placeholder = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.placeholder`, {}); + const tooltip = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.tooltip`, {}); const [active, setActive] = useState(false); const [newValue, setNuewValue] = useState("") @@ -51,7 +51,7 @@ export function InputSecured<T>({ name, readonly }: Props<T>): VNode { <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> - <Message id={`fields.instance.${name}.label`} /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.label`} /> {tooltip && <span class="icon" data-tooltip={tooltip}> <i class="mdi mdi-information" /> </span>} @@ -81,7 +81,7 @@ export function InputSecured<T>({ name, readonly }: Props<T>): VNode { onInput={(e): void => { setNuewValue(e.currentTarget.value) }} /> - <Message id={`fields.instance.${name}.help`}> </Message> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.help`}> </Message> </div> <div class="control"> <button class="button is-info" disabled={fromStr(newValue) === value} onClick={(): void => { onChange(fromStr(newValue)); setActive(!active); setNuewValue(""); }} > diff --git a/packages/frontend/src/components/form/InputSelector.tsx b/packages/frontend/src/components/form/InputSelector.tsx index 48ec0a6..36ea728 100644 --- a/packages/frontend/src/components/form/InputSelector.tsx +++ b/packages/frontend/src/components/form/InputSelector.tsx @@ -35,15 +35,15 @@ const defaultToString = (f?: any): string => f || '' const defaultFromString = (v: string): any => v as any export function InputSelector<T>({ name, readonly, expand, values, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { - const { error, value, onChange } = useField<T>(name); + const { error, value, onChange, formName } = useField<T>(name); - const placeholder = useMessage(`fields.instance.${name}.placeholder`); - const tooltip = useMessage(`fields.instance.${name}.tooltip`); + const placeholder = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.placeholder`); + const tooltip = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.tooltip`); return <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> - <Message id={`fields.instance.${name}.label`} /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.label`} /> {tooltip && <span class="icon" data-tooltip={tooltip}> <i class="mdi mdi-information" /> </span>} @@ -53,13 +53,13 @@ export function InputSelector<T>({ name, readonly, expand, values, fromStr = def <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} + name={String(name)} disabled={readonly} readonly={readonly} onChange={(e) => { onChange(fromStr(e.currentTarget.value)) }}> <option>{placeholder}</option> {values .map(v => <option value={toStr(v)}>{toStr(v)}</option>)} </select> - <Message id={`fields.instance.${name}.help`}> </Message> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.help`}> </Message> </p> {error ? <p class="help is-danger"> <Message id={`validation.${error.type}`} fields={error.params}>{error.message} </Message> diff --git a/packages/frontend/src/components/form/InputStock.stories.tsx b/packages/frontend/src/components/form/InputStock.stories.tsx new file mode 100644 index 0000000..5fae9dc --- /dev/null +++ b/packages/frontend/src/components/form/InputStock.stories.tsx @@ -0,0 +1,110 @@ +/* + 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 './Field'; +import { InputStock, Stock } from './InputStock' + +export default { + title: 'Fields/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" /> + <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" /> + <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_ms: 'never' } + } + }) + return <FormProvider<T> name="product" object={state} errors={{}} valueHandler={setState}> + <InputStock<T> name="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_ms: addDays(new Date(), 1).getTime() } + } + }) + return <FormProvider<T> name="product" object={state} errors={{}} valueHandler={setState}> + <InputStock<T> name="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_ms: addDays(new Date(), 1).getTime() } + } + }) + return <FormProvider<T> name="product" object={state} errors={{}} valueHandler={setState}> + <InputStock<T> name="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" alreadyExist /> + <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div> + </FormProvider> +} + + diff --git a/packages/frontend/src/components/form/InputStock.tsx b/packages/frontend/src/components/form/InputStock.tsx new file mode 100644 index 0000000..22690b1 --- /dev/null +++ b/packages/frontend/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 { FormErrors, FormProvider, useField } from "./Field"; +import { useLayoutEffect, useState } from "preact/hooks"; +import { Input } from "./Input"; +import { Message, useMessage } from "preact-messages"; +import { InputGroup } from "./InputGroup"; +import { InputNumber } from "./InputNumber"; +import { InputDate } from "./InputDate"; + +export interface Props<T> { + name: keyof T; + readonly?: boolean; + 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, readonly, alreadyExist }: Props<T>) { + const { error, value, onChange, formName } = useField<T>(name); + + const [errors, setErrors] = useState<FormErrors<Entity>>({}) + + const tooltip = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.tooltip`, {}); + + const [formValue, valueHandler] = useState<Partial<Entity>>(value) + const [addedStock, setAddedStock] = useState<StockDelta>({ incoming: 0, lost: 0 }) + + useLayoutEffect(() => { + console.log(formValue) + + 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"> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.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 has-addons"> + {!alreadyExist ? + <button class="button" onClick={(): void => { valueHandler({ current: 0, lost: 0, sold: 0 } as Stock as any); }} > + <span>Manage stock</span> + </button> : <button class="button" disabled > + <span>Infinite</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 ? { + message: `lost cannot be greater that current + incoming (max ${currentStock + addedStock.incoming})` + } : undefined + } + + const stockUpdateDescription = stockAddedErrors.lost ? '' : ( + !!addedStock.incoming || !!addedStock.lost ? + `current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` : + `current stock will stay at ${currentStock}` + ) + + return <Fragment> + <div class="card"> + <header class="card-header"> + <p class="card-header-title">Stock</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" /> + <InputNumber name="lost" /> + </FormProvider> + + <div class="field is-horizontal"> + <div class="field-label is-normal"></div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + {stockUpdateDescription} + </div> + </div> + </div> + </Fragment> : <InputNumber<Entity> name="current" + side={ + <button class="button is-danger" onClick={(): void => { valueHandler(undefined as any) }} > + <span>without stock</span> + </button> + } + />} + + <InputDate<Entity> name="nextRestock" withTimestampSupport /> + + <InputGroup<Entity> name="address"> + + <Input name="address.country" /> + + <Input name="address.address_lines" inputType="multiline" + toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} + fromStr={(v: string) => v.split('\n')} + /> + + <Input name="address.building_number" /> + <Input name="address.building_name" /> + <Input name="address.street" /> + <Input name="address.post_code" /> + <Input name="address.town_location" /> + <Input name="address.town" /> + <Input name="address.district" /> + <Input name="address.country_subdivision" /> + </InputGroup> + </FormProvider> + </div> + </div> + </Fragment> +} + // ( + + diff --git a/packages/frontend/src/components/form/InputTaxes.tsx b/packages/frontend/src/components/form/InputTaxes.tsx new file mode 100644 index 0000000..666c16e --- /dev/null +++ b/packages/frontend/src/components/form/InputTaxes.tsx @@ -0,0 +1,93 @@ +/* + 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 } from "preact"; +import { InputArray } from "./InputArray"; +import { AMOUNT_REGEX, PAYTO_REGEX } from "../../utils/constants"; +import { useConfigContext } from "../../context/backend"; +import { Amount, MerchantBackend } from "../../declaration"; +import { FormErrors, FormProvider, useField } from "./Field"; +import { useCallback, useState } from "preact/hooks"; +import { InputCurrency } from "./InputCurrency"; +import { Input } from "./Input"; +import { TaxSchema as schema } from '../../schemas' +import * as yup from 'yup'; +import { InputGroup } from "./InputGroup"; + +export interface Props<T> { + name: keyof T; + readonly?: boolean; + isValid?: (e: any) => boolean; +} +type Entity = MerchantBackend.Tax +export function InputTaxes<T>({ name, readonly }: Props<T>) { + const { error, value: taxes, onChange, } = useField<T>(name); + + const [value, valueHandler] = useState<Partial<Entity>>({}) + const [errors, setErrors] = useState<FormErrors<Entity>>({}) + + const submit = useCallback((): void => { + try { + schema.validateSync(value, { abortEarly: false }) + onChange([value as any, ...taxes] as any) + valueHandler({}) + } catch (err) { + const errors = err.inner as yup.ValidationError[] + const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message } }), {}) + setErrors(pathMessages) + } + }, [value]) + + return ( + <InputGroup name="tax" alternative={taxes.length > 0 && <p>this product has {taxes.length} taxes</p>}> + <FormProvider<Entity> name="tax" errors={errors} object={value} valueHandler={valueHandler} > + + <div class="field is-horizontal"> + <div class="field-label is-normal"> + </div> + <div class="field-body" style={{ display: 'block' }}> + {taxes.map((v: any) => <div 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 && 'this product has no taxes'} + </div> + </div> + + <Input<Entity> name="tax" > + currency and value separated with colon <b>USD:2.3</b> + </Input> + + <Input<Entity> name="name" /> + + <div class="buttons is-right mt-5"> + <button class="button is-info" onClick={submit}>add</button> + </div> + + + </FormProvider> + </InputGroup> + ) +} + diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx b/packages/frontend/src/components/form/InputWithAddon.tsx index a983143..73f3fb1 100644 --- a/packages/frontend/src/components/form/InputWithAddon.tsx +++ b/packages/frontend/src/components/form/InputWithAddon.tsx @@ -33,21 +33,22 @@ export interface Props<T> { 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, inputType, inputExtra, addonAfter, toStr = defaultToString, fromStr = defaultFromString }: Props<T>): VNode { - const { error, value, onChange } = useField<T>(name); +export function InputWithAddon<T>({ name, readonly, addonBefore, children, expand, inputType, inputExtra, side, addonAfter, toStr = defaultToString, fromStr = defaultFromString }: Props<T>): VNode { + const { error, value, onChange, formName } = useField<T>(name); - const placeholder = useMessage(`fields.instance.${name}.placeholder`); - const tooltip = useMessage(`fields.instance.${name}.tooltip`); + const placeholder = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.placeholder`); + const tooltip = useMessage(`fields.${!formName ? 'instance' : formName}.${name}.tooltip`); return <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> - <Message id={`fields.instance.${name}.label`} /> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.label`} /> {tooltip && <span class="icon" data-tooltip={tooltip}> <i class="mdi mdi-information" /> </span>} @@ -64,7 +65,7 @@ export function InputWithAddon<T>({ name, readonly, addonBefore, children, expan placeholder={placeholder} readonly={readonly} name={String(name)} value={toStr(value)} onChange={(e): void => onChange(fromStr(e.currentTarget.value))} /> - <Message id={`fields.instance.${name}.help`}> </Message> + <Message id={`fields.${!formName ? 'instance' : formName}.${name}.help`}> </Message> {children} </p> {addonAfter && <div class="control"> @@ -73,6 +74,7 @@ export function InputWithAddon<T>({ name, readonly, addonBefore, children, expan </div> {error ? <p class="help is-danger"><Message id={`validation.${error.type}`} fields={error.params}>{error.message}</Message></p> : null} </div> + {side} </div> </div>; } diff --git a/packages/frontend/src/components/product/ProductForm.tsx b/packages/frontend/src/components/product/ProductForm.tsx index 94bb2b3..5071fac 100644 --- a/packages/frontend/src/components/product/ProductForm.tsx +++ b/packages/frontend/src/components/product/ProductForm.tsx @@ -5,37 +5,57 @@ import { Input } from "../form/Input"; import { InputCurrency } from "../form/InputCurrency"; import { useBackendContext, useConfigContext } from "../../context/backend"; import { MerchantBackend } from "../../declaration"; -import { +import { ProductUpdateSchema as updateSchema, ProductCreateSchema as createSchema, - } from '../../schemas' +} from '../../schemas' import * as yup from 'yup'; -import { InputGroup } from "../form/InputGroup"; -import { useBackendURL } from "../../hooks"; import { InputWithAddon } from "../form/InputWithAddon"; import { InputImage } from "../form/InputImage"; +import { InputTaxes } from "../form/InputTaxes"; +import { InputStock, Stock } from "../form/InputStock"; -type Entity = MerchantBackend.Products.ProductAddDetail +type Entity = MerchantBackend.Products.ProductDetail & { product_id: string } interface Props { onSubscribe: (c: () => Entity | undefined) => void; initial?: Partial<Entity>; - showId?: boolean; + alreadyExist?: boolean; } -export function ProductForm({ onSubscribe, initial, showId }: Props) { - const [value, valueHandler] = useState<Partial<Entity>>(initial || { +export function ProductForm({ onSubscribe, initial, alreadyExist, }: Props) { + const [value, valueHandler] = useState<Partial<Entity & { stock: Stock }>>({ address: {}, description_i18n: {}, taxes: [], - next_restock: { t_ms: 'never' } + next_restock: { t_ms: 'never' }, + ...initial, + stock: !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, + } }) const [errors, setErrors] = useState<FormErrors<Entity>>({}) const submit = useCallback((): Entity | undefined => { try { - (showId ? createSchema : updateSchema).validateSync(value, { abortEarly: false }) - return value as MerchantBackend.Products.ProductAddDetail + (alreadyExist ? updateSchema : createSchema).validateSync(value, { abortEarly: false }) + const stock:Stock = (value as any).stock; + delete (value as any).stock; + + if (!stock) { + value.total_stock = -1 + } else { + value.total_stock = stock.current; + value.total_lost = stock.lost; + value.next_restock = stock.nextRestock instanceof Date ? { t_ms: stock.nextRestock.getTime() } : stock.nextRestock; + value.address = stock.address; + } + console.log(value) + return value as MerchantBackend.Products.ProductDetail & { product_id: string } } catch (err) { const errors = err.inner as yup.ValidationError[] const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message } }), {}) @@ -43,39 +63,25 @@ export function ProductForm({ onSubscribe, initial, showId }: Props) { } }, [value]) - const config = useConfigContext() - useEffect(() => { onSubscribe(submit) }, [submit]) const backend = useBackendContext(); + return <div> - <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > + <FormProvider<Entity> name="product" errors={errors} object={value} valueHandler={valueHandler} > - { showId ? <InputWithAddon<Entity> name="product_id" addonBefore={`${backend.url}/product/`} /> : undefined } + {alreadyExist ? undefined : <InputWithAddon<Entity> name="product_id" addonBefore={`${backend.url}/product/`} /> } + <InputImage<Entity> name="image" /> <Input<Entity> name="description" inputType="multiline" /> <Input<Entity> name="unit" /> - <InputCurrency<Entity> name="price" currency={config.currency} /> + <InputCurrency<Entity> name="price" /> - <Input<Entity> name="total_stock" inputType="number" fromStr={(v) => parseInt(v, 10)} toStr={(v) => "" + v} inputExtra={{ min: 0 }} /> + <InputStock name="stock" alreadyExist={alreadyExist}/> - <InputGroup<Entity> name="address"> - <Input name="address.country" /> - <Input name="address.address_lines" inputType="multiline" - toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} - fromStr={(v: string) => v.split('\n')} - /> - <Input name="address.building_number" /> - <Input name="address.building_name" /> - <Input name="address.street" /> - <Input name="address.post_code" /> - <Input name="address.town_location" /> - <Input name="address.town" /> - <Input name="address.district" /> - <Input name="address.country_subdivision" /> - </InputGroup> + <InputTaxes<Entity> name="taxes" /> </FormProvider> </div> diff --git a/packages/frontend/src/hooks/order.ts b/packages/frontend/src/hooks/order.ts index da056dd..1d7330b 100644 --- a/packages/frontend/src/hooks/order.ts +++ b/packages/frontend/src/hooks/order.ts @@ -92,7 +92,13 @@ export function useOrderDetails(oderId: string): HttpResponse<MerchantBackend.Or const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}` - const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, HttpError>([`/private/orders/${oderId}`, token, url], fetcher) + const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, HttpError>([`/private/orders/${oderId}`, token, url], fetcher, { + refreshInterval:0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }) if (isValidating) return { loading: true, data: data?.data } if (data) return data diff --git a/packages/frontend/src/hooks/product.ts b/packages/frontend/src/hooks/product.ts index c74496d..8ae5b10 100644 --- a/packages/frontend/src/hooks/product.ts +++ b/packages/frontend/src/hooks/product.ts @@ -28,10 +28,7 @@ export function useProductAPI(): ProductAPI { await request(`${url}/private/products`, { method: 'post', token, - data: { - ...data, - image: {} - } + data }); mutateAll(/@"\/private\/products"@/); @@ -118,7 +115,13 @@ export function useProductDetails(productId: string): HttpResponse<MerchantBacke const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}` const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Products.ProductDetail>, HttpError>( - [`/private/products/${productId}`, token, url], fetcher + [`/private/products/${productId}`, token, url], fetcher, { + refreshInterval:0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + } ) if (isValidating) return { loading: true, data: data?.data } diff --git a/packages/frontend/src/messages/en.po b/packages/frontend/src/messages/en.po index cabbc68..da29624 100644 --- a/packages/frontend/src/messages/en.po +++ b/packages/frontend/src/messages/en.po @@ -260,18 +260,12 @@ msgstr "three state boolean" msgid "fields.instance.paid.label" msgstr "Paid" -msgid "fields.instance.refunded.placeholder" -msgstr "" - # msgid "fields.instance.refunded.tooltip" # msgstr "" msgid "fields.instance.refunded.label" msgstr "Refunded" -msgid "fields.instance.wired.placeholder" -msgstr "" - # msgid "fields.instance.wired.tooltip" # msgstr "" @@ -311,9 +305,6 @@ msgstr "Description" msgid "fields.instance.description.placeholder" msgstr "add more information about the refund" -msgid "fields.instance.order_status.placeholder" -msgstr "" - msgid "fields.instance.order_status.label" msgstr "Order status" @@ -374,15 +365,6 @@ msgstr "Stock" msgid "fields.product.sold.label" msgstr "Sold" -msgid "fields.instance.stock.label" -msgstr "add stock" - -msgid "fields.instance.lost.label" -msgstr "add stock lost" - -msgid "fields.instance.price.label" -msgstr "new price" - msgid "fields.instance.inventory_products.label" msgstr "Products from inventory" @@ -520,4 +502,109 @@ msgstr "Unit" msgid "fields.instance.total_stock.label" -msgstr "Total Stock"
\ No newline at end of file +msgstr "Total Stock" + + +msgid "fields.product.description.label" +msgstr "Description" + +msgid "fields.product.unit.label" +msgstr "Unit" + +msgid "fields.product.total_stock.label" +msgstr "Total Stock" + +msgid "fields.product.product_id.label" +msgstr "ID" + +msgid "fields.product.price.label" +msgstr "Price" + +msgid "fields.tax.name.label" +msgstr "Name" + +msgid "fields.tax.tax.label" +msgstr "Amount" + +msgid "fields.groups.address.label" +msgstr "Storage address" + +msgid "fields.stock.current.label" +msgstr "Current stock" + +msgid "fields.stock.lost.label" +msgstr "Lost stock" + +msgid "fields.stock.nextRestock.label" +msgstr "Next Restock" + + +msgid "fields.stock.address.country.label" +msgstr "Country" + +msgid "fields.stock.address.country_subdivision.label" +msgstr "Country Subdivision" + +msgid "fields.stock.address.district.label" +msgstr "District" + +msgid "fields.stock.address.town.label" +msgstr "Town" + +msgid "fields.stock.address.town_location.label" +msgstr "Town Location" + +msgid "fields.stock.address.post_code.label" +msgstr "Post code" + +msgid "fields.stock.address.street.label" +msgstr "Street" + +msgid "fields.stock.address.building_name.label" +msgstr "Building Name" + +msgid "fields.stock.address.building_number.label" +msgstr "Building Number" + +msgid "fields.stock.address.address_lines.label" +msgstr "Address" + +msgid "fields.groups.jurisdiction.label" +msgstr "Jurisdiction" + +msgid "fields.groups.inventory_products.label" +msgstr "Inventory Products" + +msgid "fields.groups.products.label" +msgstr "Products" + +msgid "fields.groups.payments.label" +msgstr "Payments" + +msgid "fields.groups.extra.label" +msgstr "Extra" + + +msgid "fields.instance.stock.label" +msgstr "Stock" + +msgid "fields.instance.lost.label" +msgstr "Lost" + +msgid "fields.instance.price.label" +msgstr "Price" + +msgid "fields.groups.tax.label" +msgstr "Taxes" + +msgid "validation.imageSizeLimit" +msgstr "Image max size is 1 MB" + +msgid "fields.added.incoming.label" +msgstr "Incoming" + +msgid "fields.added.lost.label" +msgstr "Notify Lost" + +msgid "fields.added.price.label" +msgstr "New Price" diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx b/packages/frontend/src/paths/admin/create/CreatePage.tsx index 21cc219..aa8a778 100644 --- a/packages/frontend/src/paths/admin/create/CreatePage.tsx +++ b/packages/frontend/src/paths/admin/create/CreatePage.tsx @@ -77,7 +77,6 @@ export function CreatePage({ onCreate, isLoading, onBack, forceId }: Props): VNo } } const backend = useBackendContext() - const config = useConfigContext() return <div> @@ -95,9 +94,9 @@ export function CreatePage({ onCreate, isLoading, onBack, forceId }: Props): VNo <InputPayto<Entity> name="payto_uris" /> - <InputCurrency<Entity> name="default_max_deposit_fee" currency={config.currency} /> + <InputCurrency<Entity> name="default_max_deposit_fee" /> - <InputCurrency<Entity> name="default_max_wire_fee" currency={config.currency} /> + <InputCurrency<Entity> name="default_max_wire_fee" /> <Input<Entity> name="default_wire_fee_amortization" /> diff --git a/packages/frontend/src/paths/admin/list/Table.tsx b/packages/frontend/src/paths/admin/list/Table.tsx index f0c3242..52c0fb5 100644 --- a/packages/frontend/src/paths/admin/list/Table.tsx +++ b/packages/frontend/src/paths/admin/list/Table.tsx @@ -96,7 +96,6 @@ function toggleSelected<T>(id: T): (prev: T[]) => T[] { } function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, onDelete }: TableProps): VNode { - const { changeBackend, url } = useBackendContext() return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx index 34079cf..1e6b9fd 100644 --- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx @@ -105,12 +105,12 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { summary: order.pricing.summary, products: productList, extra: value.extra, - pay_deadline: value.payments.pay_deadline ? { t_ms: value.payments.pay_deadline.getTime() * 1000 } : undefined, - wire_transfer_deadline: value.payments.pay_deadline ? { t_ms: value.payments.pay_deadline.getTime() * 1000 } : undefined, - refund_deadline: value.payments.refund_deadline ? { t_ms: value.payments.refund_deadline.getTime() * 1000 } : undefined, + pay_deadline: value.payments.pay_deadline ? { t_ms: Math.floor(value.payments.pay_deadline.getTime()/1000)*1000 } : undefined, + wire_transfer_deadline: value.payments.pay_deadline ? { t_ms: Math.floor(value.payments.pay_deadline.getTime()/1000)*1000 } : undefined, + refund_deadline: value.payments.refund_deadline ? { t_ms: Math.floor(value.payments.refund_deadline.getTime()/1000)*1000 } : undefined, max_fee: value.payments.max_fee, max_wire_fee: value.payments.max_wire_fee, - delivery_date: value.payments.delivery_date ? { t_ms: value.payments.delivery_date.getTime() * 1000 } : undefined, + delivery_date: value.payments.delivery_date ? { t_ms: value.payments.delivery_date.getTime() } : undefined, delivery_location: value.payments.delivery_location, }, inventory_products: inventoryList.map(p => ({ @@ -280,17 +280,17 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler as any}> {hasProducts ? <Fragment> - <InputCurrency name="pricing.products_price" readonly currency={config.currency} /> - <InputCurrency name="pricing.products_taxes" readonly currency={config.currency} /> - <InputCurrency name="pricing.order_price" currency={config.currency} + <InputCurrency name="pricing.products_price" readonly /> + <InputCurrency name="pricing.products_taxes" readonly /> + <InputCurrency name="pricing.order_price" addonAfter={value.pricing.order_price !== totalPrice && (discountOrRise < 1 ? `discount of %${Math.round((1 - discountOrRise) * 100)}` : `rise of %${Math.round((discountOrRise - 1) * 100)}`) } /> - <InputCurrency name="pricing.net" readonly currency={config.currency} /> + <InputCurrency name="pricing.net" readonly /> </Fragment> : - <InputCurrency name="pricing.order_price" currency={config.currency} /> + <InputCurrency name="pricing.order_price" /> } <Input name="pricing.summary" inputType="multiline" /> @@ -317,8 +317,8 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <Input name="payments.delivery_location.country_subdivision" /> </InputGroup>} - <InputCurrency name="payments.max_fee" currency={config.currency} /> - <InputCurrency name="payments.max_wire_fee" currency={config.currency} /> + <InputCurrency name="payments.max_fee" /> + <InputCurrency name="payments.max_wire_fee" /> <Input name="payments.wire_fee_amortization" /> <Input name="payments.fullfilment_url" /> </InputGroup> diff --git a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx index 73be77b..5ab94ee 100644 --- a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx +++ b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx @@ -1,10 +1,8 @@ import { h } from "preact"; -import { Message } from "preact-messages"; import { useState } from "preact/hooks"; import { FormErrors, FormProvider } from "../../../../components/form/Field"; -import { Input } from "../../../../components/form/Input"; +import { InputNumber } from "../../../../components/form/InputNumber"; import { InputSearchProduct } from "../../../../components/form/InputSearchProduct"; -import { InputWithAddon } from "../../../../components/form/InputWithAddon"; import { MerchantBackend, WithId } from "../../../../declaration"; import { ProductMap } from "./CreatePage"; @@ -52,9 +50,9 @@ export function InventoryProductForm({ currentProducts, onAddProduct }: Props) { return <FormProvider<Form> errors={errors} object={state} valueHandler={setState}> <InputSearchProduct selected={state.product} onChange={(p) => setState(v => ({ ...v, product: p }))} /> - <Input<Form> name="quantity" inputType="number" fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v} inputExtra={{min:0}}/> + <InputNumber<Form> name="quantity" /> <div class="buttons is-right mt-5"> - <button class="button is-success" onClick={submit} >add</button> + <button class="button is-success" onClick={submit}>add</button> </div> </FormProvider> }
\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx index 92b7413..21fba22 100644 --- a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx +++ b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx @@ -37,7 +37,8 @@ export function NonInventoryProductFrom({ value, onAddProduct }: Props) { const initial: Partial<MerchantBackend.Products.ProductAddDetail> = { ...value, total_stock: value?.quantity || 0, - } + taxes: [] + } return <Fragment> <div class="buttons"> diff --git a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx index c71aca5..23fe8dc 100644 --- a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx +++ b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx @@ -154,7 +154,7 @@ function ClaimedPage({ id, order }: { id: string; order: MerchantBackend.Orders. <div class="title">Payment details</div> <FormProvider<Claimed> errors={errors} object={value} valueHandler={valueHandler} > <Input name="contract_terms.summary" readonly inputType="multiline" /> - <InputCurrency name="contract_terms.amount" readonly currency={config.currency} /> + <InputCurrency name="contract_terms.amount" readonly /> <Input<Claimed> name="order_status" readonly /> </FormProvider> </div> @@ -233,7 +233,6 @@ function PaidPage({ id, order, onRefund }: { id: string; order: MerchantBackend. events.sort((a, b) => a.when.getTime() - b.when.getTime()) const [value, valueHandler] = useState<Partial<Paid>>({ ...order, fee: 'COL:0.1' } as any) const [errors, setErrors] = useState<KeyValue>({}) - const config = useConfigContext() const refundable = new Date().getTime() < order.contract_terms.refund_deadline.t_ms @@ -310,10 +309,10 @@ function PaidPage({ id, order, onRefund }: { id: string; order: MerchantBackend. <div class="title">Payment details</div> <FormProvider<Paid> errors={errors} object={value} valueHandler={valueHandler} > <Input name="contract_terms.summary" readonly inputType="multiline" /> - <InputCurrency name="contract_terms.amount" readonly currency={config.currency} /> - <InputCurrency name="fee" readonly currency={config.currency} /> - {order.refunded && <InputCurrency<Paid> name="refund_amount" readonly currency={config.currency} />} - <InputCurrency<Paid> name="deposit_total" readonly currency={config.currency} /> + <InputCurrency name="contract_terms.amount" readonly /> + <InputCurrency name="fee" readonly /> + {order.refunded && <InputCurrency<Paid> name="refund_amount" readonly />} + <InputCurrency<Paid> name="deposit_total" readonly /> <Input<Paid> name="order_status" readonly /> </FormProvider> </div> diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx b/packages/frontend/src/paths/instance/orders/list/Table.tsx index e0646cc..dc0fcae 100644 --- a/packages/frontend/src/paths/instance/orders/list/Table.tsx +++ b/packages/frontend/src/paths/instance/orders/list/Table.tsx @@ -167,7 +167,6 @@ interface RefundModalProps { } export function RefundModal({ id, onCancel, onConfirm }: RefundModalProps): VNode { - const config = useConfigContext() const result = useOrderDetails(id) type State = { mainReason?: string, description?: string, refund?: string } const [form, setValue] = useState<State>({}) @@ -226,7 +225,7 @@ export function RefundModal({ id, onCancel, onConfirm }: RefundModalProps): VNod </div>} { isRefundable && <FormProvider<State> errors={errors} object={form} valueHandler={(d) => setValue(d as any)}> - <InputCurrency<State> name="refund" currency={config.currency}> + <InputCurrency<State> name="refund"> Max refundable: {totalRefundable} </InputCurrency> <InputSelector name="mainReason" values={['duplicated', 'requested by the customer', 'other']} /> diff --git a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx index 9df2c31..d5fff9d 100644 --- a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx +++ b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx @@ -25,7 +25,7 @@ import { Message } from "preact-messages"; import { ProductForm } from "../../../../components/product/ProductForm"; import { useListener } from "../../../../hooks"; -type Entity = MerchantBackend.Products.ProductAddDetail +type Entity = MerchantBackend.Products.ProductDetail & { product_id: string} interface Props { onCreate: (d: Entity) => void; @@ -44,7 +44,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <div class="columns"> <div class="column" /> <div class="column is-two-thirds"> - <ProductForm onSubscribe={addFormSubmitter} showId /> + <ProductForm onSubscribe={addFormSubmitter} /> <div class="buttons is-right mt-5"> {onBack && <button class="button" onClick={onBack} ><Message id="Cancel" /></button>} diff --git a/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx index 002c695..d6d82f9 100644 --- a/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx +++ b/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx @@ -13,7 +13,7 @@ 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/> */ - import { h } from "preact"; +import { h } from "preact"; import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully"; import { Entity } from "./index"; @@ -24,7 +24,7 @@ interface Props { } export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Props) { - + return <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> <div class="field is-horizontal"> <div class="field-label is-normal"> @@ -33,7 +33,7 @@ export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Prop <div class="field-body is-flex-grow-3"> <div class="field"> <p class="control"> - <img src={entity.image} /> + <img src={entity.image} style={{ width: 200, height: 200 }} /> </p> </div> </div> diff --git a/packages/frontend/src/paths/instance/products/create/index.tsx b/packages/frontend/src/paths/instance/products/create/index.tsx index 81faeeb..86652ab 100644 --- a/packages/frontend/src/paths/instance/products/create/index.tsx +++ b/packages/frontend/src/paths/instance/products/create/index.tsx @@ -47,7 +47,7 @@ export default function ({ onConfirm, onBack }: Props): VNode { <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: MerchantBackend.Products.ProductAddDetail) => { + onCreate={(request: MerchantBackend.Products.ProductDetail & { product_id: string}) => { createProduct(request).then(() => { setCreatedOk(request) }).catch((error) => { diff --git a/packages/frontend/src/paths/instance/products/list/Table.tsx b/packages/frontend/src/paths/instance/products/list/Table.tsx index f097648..bad5530 100644 --- a/packages/frontend/src/paths/instance/products/list/Table.tsx +++ b/packages/frontend/src/paths/instance/products/list/Table.tsx @@ -19,12 +19,14 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Fragment, h, VNode } from "preact" +import { format } from "date-fns" +import { ComponentChildren, Fragment, h, VNode } from "preact" import { Message } from "preact-messages" import { StateUpdater, useEffect, useState } from "preact/hooks" import { FormErrors, FormProvider } from "../../../../components/form/Field" import { Input } from "../../../../components/form/Input" import { InputCurrency } from "../../../../components/form/InputCurrency" +import { InputNumber } from "../../../../components/form/InputNumber" import { useConfigContext } from "../../../../context/backend" import { MerchantBackend, WithId } from "../../../../declaration" import { useProductAPI } from "../../../../hooks/product" @@ -80,13 +82,12 @@ interface TableProps { } function Table({ rowSelection, rowSelectionHandler, instances, onSelect, onUpdate, onDelete }: TableProps): VNode { - const { } = useProductAPI() return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> - <th><Message id="fields.product.image.label" /></th> + <th><Message id="fields.product.image.label" style={{ with: 100 }} /></th> <th><Message id="fields.product.description.label" /></th> <th><Message id="fields.product.sell.label" /></th> <th><Message id="fields.product.taxes.label" /></th> @@ -98,13 +99,27 @@ function Table({ rowSelection, rowSelectionHandler, instances, onSelect, onUpdat </thead> <tbody> {instances.map(i => { + + let restStockInfo = !i.next_restock ? '' : ( + i.next_restock.t_ms === 'never' ? + 'never' : + `restock at ${format(new Date(i.next_restock.t_ms), 'yyyy/MM/dd')}` + ) + let stockInfo: ComponentChildren = ''; + if (i.total_stock < 0) { + stockInfo = 'infinite' + } else { + const totalStock = i.total_stock - i.total_lost - i.total_sold + stockInfo = <label title={restStockInfo}>{totalStock} {i.unit}</label> + } + return <Fragment><tr> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{JSON.stringify(i.image)}</td> + <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} ><img src={i.image} style={{ border: 'solid black 1px', width: 100, height: 100 }} /></td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.description}</td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.price} / {i.unit}</td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{sum(i.taxes)}</td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{difference(i.price, sum(i.taxes))}</td> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.total_stock} {i.unit} ({i.next_restock?.t_ms})</td> + <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{stockInfo}</td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.total_sold} {i.unit}</td> <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> @@ -136,51 +151,62 @@ interface FastProductUpdateFormProps { onCancel: () => void; } interface FastProductUpdate { - stock?: number; - lost?: number; - price?: string; + incoming: number; + lost: number; + price: string; } function FastProductUpdateForm({ product, onUpdate, onCancel }: FastProductUpdateFormProps) { - const [value, valueHandler] = useState<FastProductUpdate>({}) - const config = useConfigContext() + const [value, valueHandler] = useState<FastProductUpdate>({ + incoming: 0, lost: 0, price: product.price + }) + + const currentStock = product.total_stock - product.total_sold - product.total_lost - const errors:FormErrors<FastProductUpdate> = { - lost: !value.lost ? undefined : (value.lost < product.total_lost ? {message: `should be greater than ${product.total_lost}`} : undefined), - stock: !value.stock ? undefined : (value.stock < product.total_stock ? {message: `should be greater than ${product.total_stock}`} : undefined), - price: undefined, + const errors: FormErrors<FastProductUpdate> = { + lost: currentStock + value.incoming < value.lost ? { + message: `lost cannot be greater that current + incoming (max ${currentStock + value.incoming})` + } : undefined } + const stockUpdateDescription = errors.lost ? '' : ( + !!value.incoming || !!value.lost ? + `current stock will change from ${currentStock} to ${currentStock + value.incoming - value.lost}` : + `current stock will stay at ${currentStock}` + ) + const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) const isDirty = Object.keys(value).some(k => !!(value as any)[k]) return <Fragment> - <FormProvider<FastProductUpdate> errors={errors} object={value} valueHandler={valueHandler} > - <div class="columns"> - <div class="column"> - <Input<FastProductUpdate> name="stock" inputType="number" fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v}/> - </div> - <div class="column"> - <Input<FastProductUpdate> name="lost" inputType="number" fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v}/> - </div> - <div class="column"> - <InputCurrency<FastProductUpdate> name="price" currency={config.currency} /> + <FormProvider<FastProductUpdate> name="added" errors={errors} object={value} valueHandler={valueHandler as any} > + <InputNumber<FastProductUpdate> name="incoming" /> + <InputNumber<FastProductUpdate> name="lost" /> + <div class="field is-horizontal"> + <div class="field-label is-normal"></div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + {stockUpdateDescription} + </div> </div> </div> + <InputCurrency<FastProductUpdate> name="price" /> </FormProvider> <div class="buttons is-right mt-5"> <button class="button" onClick={onCancel} ><Message id="Cancel" /></button> <button class="button is-info" disabled={hasErrors || !isDirty} onClick={() => { + return onUpdate({ ...product, - total_stock: value.stock || product.total_stock, - total_lost: value.lost || product.total_lost, - price: value.price || product.price, + total_stock: product.total_stock + value.incoming, + total_lost: product.total_lost+ value.lost, + price: value.price, }) + }}><Message id="Confirm" /></button> </div> - + </Fragment> } diff --git a/packages/frontend/src/paths/instance/products/list/index.tsx b/packages/frontend/src/paths/instance/products/list/index.tsx index c5f5564..baef0c2 100644 --- a/packages/frontend/src/paths/instance/products/list/index.tsx +++ b/packages/frontend/src/paths/instance/products/list/index.tsx @@ -49,13 +49,6 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo if (!result.ok) return onLoadError(result) return <section class="section is-main-section"> - <NotificationCard notification={{ - message: 'DEMO', - type: 'WARN', - description: <ul> - <li>image return object when api says string</li> - </ul> - }} /> <NotificationCard notification={notif} /> <CardTable instances={result.data} diff --git a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx index 40319a8..70f11b3 100644 --- a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx +++ b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx @@ -25,7 +25,7 @@ import { Message } from "preact-messages"; import { ProductForm } from "../../../../components/product/ProductForm"; import { useListener } from "../../../../hooks"; -type Entity = MerchantBackend.Products.ProductPatchDetail & WithId +type Entity = MerchantBackend.Products.ProductDetail & WithId interface Props { onUpdate: (d: Entity) => void; @@ -48,7 +48,7 @@ export function UpdatePage({ product, onUpdate, onBack }: Props): VNode { const p = a() return p as any }) - }} /> + }} alreadyExist /> <div class="buttons is-right mt-5"> {onBack && <button class="button" onClick={onBack} ><Message id="Cancel" /></button>} diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx b/packages/frontend/src/paths/instance/update/UpdatePage.tsx index e77bd38..8b5f7db 100644 --- a/packages/frontend/src/paths/instance/update/UpdatePage.tsx +++ b/packages/frontend/src/paths/instance/update/UpdatePage.tsx @@ -93,7 +93,6 @@ export function UpdatePage({ onUpdate, isLoading, selected, onBack }: Props): VN setErrors(pathMessages) } } - const config = useConfigContext() return <div> <section class="section is-main-section"> @@ -108,9 +107,9 @@ export function UpdatePage({ onUpdate, isLoading, selected, onBack }: Props): VN <InputPayto<Entity> name="payto_uris" /> - <InputCurrency<Entity> name="default_max_deposit_fee" currency={config.currency} /> + <InputCurrency<Entity> name="default_max_deposit_fee" /> - <InputCurrency<Entity> name="default_max_wire_fee" currency={config.currency} /> + <InputCurrency<Entity> name="default_max_wire_fee" /> <Input<Entity> name="default_wire_fee_amortization" inputType="number" /> diff --git a/packages/frontend/src/schemas/index.ts b/packages/frontend/src/schemas/index.ts index 01209df..54a3a8e 100644 --- a/packages/frontend/src/schemas/index.ts +++ b/packages/frontend/src/schemas/index.ts @@ -37,20 +37,14 @@ function listOfPayToUrisAreValid(values?: (string | undefined)[]): boolean { return !!values && values.every(v => v && PAYTO_REGEX.test(v)); } -// function numberToDuration(this: yup.AnySchema, current: any, original: string): Duration { -// if (this.isType(current)) return current; -// const d_ms = parseInt(original, 10) * 1000 -// return { d_ms } -// } - function currencyWithAmountIsValid(value?: string): boolean { return !!value && AMOUNT_REGEX.test(value) } function currencyGreaterThan0(value?: string) { if (value) { try { - const [,amount] = value.split(':') - const intAmount = parseInt(amount,10) + const [, amount] = value.split(':') + const intAmount = parseInt(amount, 10) return intAmount > 0 } catch { return false @@ -160,7 +154,7 @@ export const OrderCreateSchema = yup.object().shape({ .test('future', 'should be in the future', (d) => d ? isFuture(d) : true), }).test('payment', 'dates', (d) => { if (d.pay_deadline && d.refund_deadline && isAfter(d.refund_deadline, d.pay_deadline)) { - return new yup.ValidationError('pay deadline should be greater than refund','asd','payments.pay_deadline') + return new yup.ValidationError('pay deadline should be greater than refund', 'asd', 'payments.pay_deadline') } return true }) @@ -173,7 +167,9 @@ export const ProductCreateSchema = yup.object().shape({ price: yup.string() .required() .test('amount', 'the amount is not valid', currencyWithAmountIsValid), - total_stock: yup.number().required(), + stock: yup.object({ + + }).optional(), }) export const ProductUpdateSchema = yup.object().shape({ @@ -181,5 +177,15 @@ export const ProductUpdateSchema = yup.object().shape({ price: yup.string() .required() .test('amount', 'the amount is not valid', currencyWithAmountIsValid), - total_stock: yup.number().required(), + stock: yup.object({ + + }).optional(), }) + + +export const TaxSchema = yup.object().shape({ + name: yup.string().required().ensure(), + tax: yup.string() + .required() + .test('amount', 'the amount is not valid', currencyWithAmountIsValid), +})
\ No newline at end of file |