merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit da73a14c6909150b5779574dfc0036b27a3752e8
parent 0e365961bee9abf78fa7a0dc3eaae91a8985af3e
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue, 23 Mar 2021 14:09:10 -0300

pagination into the orders

Diffstat:
MCHANGELOG.md | 22++++------------------
Mpackages/frontend/src/ApplicationReadyRoutes.tsx | 6+++---
Mpackages/frontend/src/InstanceRoutes.tsx | 69++++++++++++++++++++++++++++++++++++++++++---------------------------
Mpackages/frontend/src/components/form/Field.tsx | 4++--
Mpackages/frontend/src/components/menu/index.tsx | 12++++++------
Mpackages/frontend/src/hooks/backend.ts | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mpackages/frontend/src/index.tsx | 7-------
Mpackages/frontend/src/messages/en.po | 4++--
Mpackages/frontend/src/paths/admin/create/CreatePage.tsx | 42++++++++++++++++++++++++++++++++++--------
Mpackages/frontend/src/paths/admin/create/index.tsx | 133+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mpackages/frontend/src/paths/instance/orders/list/Table.tsx | 59+++++++++++++++++++++++++++++------------------------------
Mpackages/frontend/src/paths/instance/orders/list/index.tsx | 47+++++++++++++++++++++++++++++++++++------------
Mpackages/frontend/src/utils/constants.ts | 4++++
13 files changed, 308 insertions(+), 185 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -5,7 +5,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Future work] - - notifications should tale place between title and content, and not disapear (#6788) - complete product list information (#6792) - complete order list information (#6793) - gettext templates should be generated from the source code (#6791) @@ -27,30 +26,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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: main action => refund - * exchange errors - * existing refund - -new refund, amount, < allow - -product -increase total -increase lost -changes taxes -changes prices -update descripcion - -deletiing product - + - product detail: we could have some button that brings us to thedetailed screen for the product + - ## [Unreleased] +## [0.0.5] - 2021-03-18 - change the admin title to "instances" if we are listing the instances and "settings: $ID" on updating instances (#6790) - update title with: Taler Backoffice: $PAGE_TITLE (#6790) - paths should be /orders instead of /o (same others) - if there is enough space for tables in mobile, make the scrollables (#6789) - show create default instance if it is not already - - notifications should tale place between title and content, and not disapear + - notifications should tale place between title and content, and not disapear (#6788) - create a loading page to be use when the data is not ready (test at #/loading) - confirmation page when creating instances diff --git a/packages/frontend/src/ApplicationReadyRoutes.tsx b/packages/frontend/src/ApplicationReadyRoutes.tsx @@ -32,7 +32,7 @@ interface Props { } export function ApplicationReadyRoutes({ }: Props): VNode { const i18n = useMessageTemplate(); - const { url: currentBaseUrl, changeBackend, updateToken, clearAllTokens } = useBackendContext(); + const { url: backendURL, changeBackend, updateToken, clearAllTokens } = useBackendContext(); const updateLoginStatus = (url: string, token?: string) => { changeBackend(url); @@ -61,7 +61,7 @@ export function ApplicationReadyRoutes({ }: Props): VNode { </Fragment> } if (list.notfound) { - const path = new URL(currentBaseUrl).pathname + const path = new URL(backendURL).pathname const match = INSTANCE_ID_LOOKUP.exec(path) if (!match || !match[1]) { // this should be rare becuase @@ -71,7 +71,7 @@ export function ApplicationReadyRoutes({ }: Props): VNode { <NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} /> <NotificationCard notification={{ message: i18n`Couldnt access the server`, - description: i18n`Could not infer instance id from url ${currentBaseUrl}`, + description: i18n`Could not infer instance id from url ${backendURL}`, type: 'ERROR', }} /> diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx @@ -55,7 +55,7 @@ import { NotificationCard } from './components/menu'; import { Loading } from './components/exception/loading'; export enum InstancePaths { - details = '/', + // details = '/', update = '/update', product_list = '/products', @@ -63,15 +63,15 @@ export enum InstancePaths { product_new = '/product/new', order_list = '/orders', - order_update = '/p/:oid/update', - order_new = '/o/new', + order_update = '/order/:oid/update', + // order_new = '/oreder/new', - tips_list = '/tips', - tips_update = '/tip/:rid/update', - tips_new = '/tip/new', + // tips_list = '/tips', + // tips_update = '/tip/:rid/update', + // tips_new = '/tip/new', transfers_list = '/transfers', - transfers_new = '/transfer/new', + // transfers_new = '/transfer/new', } export enum AdminPaths { @@ -197,7 +197,8 @@ export function InstanceRoutes({ id, admin }: Props): VNode { /> } - <Route path={InstancePaths.details} + <Route path="/" component={Redirect} to={InstancePaths.order_list} /> + {/* component={DetailPage} onUnauthorized={() => { @@ -212,14 +213,14 @@ export function InstanceRoutes({ id, admin }: Props): VNode { return <Fragment> <NotificationCard notification={{ message: 'No default instance', - description: 'in order to use merchant backend, you should create the default instance', + description: 'in order to use merchant backoffice, you should create the default instance', type: 'INFO' }} /> <InstanceCreatePage onError={() => null} onConfirm={() => null} /> </Fragment> } return <NotFoundPage /> - }} + }} onLoadError={(error: SwrError) => { return <Fragment> @@ -227,7 +228,7 @@ export function InstanceRoutes({ id, admin }: Props): VNode { <LoginPage onConfirm={updateLoginStatus} /> </Fragment> }} - /> + /> */} <Route path={InstancePaths.update} component={InstanceUpdatePage} @@ -251,10 +252,12 @@ export function InstanceRoutes({ id, admin }: Props): VNode { return <Fragment> <NotificationCard notification={{ message: 'No default instance', - description: 'in order to use merchant backend, you should create the default instance', + description: 'in order to use merchant backoffice, you should create the default instance', type: 'INFO' }} /> - <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + <InstanceCreatePage onError={() => null} forceId="default" onConfirm={() => { + route(AdminPaths.list_instances) + }} /> </Fragment> } return <NotFoundPage /> @@ -289,10 +292,12 @@ export function InstanceRoutes({ id, admin }: Props): VNode { return <Fragment> <NotificationCard notification={{ message: 'No default instance', - description: 'in order to use merchant backend, you should create the default instance', + description: 'in order to use merchant backoffice, you should create the default instance', type: 'INFO' }} /> - <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + <InstanceCreatePage onError={() => null} forceId="default" onConfirm={() => { + route(AdminPaths.list_instances) + }} /> </Fragment> } return <NotFoundPage /> @@ -327,10 +332,12 @@ export function InstanceRoutes({ id, admin }: Props): VNode { return <Fragment> <NotificationCard notification={{ message: 'No default instance', - description: 'in order to use merchant backend, you should create the default instance', + description: 'in order to use merchant backoffice, you should create the default instance', type: 'INFO' }} /> - <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + <InstanceCreatePage onError={() => null} forceId="default" onConfirm={() => { + route(AdminPaths.list_instances) + }} /> </Fragment> } return <NotFoundPage /> @@ -346,10 +353,7 @@ export function InstanceRoutes({ id, admin }: Props): VNode { <Route path={InstancePaths.order_update} component={OrderUpdatePage} /> - <Route path={InstancePaths.order_new} - component={OrderCreatePage} - /> - + {/* <Route path={InstancePaths.tips_list} component={TipListPage} @@ -365,7 +369,7 @@ export function InstanceRoutes({ id, admin }: Props): VNode { return <Fragment> <NotificationCard notification={{ message: 'No default instance', - description: 'in order to use merchant backend, you should create the default instance', + description: 'in order to use merchant backoffice, you should create the default instance', type: 'INFO' }} /> <InstanceCreatePage onError={() => null} onConfirm={() => null} /> @@ -386,7 +390,8 @@ export function InstanceRoutes({ id, admin }: Props): VNode { /> <Route path={InstancePaths.tips_new} component={TipCreatePage} - /> + /> + */} <Route path={InstancePaths.transfers_list} component={TransferListPage} @@ -403,10 +408,12 @@ export function InstanceRoutes({ id, admin }: Props): VNode { return <Fragment> <NotificationCard notification={{ message: 'No default instance', - description: 'in order to use merchant backend, you should create the default instance', + description: 'in order to use merchant backoffice, you should create the default instance', type: 'INFO' }} /> - <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + <InstanceCreatePage onError={() => null} forceId="default" onConfirm={() => { + route(AdminPaths.list_instances) + }} /> </Fragment> } return <NotFoundPage /> @@ -419,9 +426,9 @@ export function InstanceRoutes({ id, admin }: Props): VNode { </Fragment> }} /> - <Route path={InstancePaths.transfers_new} + {/* <Route path={InstancePaths.transfers_new} component={TransferCreatePage} - /> + /> */} {/* example of loading page*/} <Route path="/loading" component={Loading} /> @@ -430,3 +437,11 @@ export function InstanceRoutes({ id, admin }: Props): VNode { </InstanceContextProvider>; } + +export function Redirect({ to }: { to: string }): null { + useEffect(() => { + route(to, true) + }) + return null +} + diff --git a/packages/frontend/src/components/form/Field.tsx b/packages/frontend/src/components/form/Field.tsx @@ -82,13 +82,13 @@ export function FormProvider<T>({ object = {}, errors = {}, valueHandler, childr * @returns */ const readField = (object: any, name: string) => { - return name.split('.').reduce((prev, current) => prev[current], object) + return name.split('.').reduce((prev, current) => prev && prev[current], object) } const setValueDeeper = (object: any, names: string[], value: any): any => { if (names.length === 0) return value const [head, ...rest] = names - return {...object, [head]: setValueDeeper(object[head], rest, value) } + return {...object, [head]: setValueDeeper(object[head] || {}, rest, value) } } export function useField<T>(name: keyof T) { diff --git a/packages/frontend/src/components/menu/index.tsx b/packages/frontend/src/components/menu/index.tsx @@ -27,19 +27,19 @@ import { Notification } from "../../utils/types"; function getInstanceTitle(path: string, id: string): string { switch (path) { - case InstancePaths.details: return `${id}` + // case InstancePaths.details: return `${id}` case InstancePaths.update: return `${id}: Settings` case InstancePaths.order_list: return `${id}: Orders` - case InstancePaths.order_new: return `${id}: New order` + // case InstancePaths.order_new: return `${id}: New order` case InstancePaths.order_update: return `${id}: Update order` case InstancePaths.product_list: return `${id}: Products` case InstancePaths.product_new: return `${id}: New product` case InstancePaths.product_update: return `${id}: Update product` - case InstancePaths.tips_list: return `${id}: Tips` - case InstancePaths.tips_new: return `${id}: New tip` - case InstancePaths.tips_update: return `${id}: Update tip` + // case InstancePaths.tips_list: return `${id}: Tips` + // case InstancePaths.tips_new: return `${id}: New tip` + // case InstancePaths.tips_update: return `${id}: Update tip` case InstancePaths.transfers_list: return `${id}: Transfers` - case InstancePaths.transfers_new: return `${id}: New Transfer` + // case InstancePaths.transfers_new: return `${id}: New Transfer` default: return ''; } } diff --git a/packages/frontend/src/hooks/backend.ts b/packages/frontend/src/hooks/backend.ts @@ -19,11 +19,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import useSWR, { mutate, cache } from 'swr'; +import useSWR, { mutate, cache, useSWRInfinite } from 'swr'; import axios from 'axios' import { MerchantBackend } from '../declaration'; import { useBackendContext, useInstanceContext } from '../context/backend'; -import { useEffect, useState } from 'preact/hooks'; +import { useEffect, useMemo, useState } from 'preact/hooks'; +import { MAX_RESULT_SIZE, PAGE_SIZE } from '../utils/constants'; +import { format, max } from 'date-fns'; function mutateAll(re: RegExp) { cache.keys().filter(key => re.test(key)).forEach(key => mutate(key, null)) @@ -35,6 +37,11 @@ interface HttpResponseOk<T> { data: T; unauthorized: boolean; notfound: boolean; + isLoadingMore?: boolean; + loadMore?: () => void; + loadMorePrev?: () => void; + isReachingEnd?: boolean; + isReachingStart?: boolean; } export interface SwrError { @@ -49,6 +56,11 @@ interface HttpResponseError { unauthorized: boolean; notfound: boolean; error?: SwrError; + isLoadingMore?: boolean; + loadMore?: () => void; + loadMorePrev?: () => void; + isReachingEnd?: boolean; + isReachingStart?: boolean; } @@ -98,8 +110,9 @@ function fetcher(url: string, token: string, backend: string) { type YesOrNo = 'yes' | 'no'; -function orderFetcher(url: string, token: string, backend: string, paid?: YesOrNo, refunded?: YesOrNo, wired?: YesOrNo) { - return request(`${backend}${url}`, { token, params: { paid, refunded, wired } }) +function orderFetcher(url: string, token: string, backend: string, paid?: YesOrNo, refunded?: YesOrNo, wired?: YesOrNo, date?: Date, start?: number, delta?: number) { + //%Y-%m-%d %H:%M:%S + return request(`${backend}${url}`, { token, params: { paid, refunded, wired, start, delta, date: date? format(date, 'yyyy-MM-dd HH:mm:ss'): undefined } }) } function transferFetcher(url: string, token: string, backend: string) { @@ -405,13 +418,13 @@ export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend. data?: MerchantBackend.Instances.InstancesResponse; error?: SwrError; } - const [result, setResult] = useState<Result|undefined>(undefined) + const [result, setResult] = useState<Result | undefined>(undefined) useEffect(() => { request(`${url}/private/instances`, { token }) - .then(data => setResult({data})) - .catch(error => setResult({error})) - },[url, token]) + .then(data => setResult({ data })) + .catch(error => setResult({ error })) + }, [url, token]) const data = result?.data const error = result?.error @@ -456,7 +469,9 @@ export interface InstanceOrderFilter { paid?: YesOrNo; refunded?: YesOrNo; wired?: YesOrNo; + date?: Date; } + export function useInstanceOrders(args?: InstanceOrderFilter): HttpResponse<MerchantBackend.Orders.OrderHistory> { const { url: baseUrl, token: baseToken } = useBackendContext(); const { token: instanceToken, id, admin } = useInstanceContext(); @@ -467,9 +482,58 @@ export function useInstanceOrders(args?: InstanceOrderFilter): HttpResponse<Merc url: `${baseUrl}/instances/${id}`, token: instanceToken } - const { data, error } = useSWR<MerchantBackend.Orders.OrderHistory, SwrError>([`/private/orders`, token, url, args?.paid, args?.refunded, args?.wired], orderFetcher) + const [pageBefore, setPageBefore] = useState(0) + const [pageAfter, setPageAfter] = useState(1) + const [start, setStart] = useState<number | undefined>(17); + const totalAfter = pageAfter * PAGE_SIZE; + const totalBefore = pageBefore * PAGE_SIZE; + + const { data:beforeData, error:beforeError } = useSWR<MerchantBackend.Orders.OrderHistory, SwrError>( + [`/private/orders`, token, url, args?.paid, args?.refunded, args?.wired, args?.date, start, totalBefore], + orderFetcher, + ) + const { data:afterData, error:afterError } = useSWR<MerchantBackend.Orders.OrderHistory, SwrError>( + [`/private/orders`, token, url, args?.paid, args?.refunded, args?.wired, args?.date, start ? start+1 : undefined, -totalAfter], + orderFetcher, + ) + + //this will save last result + const [lastBefore, setLastBefore] = useState<MerchantBackend.Orders.OrderHistory | undefined>(undefined) + const [lastAfter, setLastAfter] = useState<MerchantBackend.Orders.OrderHistory | undefined>(undefined) + useEffect(() => { + if (afterData) setLastAfter(afterData) + if (beforeData) setLastBefore(beforeData) + }, [afterData, beforeData]) + + // this has problems when there are some ids missing + const isReachingEnd = afterData && afterData.orders.length < totalAfter; + const isReachingStart = beforeData && beforeData.orders.length < totalBefore; + + const loadMore = () => { + if (totalAfter < MAX_RESULT_SIZE) { + setPageAfter(pageAfter + 1) + } else { + const from = afterData?.orders?.[PAGE_SIZE]?.row_id + console.log('after',start,from) + if (from) setStart(from) + } + } - return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } + const loadMorePrev = () => { + if (totalBefore < MAX_RESULT_SIZE) { + setPageBefore(pageBefore + 1) + } else { + const from = beforeData?.orders?.[PAGE_SIZE-1]?.row_id + console.log('prev',start,from) + if (from) setStart(from) + } + } + + const orders = (beforeData || lastBefore || {orders:[]}).orders.slice().reverse().concat((afterData || lastAfter || {orders:[]}).orders) + const unauthorized = beforeError?.status === 401 || afterError?.status === 401 + const notfound = beforeError?.status === 404 || afterError?.status === 404 + + return { data: {orders}, loadMorePrev, loadMore, isReachingEnd, isReachingStart, unauthorized, notfound } } export function useInstanceTips(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> { diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx @@ -36,13 +36,6 @@ import LoginPage from './paths/login'; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes"; import { NotificationCard, NotYetReadyAppMenu } from "./components/menu"; -export function Redirect({ to }: { to: string }): null { - useEffect(() => { - route(to, true) - }) - return null -} - export default function Application(): VNode { const state = useBackendContextState() diff --git a/packages/frontend/src/messages/en.po b/packages/frontend/src/messages/en.po @@ -227,8 +227,8 @@ msgstr "Amount" msgid "fields.order.summary.label" msgstr "Summary" -msgid "fields.order.paid.label" -msgstr "Paid" +msgid "fields.order.date.label" +msgstr "Date" msgid "Products" msgstr "Products" diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx b/packages/frontend/src/paths/admin/create/CreatePage.tsx @@ -35,28 +35,30 @@ import { InputDuration } from "../../../components/form/InputDuration"; import { InputCurrency } from "../../../components/form/InputCurrency"; import { InputPayto } from "../../../components/form/InputPayto"; -type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & {auth_token?: string} +type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & { auth_token?: string } interface Props { onCreate: (d: Entity) => void; isLoading: boolean; onBack?: () => void; + forceId?: string; } interface KeyValue { [key: string]: string; } -function with_defaults(): Partial<Entity> { +function with_defaults(id?:string): Partial<Entity> { return { + id, default_pay_delay: { d_ms: 1000 }, default_wire_fee_amortization: 10, default_wire_transfer_delay: { d_ms: 2000 }, }; } -export function CreatePage({ onCreate, isLoading, onBack }: Props): VNode { - const [value, valueHandler] = useState(with_defaults()) +export function CreatePage({ onCreate, isLoading, onBack, forceId }: Props): VNode { + const [value, valueHandler] = useState(with_defaults(forceId)) const [errors, setErrors] = useState<FormErrors<Entity>>({}) const submit = (): void => { @@ -85,7 +87,7 @@ export function CreatePage({ onCreate, isLoading, onBack }: Props): VNode { <div class="column is-two-thirds"> <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > - <InputWithAddon<Entity> name="id" addonBefore={`${backend.url}/private/instances/`} /> + <InputWithAddon<Entity> name="id" addonBefore={`${backend.url}/private/instances/`} readonly={!!forceId}/> <Input<Entity> name="name" /> @@ -100,11 +102,35 @@ export function CreatePage({ onCreate, isLoading, onBack }: Props): VNode { <Input<Entity> name="default_wire_fee_amortization" /> <InputGroup name="address"> - <Input<Entity> name="name" /> + <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> <InputGroup name="jurisdiction"> - <Input<Entity> name="name" /> + <Input name="jurisdiction.country" /> + <Input name="jurisdiction.address_lines" inputType="multiline" + toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} + fromStr={(v: string) => v.split('\n')} + /> + <Input name="jurisdiction.building_number" /> + <Input name="jurisdiction.building_name" /> + <Input name="jurisdiction.street" /> + <Input name="jurisdiction.post_code" /> + <Input name="jurisdiction.town_location" /> + <Input name="jurisdiction.town" /> + <Input name="jurisdiction.district" /> + <Input name="jurisdiction.country_subdivision" /> </InputGroup> <InputDuration<Entity> name="default_pay_delay" /> @@ -114,7 +140,7 @@ export function CreatePage({ onCreate, isLoading, onBack }: Props): VNode { </FormProvider> <div class="buttons is-right mt-5"> - { onBack && <button class="button" onClick={onBack} ><Message id="Cancel" /></button> } + {onBack && <button class="button" onClick={onBack} ><Message id="Cancel" /></button>} <button class="button is-success" onClick={submit} ><Message id="Confirm" /></button> </div> diff --git a/packages/frontend/src/paths/admin/create/index.tsx b/packages/frontend/src/paths/admin/create/index.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 { Fragment, h, VNode } from "preact"; +import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { Loading } from "../../../components/exception/loading"; import { FormProvider } from "../../../components/form/Field"; @@ -28,79 +28,54 @@ interface Props { onBack?: () => void; onConfirm: () => void; onError: (error: any) => void; + forceId?: string; } type Entity = MerchantBackend.Instances.InstanceConfigurationMessage; -export default function Create({ onBack, onConfirm, onError }: Props): VNode { +export default function Create({ onBack, onConfirm, onError, forceId }: Props): VNode { const { createInstance } = useAdminMutateAPI(); const [notif, setNotif] = useState<Notification | undefined>(undefined) const [createdOk, setCreatedOk] = useState<Entity | undefined>(undefined); if (createdOk) { - return <div class="columns is-fullwidth is-vcentered content-full-size"> - <div class="column" /> - <div class="column is-half"> - <div class="card"> - <header class="card-header has-background-success"> - <p class="card-header-title has-text-white-ter"> - Instance created successfully + return <CreatedSuccessfully onConfirm={onConfirm}> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">ID</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={createdOk.id} readOnly /> </p> - </header> - <div class="card-content"> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">ID</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={createdOk.id} disabled /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Business Name</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={createdOk.name} disabled /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Token</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input" readonly value={createdOk.auth.token} disabled /> - </p> - </div> - </div> - </div> </div> - <footer class="card-footer"> - <p class="card-footer-item" style={{ border: 'none' }}> - <span> - </span> - </p> - <p class="card-footer-item" style={{ border: 'none' }}> - <span> - </span> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Business Name</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={createdOk.name} disabled /> </p> - <p class="card-footer-item"> - <button class="button is-info" onClick={onConfirm}>Continue</button> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Token</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={createdOk.auth.token} disabled /> </p> - </footer> + </div> </div> </div> - <div class="column" /> - </div> + </CreatedSuccessfully> } return <Fragment> @@ -108,6 +83,7 @@ export default function Create({ onBack, onConfirm, onError }: Props): VNode { <CreatePage onBack={onBack} + forceId={forceId} isLoading={false} onCreate={(d: MerchantBackend.Instances.InstanceConfigurationMessage) => { createInstance(d).then((r) => { @@ -121,4 +97,41 @@ export default function Create({ onBack, onConfirm, onError }: Props): VNode { }) }} /> </Fragment> +} + +interface CreatedSuccessfullyProps { + onConfirm: () => void; + children: ComponentChildren; +} + +function CreatedSuccessfully({ children, onConfirm }: CreatedSuccessfullyProps) { + return <div class="columns is-fullwidth is-vcentered content-full-size"> + <div class="column" /> + <div class="column is-half"> + <div class="card"> + <header class="card-header has-background-success"> + <p class="card-header-title has-text-white-ter"> + Instance created successfully + </p> + </header> + <div class="card-content"> + {children} + </div> + <footer class="card-footer"> + <p class="card-footer-item" style={{ border: 'none' }}> + <span> + </span> + </p> + <p class="card-footer-item" style={{ border: 'none' }}> + <span> + </span> + </p> + <p class="card-footer-item"> + <button class="button is-info" onClick={onConfirm}>Continue</button> + </p> + </footer> + </div> + </div> + <div class="column" /> + </div> } \ No newline at end of file diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx b/packages/frontend/src/paths/instance/orders/list/Table.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { format } from "date-fns"; import { Fragment, h, VNode } from "preact" import { Message } from "preact-messages" import { StateUpdater, useCallback, useEffect, useRef, useState } from "preact/hooks" @@ -32,9 +33,16 @@ interface Props { onDelete: (id: Entity) => void; onCreate: () => void; selected?: boolean; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; } +// onLoadMoreBefore={result.loadMorePrev} hasMoreBefore={!result.isReachingStart} +// onLoadMoreAfter={result.loadMore} hasMoreAfter={!result.isReachingEnd} -export function CardTable({ instances, onCreate, onUpdate, onDelete, selected }: Props): VNode { + +export function CardTable({ instances, onCreate, onUpdate, onDelete, selected, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: Props): VNode { const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]); const [rowSelection, rowSelectionHandler] = useState<string[]>([]) @@ -59,12 +67,6 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected }: <div class="card-header-icon" aria-label="more options"> - <button class={rowSelection.length > 0 ? "button is-danger" : "is-hidden"} - type="button" - onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} - > - Delete - </button> </div> <div class="card-header-icon" aria-label="more options"> <button class="button is-info" type="button" onClick={onCreate}> @@ -77,7 +79,11 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected }: <div class="b-table has-pagination"> <div class="table-wrapper has-mobile-cards"> {instances.length > 0 ? - <Table instances={instances} onUpdate={onUpdate} onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> : + <Table instances={instances} onUpdate={onUpdate} onDelete={onDelete} + rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} + hasMoreAfter={hasMoreAfter} hasMoreBefore={hasMoreBefore} + /> : <EmptyTable /> } </div> @@ -93,53 +99,46 @@ interface TableProps { onUpdate: (id: string) => void; onDelete: (id: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + hasMoreBefore?: boolean; + hasMoreAfter?: boolean; + onLoadMoreAfter?: () => void; } function toggleSelected<T>(id: T): (prev: T[]) => T[] { return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) } -function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, onDelete }: TableProps): VNode { +function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, onDelete, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: TableProps): VNode { return <div class="table-container"> + {onLoadMoreBefore && <button class="button is-fullwidth" disabled={!hasMoreBefore} onClick={onLoadMoreBefore}> load more before </button>} <table class="table is-striped is-hoverable is-fullwidth"> <thead> <tr> - <th class="is-checkbox-cell" style={{minWidth: 50}}> - <label class="b-checkbox checkbox"> - <input type="checkbox" checked={rowSelection.length === instances.length} onClick={(): void => rowSelectionHandler(rowSelection.length === instances.length ? [] : instances.map(i => i.id))} /> - <span class="check" /> - </label> - </th> - <th style={{minWidth: 100}}><Message id="fields.order.amount.label" /></th> - <th style={{minWidth: 500}}><Message id="fields.order.summary.label" /></th> - <th style={{minWidth: 100}}><Message id="fields.order.paid.label" /></th> - <th style={{minWidth: 50}}/> + <th style={{ minWidth: 100 }}><Message id="fields.order.date.label" /></th> + <th style={{ minWidth: 100 }}><Message id="fields.order.amount.label" /></th> + <th style={{ minWidth: 500 }}><Message id="fields.order.summary.label" /></th> + <th style={{ minWidth: 50 }} /> </tr> </thead> <tbody> {instances.map(i => { return <tr> - <td class="is-checkbox-cell"> - <label class="b-checkbox checkbox"> - <input type="checkbox" checked={rowSelection.indexOf(i.id) != -1} onClick={(): void => rowSelectionHandler(toggleSelected(i.id))} /> - <span class="check" /> - </label> - </td> + <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 'pointer' }} >{format(new Date(i.timestamp.t_ms), 'dd/MM/yyyy HH:mm:ss')}</td> <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 'pointer' }} >{i.amount}</td> - <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 'pointer' }} >{i.summary}</td> - <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 'pointer' }} >{i.paid}</td> + <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 'pointer' }} >{i.row_id}</td> <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onDelete(i)}> - Delete + Refund </button> </div> </td> </tr> })} - </tbody> </table> + {onLoadMoreAfter && <button class="button is-fullwidth" disabled={!hasMoreAfter} onClick={onLoadMoreAfter}> load more after </button>} </div> } @@ -148,7 +147,7 @@ function EmptyTable(): VNode { <p> <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> </p> - <p><Message id="There is no instances yet, add more pressing the + sign" /></p> + <p>No orders has been found</p> </div> } diff --git a/packages/frontend/src/paths/instance/orders/list/index.tsx b/packages/frontend/src/paths/instance/orders/list/index.tsx @@ -6,6 +6,8 @@ import { InstanceOrderFilter, SwrError, useInstanceOrders, useOrderMutateAPI, us import { CardTable } from './Table'; import { FormProvider, FormErrors } from "../../../../components/form/Field" import { InputBoolean } from "../../../../components/form/InputBoolean"; +import { InputArray } from '../../../../components/form/InputArray'; +import { format, parse } from 'date-fns'; interface Props { onUnauthorized: () => VNode; @@ -14,35 +16,54 @@ interface Props { onCreate: () => void; } -const fromBooleanToYesAndNo = { - fromBoolean: (b?: boolean) => b === true ? 'yes' : (b === false ? 'no' : undefined), - toBoolean: (b: string) => b === 'yes' ? true : (b === 'no' ? false : undefined) -} +// const fromBooleanToYesAndNo = { +// fromBoolean: (b?: boolean) => b === true ? 'yes' : (b === false ? 'no' : undefined), +// toBoolean: (b: string) => b === 'yes' ? true : (b === 'no' ? false : undefined) +// } export default function ({ onUnauthorized, onLoadError, onCreate, onNotFound }: Props): VNode { + // const [filter, setFilter] = useState<InstanceOrderFilter>({paid:'yes'}) + const date = parse('22/03/2021 11:13:25','dd/MM/yyyyy HH:mm:ss', new Date()) const [filter, setFilter] = useState<InstanceOrderFilter>({}) + + const result = useInstanceOrders(filter) const { createOrder, deleteOrder } = useOrderMutateAPI() const { currency } = useConfigContext() - let instances: (MerchantBackend.Orders.OrderHistoryEntry & {id: string})[]; - + let instances: (MerchantBackend.Orders.OrderHistoryEntry & { id: string })[]; + if (result.unauthorized) return onUnauthorized() if (result.notfound) return onNotFound() if (!result.data) { if (result.error) return onLoadError(result.error) - //if loading asume empty list + //if loading assume empty list instances = [] } else { instances = result.data.orders.map(o => ({ ...o, id: o.order_id })) } + interface Values { + filter: [] + } + + const VALUE_REGEX = /d/ + + const fields = ['wired','refunded','paid'] + + const isPaidActive = filter.paid === 'yes' ? "is-active" : '' + const isRefundedActive = filter.refunded === 'yes' ? "is-active" : '' + const isNotWiredActive = filter.wired === 'no' ? "is-active" : '' + const isAllActive = filter.paid === undefined && filter.refunded === undefined && filter.wired === undefined ? 'is-active' : '' return <section class="section is-main-section"> - <FormProvider<InstanceOrderFilter> errors={{}} object={filter} valueHandler={(e) => setFilter(e as any)} > - <InputBoolean<InstanceOrderFilter> name="paid" threeState {...fromBooleanToYesAndNo} /> - <InputBoolean<InstanceOrderFilter> name="refunded" {...fromBooleanToYesAndNo} /> - <InputBoolean<InstanceOrderFilter> name="wired" {...fromBooleanToYesAndNo} /> - </FormProvider> + <div class="tabs"> + <ul> + <li class={isPaidActive}><a onClick={() => setFilter({paid: 'yes'})}>Paid</a></li> + <li class={isRefundedActive}><a onClick={() => setFilter({refunded: 'yes'})}>Refunded</a></li> + <li class={isNotWiredActive}><a onClick={() => setFilter({wired: 'no'})}>Not wired</a></li> + <li class={isAllActive}><a onClick={() => setFilter({})}>All</a></li> + </ul> + </div> <CardTable instances={instances} onCreate={() => createOrder({ @@ -53,6 +74,8 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onNotFound }: })} onDelete={(order: MerchantBackend.Orders.OrderHistoryEntry) => deleteOrder(order.order_id)} onUpdate={() => null} + onLoadMoreBefore={result.loadMorePrev} hasMoreBefore={!result.isReachingStart} + onLoadMoreAfter={result.loadMore} hasMoreAfter={!result.isReachingEnd} /> </section> } \ No newline at end of file diff --git a/packages/frontend/src/utils/constants.ts b/packages/frontend/src/utils/constants.ts @@ -25,3 +25,6 @@ export const PAYTO_REGEX=/^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\( export const AMOUNT_REGEX=/^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/ export const INSTANCE_ID_LOOKUP = /^\/instances\/([^/]*)\/?$/ + +export const PAGE_SIZE = 3 +export const MAX_RESULT_SIZE = 4; +\ No newline at end of file