diff options
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages/Cases.tsx')
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 343 |
1 files changed, 343 insertions, 0 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx new file mode 100644 index 000000000..2e92c111e --- /dev/null +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -0,0 +1,343 @@ +/* + 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 { + HttpStatusCode, + TalerError, + TalerExchangeApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + Attention, + ErrorLoading, + InputChoiceHorizontal, + Loading, + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { useCases } from "../hooks/useCases.js"; + +import { privatePages } from "../Routing.js"; +import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js"; +import { undefinedIfEmpty } from "./CreateAccount.js"; +import { Officer } from "./Officer.js"; + +type FormType = { + state: TalerExchangeApi.AmlState; +}; + +export function CasesUI({ + records, + filter, + onChangeFilter, + onFirstPage, + onNext, +}: { + onFirstPage?: () => void; + onNext?: () => void; + filter: TalerExchangeApi.AmlState; + onChangeFilter: (f: TalerExchangeApi.AmlState) => void; + records: TalerExchangeApi.AmlRecord[]; +}): VNode { + const { i18n } = useTranslationContext(); + + const [form, status] = useFormState<FormType>( + { + state: filter, + }, + (state) => { + const errors = undefinedIfEmpty<FormErrors<FormType>>({ + state: state.state === undefined ? i18n.str`required` : undefined, + }); + if (errors === undefined) { + const result: FormType = { + state: state.state!, + }; + return { + status: "ok", + result, + errors, + }; + } + const result: RecursivePartial<FormType> = { + state: state.state, + }; + return { + status: "fail", + result, + errors, + }; + }, + ); + useEffect(() => { + if (status.status === "ok" && filter !== status.result.state) { + onChangeFilter(status.result.state); + } + }, [form?.state?.value]); + + return ( + <div> + <div class="sm:flex sm:items-center"> + <div class="px-2 sm:flex-auto"> + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Cases</i18n.Translate> + </h1> + <p class="mt-2 text-sm text-gray-700 w-80"> + <i18n.Translate> + A list of all the account with the status + </i18n.Translate> + </p> + </div> + <div class="px-2"> + <InputChoiceHorizontal<FormType, "state"> + name="state" + label={i18n.str`Filter`} + handler={form.state} + choices={[ + { + label: i18n.str`Pending`, + value: TalerExchangeApi.AmlState.pending, + }, + { + label: i18n.str`Frozen`, + value: TalerExchangeApi.AmlState.frozen, + }, + { + label: i18n.str`Normal`, + value: TalerExchangeApi.AmlState.normal, + }, + ]} + /> + </div> + </div> + <div class="mt-8 flow-root"> + <div class="overflow-x-auto"> + {!records.length ? ( + <div>empty result </div> + ) : ( + <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"> + <table class="min-w-full divide-y divide-gray-300"> + <thead> + <tr> + <th + scope="col" + class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-80" + > + <i18n.Translate>Account Id</i18n.Translate> + </th> + <th + scope="col" + class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40" + > + <i18n.Translate>Status</i18n.Translate> + </th> + <th + scope="col" + class="sm:hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-40" + > + <i18n.Translate>Threshold</i18n.Translate> + </th> + </tr> + </thead> + <tbody class="divide-y divide-gray-200 bg-white"> + {records.map((r) => { + return ( + <tr key={r.h_payto} class="hover:bg-gray-100 "> + <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> + <div class="text-gray-900"> + <a + href={privatePages.caseDetails.url({ + cid: r.h_payto, + })} + class="text-indigo-600 hover:text-indigo-900" + > + {r.h_payto.substring(0, 16)}... + </a> + </div> + </td> + <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500"> + {((state: TalerExchangeApi.AmlState): VNode => { + switch (state) { + case TalerExchangeApi.AmlState.normal: { + return ( + <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20"> + Normal + </span> + ); + } + case TalerExchangeApi.AmlState.pending: { + return ( + <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-700 ring-1 ring-inset ring-green-600/20"> + Pending + </span> + ); + } + case TalerExchangeApi.AmlState.frozen: { + return ( + <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-green-600/20"> + Frozen + </span> + ); + } + } + })(r.current_state)} + </td> + <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900"> + {r.threshold} + </td> + </tr> + ); + })} + </tbody> + </table> + <Pagination onFirstPage={onFirstPage} onNext={onNext} /> + </div> + )} + </div> + </div> + </div> + ); +} + +export function Cases() { + const [stateFilter, setStateFilter] = useState( + TalerExchangeApi.AmlState.pending, + ); + + const list = useCases(stateFilter); + const { i18n } = useTranslationContext(); + + if (!list) { + return <Loading />; + } + if (list instanceof TalerError) { + return <ErrorLoading error={list} />; + } + + if (list.type === "fail") { + switch (list.case) { + case HttpStatusCode.Forbidden: { + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + This account doesnt have access. Request account activation + sending your public key. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + } + case HttpStatusCode.Unauthorized: { + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + This account is not allowed to perform list the cases. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + } + case HttpStatusCode.NotFound: + case HttpStatusCode.Conflict: + return <Officer />; + default: + assertUnreachable(list); + } + } + + return ( + <CasesUI + records={list.body} + onFirstPage={list.isFirstPage ? undefined : list.loadFirst} + onNext={list.isLastPage ? undefined : list.loadNext} + filter={stateFilter} + onChangeFilter={(d) => { + setStateFilter(d) + }} + /> + ); +} + +export const PeopleIcon = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" + /> + </svg> +); + +export const HomeIcon = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" + /> + </svg> +); + +function Pagination({ + onFirstPage, + onNext, +}: { + onFirstPage?: () => void; + onNext?: () => void; +}) { + const { i18n } = useTranslationContext(); + return ( + <nav + class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" + aria-label="Pagination" + > + <div class="flex flex-1 justify-between sm:justify-end"> + <button + class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + disabled={!onFirstPage} + onClick={onFirstPage} + > + <i18n.Translate>First page</i18n.Translate> + </button> + <button + class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + disabled={!onNext} + onClick={onNext} + > + <i18n.Translate>Next</i18n.Translate> + </button> + </div> + </nav> + ); +} |