diff options
Diffstat (limited to 'packages/bank-ui/src/components/Cashouts')
-rw-r--r-- | packages/bank-ui/src/components/Cashouts/index.ts | 86 | ||||
-rw-r--r-- | packages/bank-ui/src/components/Cashouts/state.ts | 51 | ||||
-rw-r--r-- | packages/bank-ui/src/components/Cashouts/stories.tsx | 29 | ||||
-rw-r--r-- | packages/bank-ui/src/components/Cashouts/test.ts | 68 | ||||
-rw-r--r-- | packages/bank-ui/src/components/Cashouts/views.tsx | 217 |
5 files changed, 451 insertions, 0 deletions
diff --git a/packages/bank-ui/src/components/Cashouts/index.ts b/packages/bank-ui/src/components/Cashouts/index.ts new file mode 100644 index 000000000..2c6bf681c --- /dev/null +++ b/packages/bank-ui/src/components/Cashouts/index.ts @@ -0,0 +1,86 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { Loading, utils } from "@gnu-taler/web-util/browser"; +import { + AbsoluteTime, + AmountJson, + TalerCoreBankErrorsByMethod, + TalerCorebankApi, + TalerError, +} from "@gnu-taler/taler-util"; +import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js"; +import { useComponentState } from "./state.js"; +import { FailedView, ReadyView } from "./views.js"; +import { RouteDefinition } from "../../route.js"; + +export interface Props { + account: string; + routeCashoutDetails: RouteDefinition<{ cid: string }>; +} + +export type State = + | State.Loading + | State.Failed + | State.LoadingUriError + | State.Ready; + +export namespace State { + export interface Loading { + status: "loading"; + error: undefined; + } + + export interface LoadingUriError { + status: "loading-error"; + error: TalerError; + } + + export interface Failed { + status: "failed"; + error: TalerCoreBankErrorsByMethod<"getAccountCashouts">; + } + + export interface BaseInfo { + error: undefined; + } + export interface Ready extends BaseInfo { + status: "ready"; + error: undefined; + cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: number })[]; + routeCashoutDetails: RouteDefinition<{ cid: string }>; + } +} + +export interface Transaction { + negative: boolean; + counterpart: string; + when: AbsoluteTime; + amount: AmountJson | undefined; + subject: string; +} + +const viewMapping: utils.StateViewMap<State> = { + loading: Loading, + "loading-error": ErrorLoadingWithDebug, + failed: FailedView, + ready: ReadyView, +}; + +export const Cashouts = utils.compose( + (p: Props) => useComponentState(p), + viewMapping, +); diff --git a/packages/bank-ui/src/components/Cashouts/state.ts b/packages/bank-ui/src/components/Cashouts/state.ts new file mode 100644 index 000000000..8616faa1b --- /dev/null +++ b/packages/bank-ui/src/components/Cashouts/state.ts @@ -0,0 +1,51 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { TalerError } from "@gnu-taler/taler-util"; +import { useCashouts } from "../../hooks/regional.js"; +import { Props, State } from "./index.js"; + +export function useComponentState({ + account, + routeCashoutDetails, +}: Props): State { + const result = useCashouts(account); + if (!result) { + return { + status: "loading", + error: undefined, + }; + } + if (result instanceof TalerError) { + return { + status: "loading-error", + error: result, + }; + } + if (result.type === "fail") { + return { + status: "failed", + error: result, + }; + } + + return { + status: "ready", + error: undefined, + cashouts: result.body.cashouts, + routeCashoutDetails, + }; +} diff --git a/packages/bank-ui/src/components/Cashouts/stories.tsx b/packages/bank-ui/src/components/Cashouts/stories.tsx new file mode 100644 index 000000000..37ab64108 --- /dev/null +++ b/packages/bank-ui/src/components/Cashouts/stories.tsx @@ -0,0 +1,29 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 * as tests from "@gnu-taler/web-util/testing"; +import { ReadyView } from "./views.js"; + +export default { + title: "transaction list", +}; + +export const Ready = tests.createExample(ReadyView, {}); diff --git a/packages/bank-ui/src/components/Cashouts/test.ts b/packages/bank-ui/src/components/Cashouts/test.ts new file mode 100644 index 000000000..4bd6b5eac --- /dev/null +++ b/packages/bank-ui/src/components/Cashouts/test.ts @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 * as tests from "@gnu-taler/web-util/testing"; +import { SwrMockEnvironment } from "@gnu-taler/web-util/testing"; +import { expect } from "chai"; +import { Props } from "./index.js"; +import { useComponentState } from "./state.js"; +import { buildNullRoutDefinition } from "../../route.js"; + +describe("Cashout states", () => { + it.skip("should query backend and render transactions", async () => { + const env = new SwrMockEnvironment(); + + const props: Props = { + account: "123", + routeCashoutDetails: buildNullRoutDefinition(), + }; + + // env.addRequestExpectation(CASHOUT_API_EXAMPLE.LIST_FIRST_PAGE, { + // response: { + // cashouts: [], + // }, + // }); + + // env.addRequestExpectation(CASHOUT_API_EXAMPLE.MULTI_GET_EMPTY_FIRST_PAGE, { + // response: [], + // }); + + const hookBehavior = await tests.hookBehaveLikeThis( + useComponentState, + props, + [ + ({ status, error }) => { + expect(status).equals("loading"); + expect(error).undefined; + }, + ({ status, error }) => { + expect(status).equals("ready"); + expect(error).undefined; + }, + ], + env.buildTestingContext(), + ); + + expect(hookBehavior).deep.eq({ result: "ok" }); + + expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + }); +}); diff --git a/packages/bank-ui/src/components/Cashouts/views.tsx b/packages/bank-ui/src/components/Cashouts/views.tsx new file mode 100644 index 000000000..7f16d5840 --- /dev/null +++ b/packages/bank-ui/src/components/Cashouts/views.tsx @@ -0,0 +1,217 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 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 { + AbsoluteTime, + Amounts, + Duration, + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + Attention, + Loading, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; +import { Fragment, VNode, h } from "preact"; +import { useConversionInfo } from "../../hooks/regional.js"; +import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; +import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js"; +import { State } from "./index.js"; +import { Time } from "../Time.js"; + +export function FailedView({ error }: State.Failed) { + const { i18n } = useTranslationContext(); + switch (error.case) { + case HttpStatusCode.NotImplemented: { + return ( + <Attention + type="danger" + title={i18n.str`Cashout are disabled`} + > + <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate> + </Attention> + ); + } + default: + assertUnreachable(error.case); + } +} + +export function ReadyView({ + cashouts, + routeCashoutDetails, +}: State.Ready): VNode { + const { i18n, dateLocale } = useTranslationContext(); + const resp = useConversionInfo(); + if (!resp) { + return <Loading />; + } + if (resp instanceof TalerError) { + return <ErrorLoadingWithDebug error={resp} />; + } + if (resp.type === "fail") { + switch (resp.case) { + case HttpStatusCode.NotImplemented: { + return ( + <Attention + type="danger" + title={i18n.str`Cashout are disabled`} + > + <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate> + </Attention> + ); + } + default: + assertUnreachable(resp.case); + } + } + + if (!cashouts.length) return <div />; + const txByDate = cashouts.reduce( + (prev, cur) => { + const d = + cur.creation_time.t_s === "never" + ? "" + : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy", { + locale: dateLocale, + }); + if (!prev[d]) { + prev[d] = []; + } + prev[d].push(cur); + return prev; + }, + {} as Record<string, typeof cashouts>, + ); + return ( + <div class="px-4 mt-4"> + <div class="sm:flex sm:items-center"> + <div class="sm:flex-auto"> + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Latest cashouts</i18n.Translate> + </h1> + </div> + </div> + <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white"> + <table class="min-w-full divide-y divide-gray-300"> + <thead> + <tr> + <th + scope="col" + class=" pl-2 py-3.5 text-left text-sm font-semibold text-gray-900" + >{i18n.str`Created`}</th> + <th + scope="col" + class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900" + >{i18n.str`Total debit`}</th> + <th + scope="col" + class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900" + >{i18n.str`Total credit`}</th> + <th + scope="col" + class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900" + >{i18n.str`Subject`}</th> + </tr> + </thead> + <tbody> + {Object.entries(txByDate).map(([date, txs], idx) => { + return ( + <Fragment key={idx}> + <tr class="border-t border-gray-200"> + <th + colSpan={6} + scope="colgroup" + class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3" + > + {date} + </th> + </tr> + {txs.map((item) => { + return ( + <a + name="cashout details" + key={idx} + class="table-row border-b border-gray-200 hover:bg-gray-200 last:border-none" + // class="table-row" + href={routeCashoutDetails.url({ + cid: String(item.id), + })} + > + <td class="relative py-2 pl-2 pr-2 text-sm "> + <div class="font-medium text-gray-900"> + <Time format="HH:mm:ss" + timestamp={AbsoluteTime.fromProtocolTimestamp(item.creation_time)} + // relative={Duration.fromSpec({ days: 1 })} + /> + </div> + { + //FIXME: implement responsive view + } + {/* <dl class="font-normal sm:hidden"> + <dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt> + <dd class="mt-1 truncate text-gray-700"> + {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? ( + <span data-negative={item.negative ? "true" : "false"} class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"> + <RenderAmount value={item.amount} /> + </span> + ) : ( + <span style={{ color: "grey" }}><{i18n.str`invalid value`}></span> + )}</dd> + + <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt> + <dd class="mt-1 truncate text-gray-500 sm:hidden"> + {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart} + </dd> + <dd class="mt-1 text-gray-500 sm:hidden" > + <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100"> + {item.subject} + </pre> + </dd> + </dl> */} + </td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600 cursor-pointer"> + <RenderAmount + value={Amounts.parseOrThrow(item.amount_debit)} + spec={resp.body.regional_currency_specification} + /> + </td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600 cursor-pointer"> + <RenderAmount + value={Amounts.parseOrThrow(item.amount_credit)} + spec={resp.body.fiat_currency_specification} + /> + </td> + + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md"> + + {item.subject} + </td> + </a> + ); + })} + </Fragment> + ); + })} + </tbody> + </table> + </div> + </div> + ); +} |