summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-04-20 19:14:57 -0300
committerSebastian <sebasjm@gmail.com>2021-04-20 19:15:02 -0300
commite9482a5c90ee6cfbe647b50520716ed5ea46a944 (patch)
treea65c640c99bf9abd4a5ccebc033e813ff16249db
parent14b76f2c318bf483bd7534c8761aec720d067532 (diff)
downloadmerchant-backoffice-e9482a5c90ee6cfbe647b50520716ed5ea46a944.tar.gz
merchant-backoffice-e9482a5c90ee6cfbe647b50520716ed5ea46a944.tar.bz2
merchant-backoffice-e9482a5c90ee6cfbe647b50520716ed5ea46a944.zip
product stock management
-rw-r--r--CHANGELOG.md5
-rw-r--r--README.md1
-rw-r--r--packages/frontend/src/components/form/Field.tsx12
-rw-r--r--packages/frontend/src/components/form/Input.tsx22
-rw-r--r--packages/frontend/src/components/form/InputArray.tsx10
-rw-r--r--packages/frontend/src/components/form/InputBoolean.tsx10
-rw-r--r--packages/frontend/src/components/form/InputCurrency.tsx11
-rw-r--r--packages/frontend/src/components/form/InputDate.tsx48
-rw-r--r--packages/frontend/src/components/form/InputDuration.tsx4
-rw-r--r--packages/frontend/src/components/form/InputGroup.tsx2
-rw-r--r--packages/frontend/src/components/form/InputImage.tsx26
-rw-r--r--packages/frontend/src/components/form/InputNumber.tsx44
-rw-r--r--packages/frontend/src/components/form/InputSecured.tsx10
-rw-r--r--packages/frontend/src/components/form/InputSelector.tsx12
-rw-r--r--packages/frontend/src/components/form/InputStock.stories.tsx110
-rw-r--r--packages/frontend/src/components/form/InputStock.tsx171
-rw-r--r--packages/frontend/src/components/form/InputTaxes.tsx93
-rw-r--r--packages/frontend/src/components/form/InputWithAddon.tsx14
-rw-r--r--packages/frontend/src/components/product/ProductForm.tsx70
-rw-r--r--packages/frontend/src/hooks/order.ts8
-rw-r--r--packages/frontend/src/hooks/product.ts13
-rw-r--r--packages/frontend/src/messages/en.po125
-rw-r--r--packages/frontend/src/paths/admin/create/CreatePage.tsx5
-rw-r--r--packages/frontend/src/paths/admin/list/Table.tsx1
-rw-r--r--packages/frontend/src/paths/instance/orders/create/CreatePage.tsx22
-rw-r--r--packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx8
-rw-r--r--packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx3
-rw-r--r--packages/frontend/src/paths/instance/orders/details/DetailPage.tsx11
-rw-r--r--packages/frontend/src/paths/instance/orders/list/Table.tsx3
-rw-r--r--packages/frontend/src/paths/instance/products/create/CreatePage.tsx4
-rw-r--r--packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx6
-rw-r--r--packages/frontend/src/paths/instance/products/create/index.tsx2
-rw-r--r--packages/frontend/src/paths/instance/products/list/Table.tsx82
-rw-r--r--packages/frontend/src/paths/instance/products/list/index.tsx7
-rw-r--r--packages/frontend/src/paths/instance/products/update/UpdatePage.tsx4
-rw-r--r--packages/frontend/src/paths/instance/update/UpdatePage.tsx5
-rw-r--r--packages/frontend/src/schemas/index.ts28
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]
diff --git a/README.md b/README.md
index fd031e1..13fbeb6 100644
--- a/README.md
+++ b/README.md
@@ -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