merchant-backoffice

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

commit bed0ebb449a3427b58a007cb1cf0e9f03baf2736
parent ce0b95fb28a9bd50ceeda1efecafd6056c945333
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 14 May 2021 18:18:34 -0300

reserve and tips

Diffstat:
MCHANGELOG.md | 13++++++++++---
Mpackages/frontend/src/InstanceRoutes.tsx | 32+++++++++++++++++++++++++++++---
Mpackages/frontend/src/components/menu/SideBar.tsx | 8++++----
Mpackages/frontend/src/components/modal/index.tsx | 23++++++++++++++++++++++-
Mpackages/frontend/src/declaration.d.ts | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/hooks/tips.ts | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mpackages/frontend/src/paths/instance/details/index.tsx | 2--
Mpackages/frontend/src/paths/instance/orders/list/Table.tsx | 4++--
Apackages/frontend/src/paths/instance/reserves/create/CreatePage.tsx | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/paths/instance/reserves/create/index.tsx | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/paths/instance/reserves/details/DetailPage.tsx | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/paths/instance/reserves/details/index.tsx | 47+++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/paths/instance/reserves/list/CreatedSuccessfully.tsx | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/paths/instance/reserves/list/Table.tsx | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/paths/instance/reserves/list/index.tsx | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/frontend/src/paths/instance/tips/create/index.tsx | 27---------------------------
Dpackages/frontend/src/paths/instance/tips/list/Table.tsx | 150-------------------------------------------------------------------------------
Dpackages/frontend/src/paths/instance/tips/list/index.tsx | 61-------------------------------------------------------------
Dpackages/frontend/src/paths/instance/tips/update/index.tsx | 27---------------------------
Mpackages/frontend/src/schemas/index.ts | 11++++++++++-
22 files changed, 1063 insertions(+), 299 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -23,11 +23,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - unlock a product when is locked - check that there is no place where the taxes are summing up - - translation missing: yup (check for some other dynamic message) + - translation missing: yup (check for some ot her dynamic message) - contract terms - fulfillment url should check absolute url or relative to the merchant domain - - duplicate order - - order + - duplicate order button + - simplify order + - react routing refactor to use query parameters from history + - create taler ui + - contract terms in the wallet + - when backoffice get a response of the merchant that have info about the reponse of the exchange, the error is not readed correctly... see wire transfer + - when creating the first default instance, the page keeps reloading preventing for filling the form + - + ## [Unreleased] - fixed bug when updating token and not admin - showing a yellow bar on non-default instance navigation (admin) diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx @@ -40,6 +40,9 @@ import ProductListPage from './paths/instance/products/list'; import ProductUpdatePage from './paths/instance/products/update'; import TransferListPage from './paths/instance/transfers/list'; import TransferCreatePage from './paths/instance/transfers/create'; +import ReservesCreatePage from './paths/instance/reserves/create'; +import ReservesDetailsPage from './paths/instance/reserves/details'; +import ReservesListPage from './paths/instance/reserves/list'; import InstanceUpdatePage, { Props as InstanceUpdatePageProps } from "./paths/instance/update"; import LoginPage from './paths/login'; import NotFoundPage from './paths/notfound'; @@ -58,9 +61,9 @@ export enum InstancePaths { order_new = '/order/new', order_details = '/order/:oid/details', - // tips_list = '/tips', - // tips_update = '/tip/:rid/update', - // tips_new = '/tip/new', + reserves_list = '/reserves', + reserves_details = '/reserves/:rid/details', + reserves_new = '/reserves/new', transfers_list = '/transfers', transfers_new = '/transfer/new', @@ -253,6 +256,29 @@ export function InstanceRoutes({ id, admin }: Props): VNode { onConfirm={() => { route(InstancePaths.transfers_list) }} onBack={() => { route(InstancePaths.transfers_list) }} /> + + {/** + * reserves pages + */} + <Route path={InstancePaths.reserves_list} component={ReservesListPage} + onUnauthorized={LoginPageAccessDenied} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onSelect={(id: string) => { route(InstancePaths.reserves_details.replace(':rid', id)) }} + onCreate={() => { route(InstancePaths.reserves_new) }} + /> + + <Route path={InstancePaths.reserves_details} component={ReservesDetailsPage} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.reserves_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { route(InstancePaths.reserves_list) }} + /> + + <Route path={InstancePaths.reserves_new} component={ReservesCreatePage} + onConfirm={() => { route(InstancePaths.reserves_list) }} + onBack={() => { route(InstancePaths.reserves_list) }} + /> {/** * Example pages */} diff --git a/packages/frontend/src/components/menu/SideBar.tsx b/packages/frontend/src/components/menu/SideBar.tsx @@ -77,12 +77,12 @@ export function Sidebar({ mobile, instance, onLogout, admin }: Props): VNode { <span class="menu-item-label"><Translate>Transfers</Translate></span> </a> </li> - {/* <li> - <a href="/tips" class="has-icon"> + <li> + <a href="/reserves" class="has-icon"> <span class="icon"><i class="mdi mdi-cash" /></span> - <span class="menu-item-label">Tips</span> + <span class="menu-item-label">Reserves</span> </a> - </li> */} + </li> </ul> <p class="menu-label"><Translate>Connection</Translate></p> <ul class="menu-list"> diff --git a/packages/frontend/src/components/modal/index.tsx b/packages/frontend/src/components/modal/index.tsx @@ -41,7 +41,7 @@ export function ConfirmModal({ active, description, onCancel, onConfirm, childre <div class="modal-background " onClick={onCancel} /> <div class="modal-card"> <header class="modal-card-head"> - {!description ? null : <p class="modal-card-title">{description}</p>} + {!description ? null : <p class="modal-card-title has-text-white-ter">{description}</p>} <button class="delete " aria-label="close" onClick={onCancel} /> </header> <section class="modal-card-body"> @@ -58,6 +58,27 @@ export function ConfirmModal({ active, description, onCancel, onConfirm, childre </div> } +export function ContinueModal({ active, description, onCancel, onConfirm, children, disabled }: Props): VNode { + return <div class={active ? "modal is-active" : "modal"}> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head has-background-success"> + {!description ? null : <p class="modal-card-title">{description}</p>} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body"> + {children} + </section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: '100%' }}> + <button class="button is-success " disabled={disabled} onClick={onConfirm} ><Translate>Continue</Translate></button> + </div> + </footer> + </div> + <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> + </div> +} + export function ClearConfirmModal({ description, onCancel, onClear, onConfirm, children, disabled }: Props & { onClear?: () => void }): VNode { return <div class="modal is-active"> <div class="modal-background " onClick={onCancel} /> diff --git a/packages/frontend/src/declaration.d.ts b/packages/frontend/src/declaration.d.ts @@ -929,6 +929,76 @@ export namespace MerchantBackend { tip_expiration: Timestamp; } + interface ReserveDetail { + // Timestamp when it was established. + creation_time: Timestamp; + + // Timestamp when it expires. + expiration_time: Timestamp; + + // Initial amount as per reserve creation call. + merchant_initial_amount: Amount; + + // Initial amount as per exchange, 0 if exchange did + // not confirm reserve creation yet. + exchange_initial_amount: Amount; + + // Amount picked up so far. + pickup_amount: Amount; + + // Amount approved for tips that exceeds the pickup_amount. + committed_amount: Amount; + + // Array of all tips created by this reserves (possibly empty!). + // Only present if asked for explicitly. + tips?: TipStatusEntry[]; + + // Is this reserve active (false if it was deleted but not purged)? + active: boolean; + } + interface TipStatusEntry { + + // Unique identifier for the tip. + tip_id: HashCode; + + // Total amount of the tip that can be withdrawn. + total_amount: Amount; + + // Human-readable reason for why the tip was granted. + reason: string; + } + + interface TipDetails { + // Amount that we authorized for this tip. + total_authorized: Amount; + + // Amount that was picked up by the user already. + total_picked_up: Amount; + + // Human-readable reason given when authorizing the tip. + reason: string; + + // Timestamp indicating when the tip is set to expire (may be in the past). + expiration: Timestamp; + + // Reserve public key from which the tip is funded. + reserve_pub: EddsaPublicKey; + + // Array showing the pickup operations of the wallet (possibly empty!). + // Only present if asked for explicitly. + pickups?: PickupDetail[]; + } + interface PickupDetail { + // Unique identifier for the pickup operation. + pickup_id: HashCode; + + // Number of planchets involved. + num_planchets: Integer; + + // Total amount requested for this pickup_id. + requested_amount: Amount; + } + } namespace Transfers { diff --git a/packages/frontend/src/hooks/tips.ts b/packages/frontend/src/hooks/tips.ts @@ -20,7 +20,7 @@ import { MerchantBackend } from '../declaration'; import { fetcher, HttpError, HttpResponse, HttpResponseOk, mutateAll, request } from './backend'; -export function useTipsMutateAPI(): TipsMutateAPI { +export function useReservesAPI(): ReserveMutateAPI { const { url: baseUrl, token: adminToken } = useBackendContext(); const { token: instanceToken, id, admin } = useInstanceContext(); @@ -31,48 +31,56 @@ export function useTipsMutateAPI(): TipsMutateAPI { }; const createReserve = async (data: MerchantBackend.Tips.ReserveCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>> => { - mutateAll(/@"\/private\/reserves"@/); - - return request<MerchantBackend.Tips.ReserveCreateConfirmation>(`${url}/private/reserves`, { + const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>(`${url}/private/reserves`, { method: 'post', token, data }); - }; - const authorizeTipReserve = (pub: string, data: MerchantBackend.Tips.TipCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { - mutateAll(/@"\/private\/reserves"@/); + await mutateAll(/@"\/private\/reserves"@/); + + return res + }; - return request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/reserves/${pub}/authorize-tip`, { + const authorizeTipReserve = async (pub: string, data: MerchantBackend.Tips.TipCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { + const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/reserves/${pub}/authorize-tip`, { method: 'post', token, data }); - }; + await mutateAll(/@"\/private\/reserves"@/); - const authorizeTip = (data: MerchantBackend.Tips.TipCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { - mutateAll(/@"\/private\/reserves"@/); + return res + }; - return request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/tips`, { + const authorizeTip = async (data: MerchantBackend.Tips.TipCreateRequest): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => { + const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(`${url}/private/tips`, { method: 'post', token, data }); + + await mutateAll(/@"\/private\/reserves"@/); + + return res }; - const deleteReserve = (pub: string): Promise<HttpResponse<void>> => { - mutateAll(/@"\/private\/reserves"@/); - return request(`${url}/private/reserves/${pub}`, { + const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => { + const res = await request<void>(`${url}/private/reserves/${pub}`, { method: 'delete', token, }); + + await mutateAll(/@"\/private\/reserves"@/); + + return res }; return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve }; } -export interface TipsMutateAPI { +export interface ReserveMutateAPI { createReserve: (data: MerchantBackend.Tips.ReserveCreateRequest) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>; authorizeTipReserve: (id: string, data: MerchantBackend.Tips.TipCreateRequest) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; authorizeTip: (data: MerchantBackend.Tips.TipCreateRequest) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>; @@ -91,9 +99,61 @@ export function useInstanceTips(): HttpResponse<MerchantBackend.Tips.TippingRese const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>, HttpError>([`/private/reserves`, token, url], fetcher) - if (isValidating) return {loading:true, data: data?.data} + if (isValidating) return { loading: true, data: data?.data } + if (data) return data + if (error) return error + return { loading: true } +} + + +export function useReserveDetails(reserveId: string): HttpResponse<MerchantBackend.Tips.ReserveDetail> { + const { url: baseUrl } = useBackendContext(); + const { token, id: instanceId, admin } = useInstanceContext(); + + const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}` + + const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Tips.ReserveDetail>, HttpError>([`/private/reserves/${reserveId}`, token, url], reserveDetailFetcher, { + refreshInterval:0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }) + + if (isValidating) return { loading: true, data: data?.data } + if (data) return data + if (error) return error + return { loading: true } +} + +export function useTipDetails(tipId: string): HttpResponse<MerchantBackend.Tips.TipDetails> { + const { url: baseUrl } = useBackendContext(); + const { token, id: instanceId, admin } = useInstanceContext(); + + const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}` + + const { data, error, isValidating } = useSWR<HttpResponseOk<MerchantBackend.Tips.TipDetails>, HttpError>([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, { + refreshInterval:0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }) + + if (isValidating) return { loading: true, data: data?.data } if (data) return data if (error) return error - return {loading: true} + return { loading: true } } +export function reserveDetailFetcher<T>(url: string, token: string, backend: string): Promise<HttpResponseOk<T>> { + return request<T>(`${backend}${url}`, { token, params: { + tips: 'yes' + } }) +} + +export function tipsDetailFetcher<T>(url: string, token: string, backend: string): Promise<HttpResponseOk<T>> { + return request<T>(`${backend}${url}`, { token, params: { + pickups: 'yes' + } }) +} diff --git a/packages/frontend/src/paths/instance/details/index.tsx b/packages/frontend/src/paths/instance/details/index.tsx @@ -54,10 +54,8 @@ export default function Detail({ onUpdate, onLoadError, onUnauthorized, onDelete onConfirm={async (): Promise<void> => { try { await deleteInstance() - // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) onDelete() } catch (error) { - // pushNotification({ message: 'delete_error', type: 'ERROR' }) } setDeleting(false) }} diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx b/packages/frontend/src/paths/instance/orders/list/Table.tsx @@ -31,7 +31,7 @@ import { ConfirmModal } from "../../../../components/modal"; import { MerchantBackend, WithId } from "../../../../declaration"; import { useOrderDetails } from "../../../../hooks/order"; import { Translate, useTranslator } from "../../../../i18n"; -import { RefoundSchema } from "../../../../schemas"; +import { AuthorizeTipSchema, RefundSchema as RefundSchema } from "../../../../schemas"; import { mergeRefunds, subtractPrices, sumPrices } from "../../../../utils/amount"; import { AMOUNT_ZERO_REGEX } from "../../../../utils/constants"; @@ -170,7 +170,7 @@ export function RefundModal({ id, onCancel, onConfirm }: RefundModalProps): VNod const validateAndConfirm = () => { try { - RefoundSchema.validateSync(form, { abortEarly: false }) + RefundSchema.validateSync(form, { abortEarly: false }) if (!form.refund) return; onConfirm({ refund: form.refund, diff --git a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx @@ -0,0 +1,77 @@ +/* + 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, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormErrors, FormProvider } from "../../../../components/form/FormProvider"; +import { Input } from "../../../../components/form/Input"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { MerchantBackend } from "../../../../declaration"; +import { Translate, useTranslator } from "../../../../i18n"; + +type Entity = MerchantBackend.Tips.ReserveCreateRequest + +interface Props { + onCreate: (d: Entity) => void; + onBack?: () => void; +} + + +export function CreatePage({ onCreate, onBack }: Props): VNode { + const [reserve, setReserve] = useState<Partial<Entity>>({ + initial_balance: 'COL:2', + exchange_url: 'http://exchange.taler:8081/', + wire_method: 'x-taler-bank', + }) + const i18n = useTranslator() + + const errors : FormErrors<Entity> = { + + } + + const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + + const submitForm = () => { + if (hasErrors) return + onCreate(reserve as Entity) + } + + return <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-two-thirds"> + <FormProvider<Entity> object={reserve} valueHandler={setReserve}> + <InputCurrency<Entity> name="initial_balance" label={i18n`Initial balance`} /> + <Input<Entity> name="exchange_url" label={i18n`Exchange`} /> + <Input<Entity> name="wire_method" label={i18n`Wire method`} /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && <button class="button" onClick={onBack} ><Translate>Cancel</Translate></button>} + <button class="button is-success" onClick={submitForm} ><Translate>Confirm</Translate></button> + </div> + </div> + <div class="column" /> + </div> + </section> + </div> +} +\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/frontend/src/paths/instance/reserves/create/CreatedSuccessfully.tsx @@ -0,0 +1,56 @@ +/* + 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/> + */ +import { h, VNode } from "preact"; +import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully"; +import { MerchantBackend } from "../../../../declaration"; + +type Entity = MerchantBackend.Tips.ReserveCreateConfirmation; + +interface Props { + entity: Entity; + onConfirm: () => void; + onCreateAnother?: () => void; +} + +export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: Props): VNode { + + return <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Account address</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={entity.payto_uri} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Message</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={entity.reserve_pub} /> + </p> + </div> + </div> + </div> + </Template>; +} diff --git a/packages/frontend/src/paths/instance/reserves/create/index.tsx b/packages/frontend/src/paths/instance/reserves/create/index.tsx @@ -0,0 +1,61 @@ +/* + 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, VNode } from 'preact'; +import { useState } from 'preact/hooks'; +import { NotificationCard } from '../../../../components/menu'; +import { MerchantBackend } from '../../../../declaration'; +import { useReservesAPI } from '../../../../hooks/tips'; +import { useTranslator } from '../../../../i18n'; +import { Notification } from '../../../../utils/types'; +import { CreatedSuccessfully } from './CreatedSuccessfully'; +import { CreatePage } from './CreatePage'; + +interface Props { + onBack: () => void; + onConfirm: () => void; +} +export default function CreateReserve({ onBack, onConfirm }: Props): VNode { + const { createReserve } = useReservesAPI() + const [notif, setNotif] = useState<Notification | undefined>(undefined) + const i18n = useTranslator() + + const [createdOk, setCreatedOk] = useState<MerchantBackend.Tips.ReserveCreateConfirmation | undefined>(undefined); + + if (createdOk) { + return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} /> + } + + return <Fragment> + <NotificationCard notification={notif} /> + <CreatePage + onBack={onBack} + onCreate={(request: MerchantBackend.Tips.ReserveCreateRequest) => { + createReserve(request).then((r) => setCreatedOk(r.data)).catch((error) => { + setNotif({ + message: i18n`could not create reserve`, + type: "ERROR", + description: error.message + }) + }) + }} /> + </Fragment> +} diff --git a/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx b/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx @@ -0,0 +1,117 @@ +/* + 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 { format, isAfter } from "date-fns"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider } from "../../../../components/form/FormProvider"; +import { Input } from "../../../../components/form/Input"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { InputDate } from "../../../../components/form/InputDate"; +import { InputDuration } from "../../../../components/form/InputDuration"; +import { InputGroup } from "../../../../components/form/InputGroup"; +import { InputLocation } from "../../../../components/form/InputLocation"; +import { NotificationCard } from "../../../../components/menu"; +import { ProductList } from "../../../../components/product/ProductList"; +import { MerchantBackend } from "../../../../declaration"; +import { useTipDetails } from "../../../../hooks/tips"; +import { Translate, useTranslator } from "../../../../i18n"; +import { mergeRefunds } from "../../../../utils/amount"; + +type Entity = MerchantBackend.Tips.ReserveDetail; +type CT = MerchantBackend.ContractTerms + +interface Props { + onBack: () => void; + selected: Entity; +} + +export function DetailPage({ selected }: Props): VNode { + const i18n = useTranslator() + return <Fragment> + <FormProvider object={selected} valueHandler={null} > + <InputDate<Entity> name="creation_time" label={i18n`Created at`} readonly /> + <InputDate<Entity> name="expiration_time" label={i18n`Valid until`} readonly /> + <InputCurrency<Entity> name="merchant_initial_amount" label={i18n`Created balance`} readonly /> + <InputCurrency<Entity> name="exchange_initial_amount" label={i18n`Exchange balance`} readonly /> + <InputCurrency<Entity> name="pickup_amount" label={i18n`Picked up`} readonly /> + <InputCurrency<Entity> name="committed_amount" label={i18n`Committed`} readonly /> + </FormProvider> + {selected.tips && selected.tips.length > 0 ? <Table tips={selected.tips} /> : <div> + no tips for this reserve + </div>} + </Fragment> +} + +async function copyToClipboard(text: string) { + return navigator.clipboard.writeText(text) +} + +interface TableProps { + tips: MerchantBackend.Tips.TipStatusEntry[]; +} + +function Table({ tips }: TableProps): VNode { + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th><Translate>Authorized</Translate></th> + <th><Translate>Picked up</Translate></th> + <th><Translate>Reason</Translate></th> + <th><Translate>Expiration</Translate></th> + </tr> + </thead> + <tbody> + {tips.map((t, i) => { + return <TipRow id={t.tip_id} key={i} entry={t} /> + })} + </tbody> + </table></div>) +} + +function TipRow({ id, entry }: { id: string, entry: MerchantBackend.Tips.TipStatusEntry }) { + const result = useTipDetails(id) + if (result.loading) { + return <tr> + <td>...</td> + <td>...</td> + <td>...</td> + <td>...</td> + </tr> + } + if (!result.ok) { + return <tr> + <td>...</td> + <td>{entry.total_amount}</td> + <td>{entry.total_amount}</td> + <td>expired</td> + </tr> + } + const info = result.data + return <tr> + <td>{info.total_authorized}</td> + <td>{info.total_picked_up}</td> + <td>{info.reason}</td> + <td>{info.expiration.t_ms === "never" ? "never" : format(info.expiration.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> + </tr> +} +\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/reserves/details/index.tsx b/packages/frontend/src/paths/instance/reserves/details/index.tsx @@ -0,0 +1,47 @@ +/* + 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, VNode } from 'preact'; +import { Loading } from '../../../../components/exception/loading'; +import { HttpError } from '../../../../hooks/backend'; +import { useReserveDetails } from '../../../../hooks/tips'; +import { DetailPage } from './DetailPage'; + +interface Props { + rid: string; + + onUnauthorized: () => VNode; + onLoadError: (error: HttpError) => VNode; + onNotFound: () => VNode; + onDelete: () => void; + onBack: () => void; +} +export default function DetailReserve({rid, onUnauthorized, onLoadError, onNotFound, onBack, onDelete}: Props):VNode { + const result = useReserveDetails(rid) + + if (result.clientError && result.isUnauthorized) return onUnauthorized() + if (result.clientError && result.isNotfound) return onNotFound() + if (result.loading) return <Loading /> + if (!result.ok) return onLoadError(result) + return <Fragment> + <DetailPage selected={result.data} onBack={onBack} /> + </Fragment> +} diff --git a/packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx b/packages/frontend/src/paths/instance/reserves/list/AutorizeTipModal.tsx @@ -0,0 +1,81 @@ +/* + 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, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormErrors, FormProvider } from "../../../../components/form/FormProvider"; +import { Input } from "../../../../components/form/Input"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { ConfirmModal, ContinueModal } from "../../../../components/modal"; +import { MerchantBackend } from "../../../../declaration"; +import { useTranslator } from "../../../../i18n"; +import { AuthorizeTipSchema } from "../../../../schemas"; +import { CreatedSuccessfully } from "./CreatedSuccessfully"; + +interface AutorizaTipModalProps { + onCancel: () => void; + onConfirm: (value: MerchantBackend.Tips.TipCreateRequest) => void; + tipAuthorized?: { + response: MerchantBackend.Tips.TipCreateConfirmation; + request: MerchantBackend.Tips.TipCreateRequest; + }; +} + +export function AuthorizeTipModal({ onCancel, onConfirm, tipAuthorized }: AutorizaTipModalProps): VNode { + // const result = useOrderDetails(id) + type State = MerchantBackend.Tips.TipCreateRequest + const [form, setValue] = useState<Partial<State>>({}) + const i18n = useTranslator(); + const [errors, setErrors] = useState<FormErrors<State>>({}) + + const validateAndConfirm = () => { + try { + AuthorizeTipSchema.validateSync(form, { abortEarly: false }) + onConfirm(form as State) + } catch (err) { + const errors = err.inner as any[] + const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) + setErrors(pathMessages) + } + } + + if (tipAuthorized) { + return <ContinueModal description="tip" active onConfirm={onCancel}> + <CreatedSuccessfully + entity={tipAuthorized.response} + request={tipAuthorized.request} + onConfirm={onCancel} + /> + </ContinueModal> + } + + return <ConfirmModal description="tip" active onCancel={onCancel} onConfirm={validateAndConfirm}> + + <FormProvider<State> errors={errors} object={form} valueHandler={setValue} > + <InputCurrency<State> name="amount" label={i18n`Amount`} /> + <Input<State> name="justification" label={i18n`Justification`} inputType="multiline" /> + <Input<State> name="next_url" label={i18n`URL after tip`} /> + </FormProvider> + + </ConfirmModal> +} + + diff --git a/packages/frontend/src/paths/instance/reserves/list/CreatedSuccessfully.tsx b/packages/frontend/src/paths/instance/reserves/list/CreatedSuccessfully.tsx @@ -0,0 +1,81 @@ +/* + 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/> + */ +import { format } from "date-fns"; +import { Fragment, h, VNode } from "preact"; +import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully"; +import { MerchantBackend } from "../../../../declaration"; + +type Entity = MerchantBackend.Tips.TipCreateConfirmation; + +interface Props { + entity: Entity; + request: MerchantBackend.Tips.TipCreateRequest, + onConfirm: () => void; + onCreateAnother?: () => void; +} + +export function CreatedSuccessfully({ request, entity, onConfirm, onCreateAnother }: Props): VNode { + return <Fragment> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Amount</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={request.amount} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Justification</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={request.justification} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">URL</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input readonly class="input" value={entity.tip_status_url} /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Valid until</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={!entity.tip_expiration || entity.tip_expiration.t_ms === "never" ? "never" : format(entity.tip_expiration.t_ms, 'yyyy/MM/dd HH:mm:ss')} /> + </p> + </div> + </div> + </div> + </Fragment>; +} diff --git a/packages/frontend/src/paths/instance/reserves/list/Table.tsx b/packages/frontend/src/paths/instance/reserves/list/Table.tsx @@ -0,0 +1,219 @@ +/* + 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 { format } from "date-fns" +import { Fragment, h, VNode } from "preact" +import { StateUpdater, useEffect, useState } from "preact/hooks" +import { MerchantBackend, WithId } from "../../../../declaration" +import { Translate } from "../../../../i18n" +import { Actions, buildActions } from "../../../../utils/table" + +type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId + +interface Props { + instances: Entity[]; + onNewTip: (id: Entity) => void; + onSelect: (id: Entity) => void; + onDelete: (id: Entity) => void; + onCreate: () => void; + selected?: boolean; +} + +export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete, selected }: Props): VNode { + const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]); + const [rowSelection, rowSelectionHandler] = useState<string[]>([]) + + useEffect(() => { + if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'DELETE') { + onDelete(actionQueue[0].element) + actionQueueHandler(actionQueue.slice(1)) + } + }, [actionQueue, selected, onDelete]) + + useEffect(() => { + if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'UPDATE') { + onNewTip(actionQueue[0].element) + actionQueueHandler(actionQueue.slice(1)) + } + }, [actionQueue, selected, onNewTip]) + + const [withoutFunds, withFunds] = instances.reduce((prev, current) => { + const amount = current.exchange_initial_amount + if (amount.endsWith(':0')) { + prev[0] = prev[0].concat(current) + } else { + prev[1] = prev[1].concat(current) + } + return prev + }, new Array<Array<Entity>>([],[])) + + + return <Fragment> + {withoutFunds.length > 0 && <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves not yet funded</Translate></p> + + <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}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </div> + + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + <TableWithoutFund instances={withoutFunds} onNewTip={onNewTip} onSelect={onSelect} onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> + </div> + </div> + </div> + </div> } + + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves ready</Translate></p> + + <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}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </div> + + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {withFunds.length > 0 ? + <Table instances={withFunds} onNewTip={onNewTip} onSelect={onSelect} onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> : + <EmptyTable /> + } + </div> + </div> + </div> + </div> + </Fragment> +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onNewTip: (id: Entity) => void; + onDelete: (id: Entity) => void; + onSelect: (id: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; +} + +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, onNewTip, onSelect, onDelete }: TableProps): VNode { + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th><Translate>Created at</Translate></th> + <th><Translate>Expires at</Translate></th> + <th><Translate>Initial</Translate></th> + <th><Translate>Picked up</Translate></th> + <th><Translate>Committed</Translate></th> + <th /> + </tr> + </thead> + <tbody> + {instances.map(i => { + return <tr key={i.id}> + <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> + <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> + <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.exchange_initial_amount}</td> + <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.pickup_amount}</td> + <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.committed_amount}</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 + </button> + <button class="button is-small is-info jb-modal" type="button" onClick={(): void => onNewTip(i)}> + New Tip + </button> + </div> + </td> + </tr> + })} + + </tbody> + </table></div>) +} + +function EmptyTable(): VNode { + return <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> + </p> + <p><Translate>There is no ready reserves yet, add more pressing the + sign or fund them</Translate></p> + </div> +} + +function TableWithoutFund({ rowSelection, rowSelectionHandler, instances, onNewTip, onSelect, onDelete }: TableProps): VNode { + return ( + <div class="table-container"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th><Translate>Created at</Translate></th> + <th><Translate>Expires at</Translate></th> + <th><Translate>Expected Balance</Translate></th> + <th /> + </tr> + </thead> + <tbody> + {instances.map(i => { + return <tr key={i.id}> + <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.creation_time.t_ms === "never" ? "never" : format(i.creation_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> + <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.expiration_time.t_ms === "never" ? "never" : format(i.expiration_time.t_ms, 'yyyy/MM/dd HH:mm:ss')}</td> + <td onClick={(): void => onSelect(i)} style={{cursor: 'pointer'}} >{i.merchant_initial_amount}</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 + </button> + </div> + </td> + </tr> + })} + + </tbody> + </table></div>) +} diff --git a/packages/frontend/src/paths/instance/reserves/list/index.tsx b/packages/frontend/src/paths/instance/reserves/list/index.tsx @@ -0,0 +1,97 @@ +/* + 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, VNode } from 'preact'; +import { useState } from 'preact/hooks'; +import { Loading } from '../../../../components/exception/loading'; +import { NotificationCard } from '../../../../components/menu'; +import { MerchantBackend } from '../../../../declaration'; +import { HttpError } from '../../../../hooks/backend'; +import { useInstanceTips, useReservesAPI } from "../../../../hooks/tips"; +import { useTranslator } from '../../../../i18n'; +import { Notification } from '../../../../utils/types'; +import { CardTable } from './Table'; +import { AuthorizeTipModal } from './AutorizeTipModal'; + +interface Props { + onUnauthorized: () => VNode; + onLoadError: (e: HttpError) => VNode; + onSelect: (id: string) => void; + onNotFound: () => VNode; + onCreate: () => void; +} + +interface TipConfirmation { + response: MerchantBackend.Tips.TipCreateConfirmation; + request: MerchantBackend.Tips.TipCreateRequest; +} + +export default function ListTips({ onUnauthorized, onLoadError, onNotFound, onSelect, onCreate }: Props): VNode { + const result = useInstanceTips() + const { deleteReserve, authorizeTipReserve } = useReservesAPI() + const [notif, setNotif] = useState<Notification | undefined>(undefined) + const i18n = useTranslator() + const [reserveForTip, setReserveForTip] = useState<string | undefined>(undefined); + const [tipAuthorized, setTipAuthorized] = useState<TipConfirmation | undefined>(undefined); + + if (result.clientError && result.isUnauthorized) return onUnauthorized() + if (result.clientError && result.isNotfound) return onNotFound() + if (result.loading) return <Loading /> + if (!result.ok) return onLoadError(result) + + + return <section class="section is-main-section"> + <NotificationCard notification={notif} /> + + {reserveForTip && ( + <AuthorizeTipModal + onCancel={() => { + setReserveForTip(undefined) + setTipAuthorized(undefined) + }} + tipAuthorized={tipAuthorized} + onConfirm={async (request) => { + try { + const response = await authorizeTipReserve(reserveForTip, request) + setTipAuthorized({ + request, response: response.data + }) + } catch (error) { + setNotif({ + message: i18n`could not create the tip`, + type: "ERROR", + description: error.message + }) + setReserveForTip(undefined) + } + } + } + /> + )} + + <CardTable instances={result.data.reserves.filter(r => r.active).map(o => ({ ...o, id: o.reserve_pub }))} + onCreate={onCreate} + onDelete={(reserve) => deleteReserve(reserve.reserve_pub)} + onSelect={(reserve) => onSelect(reserve.id)} + onNewTip={(reserve) => setReserveForTip(reserve.id)} + /> + </section> +} diff --git a/packages/frontend/src/paths/instance/tips/create/index.tsx b/packages/frontend/src/paths/instance/tips/create/index.tsx @@ -1,26 +0,0 @@ -/* - 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, VNode } from 'preact'; - -export default function CreateTips():VNode { - return <div>tip create page</div> -} -\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/tips/list/Table.tsx b/packages/frontend/src/paths/instance/tips/list/Table.tsx @@ -1,150 +0,0 @@ -/* - 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, VNode } from "preact" -import { StateUpdater, useEffect, useState } from "preact/hooks" -import { MerchantBackend, WithId } from "../../../../declaration" -import { Translate } from "../../../../i18n" -import { Actions, buildActions } from "../../../../utils/table" - -type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId - -interface Props { - instances: Entity[]; - onUpdate: (id: string) => void; - onDelete: (id: Entity) => void; - onCreate: () => void; - selected?: boolean; -} - -export function CardTable({ instances, onCreate, onUpdate, onDelete, selected }: Props): VNode { - const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]); - const [rowSelection, rowSelectionHandler] = useState<string[]>([]) - - useEffect(() => { - if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'DELETE') { - onDelete(actionQueue[0].element) - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, selected, onDelete]) - - useEffect(() => { - if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'UPDATE') { - onUpdate(actionQueue[0].element.id) - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, selected, onUpdate]) - - - return <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Tips</Translate></p> - - <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}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> - </div> - - </header> - <div class="card-content"> - <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} /> : - <EmptyTable /> - } - </div> - </div> - </div> - </div> -} -interface TableProps { - rowSelection: string[]; - instances: Entity[]; - onUpdate: (id: string) => void; - onDelete: (id: Entity) => void; - rowSelectionHandler: StateUpdater<string[]>; -} - -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 { - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th class="is-checkbox-cell"> - <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><Translate>Committed amount</Translate></th> - <th><Translate>Exchange initial amount</Translate></th> - <th><Translate>Merchant initial amount</Translate></th> - <th /> - </tr> - </thead> - <tbody> - {instances.map(i => { - return <tr key={i.id}> - <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'}} >{i.committed_amount}</td> - <td onClick={(): void => onUpdate(i.id)} style={{cursor: 'pointer'}} >{i.exchange_initial_amount}</td> - <td onClick={(): void => onUpdate(i.id)} style={{cursor: 'pointer'}} >{i.merchant_initial_amount}</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 - </button> - </div> - </td> - </tr> - })} - - </tbody> - </table></div>) -} - -function EmptyTable(): VNode { - return <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> - </p> - <p><Translate>There is no tips yet, add more pressing the + sign</Translate></p> - </div> -} diff --git a/packages/frontend/src/paths/instance/tips/list/index.tsx b/packages/frontend/src/paths/instance/tips/list/index.tsx @@ -1,60 +0,0 @@ -/* - 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, VNode } from 'preact'; -import { Loading } from '../../../../components/exception/loading'; -import { useConfigContext } from '../../../../context/config'; -import { MerchantBackend } from '../../../../declaration'; -import { HttpError } from '../../../../hooks/backend'; -import { useInstanceTips, useTipsMutateAPI } from "../../../../hooks/tips"; -import { CardTable } from './Table'; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (e: HttpError) => VNode; - onNotFound: () => VNode; -} -export default function ListTips({ onUnauthorized, onLoadError, onNotFound }: Props): VNode { - const result = useInstanceTips() - const { createReserve, deleteReserve } = useTipsMutateAPI() - const { currency } = useConfigContext() - - if (result.clientError && result.isUnauthorized) return onUnauthorized() - if (result.clientError && result.isNotfound) return onNotFound() - if (result.loading) return <Loading /> - if (!result.ok) return onLoadError(result) - - return <section class="section is-main-section"> - <CardTable instances={result.data.reserves.filter(r => r.active).map(o => ({ ...o, id: o.reserve_pub }))} - onCreate={() => createReserve({ - // explode with basic - wire_method: 'x-taler-ban', - initial_balance: `${currency}:${Math.floor(Math.random() * 20 + 1)}`, - //explode with 1 - // hangs with /asd/asd/ - // http://localhost:8081/ - exchange_url: 'https://exchange-demo.rigel.ar/', - })} - onDelete={(reserve: MerchantBackend.Tips.ReserveStatusEntry) => deleteReserve(reserve.reserve_pub)} - onUpdate={() => null} - /> - </section> -} -\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/tips/update/index.tsx b/packages/frontend/src/paths/instance/tips/update/index.tsx @@ -1,26 +0,0 @@ -/* - 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, VNode } from 'preact'; - -export default function UpdateTips():VNode { - return <div>tip update page</div> -} -\ No newline at end of file diff --git a/packages/frontend/src/schemas/index.ts b/packages/frontend/src/schemas/index.ts @@ -113,7 +113,7 @@ export const InstanceSchema = yup.object().shape({ export const InstanceUpdateSchema = InstanceSchema.clone().omit(['id']); export const InstanceCreateSchema = InstanceSchema.clone(); -export const RefoundSchema = yup.object().shape({ +export const RefundSchema = yup.object().shape({ mainReason: yup.string().required(), description: yup.string().required(), refund: yup.string() @@ -122,6 +122,15 @@ export const RefoundSchema = yup.object().shape({ .test('amount_positive', 'the amount is not valid', currencyGreaterThan0), }) +export const AuthorizeTipSchema = yup.object().shape({ + justification: yup.string().required(), + amount: yup.string() + .required() + .test('amount', 'the amount is not valid', currencyWithAmountIsValid) + .test('amount_positive', 'the amount is not valid', currencyGreaterThan0), + next_url: yup.string().required(), +}) + const stringIsValidJSON = (value?: string) => { const p = value?.trim() if (!p) return true;