diff options
author | Sebastian <sebasjm@gmail.com> | 2024-08-27 18:20:54 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-08-27 18:21:28 -0300 |
commit | 94cb658024a17084da5de310bc5104cff6fd8337 (patch) | |
tree | 37c8459d8f7ba1262ac866de904a40d8cc6688e0 | |
parent | 3e834cd10187a488b19b8f809b55cc443cd3c675 (diff) |
freeze account
-rw-r--r-- | packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx | 9 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/Routing.tsx | 17 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/decisions.ts | 12 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 421 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 89 |
5 files changed, 452 insertions, 96 deletions
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx index 7dc36e7f6..44cd0ce82 100644 --- a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx +++ b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx @@ -33,7 +33,7 @@ import { getLabelForPreferences, usePreferences, } from "./hooks/preferences.js"; -import { HomeIcon } from "./pages/Cases.js"; +import { HomeIcon, PeopleIcon, ToInvestigateIcon } from "./pages/Cases.js"; /** * mapping route to view @@ -208,7 +208,7 @@ export function ExchangeAmlFrame({ <div class="-mt-32 flex grow "> {officer?.state !== "ready" ? undefined : <Navigation />} <div class="flex mx-auto my-4"> - <main class="rounded-lg bg-white px-5 py-6 shadow">{children}</main> + <main class="block rounded-lg bg-white px-5 py-6 shadow " style={{minWidth: 300}} >{children}</main> </div> </div> @@ -224,8 +224,9 @@ export function ExchangeAmlFrame({ function Navigation(): VNode { const { i18n } = useTranslationContext(); const pageList = [ - { route: privatePages.account, Icon: HomeIcon, label: i18n.str`Account` }, - { route: privatePages.cases, Icon: HomeIcon, label: i18n.str`Cases` }, + { route: privatePages.profile, Icon: PeopleIcon, label: i18n.str`Profile` }, + { route: privatePages.investigation, Icon: ToInvestigateIcon, label: i18n.str`Investigation` }, + { route: privatePages.active, Icon: HomeIcon, label: i18n.str`Active` }, ]; const { path } = useNavigationContext(); return ( diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx index 4000cac00..082dc8ab5 100644 --- a/packages/aml-backoffice-ui/src/Routing.tsx +++ b/packages/aml-backoffice-ui/src/Routing.tsx @@ -94,8 +94,9 @@ function PublicRounting(): VNode { } export const privatePages = { - account: urlPattern(/\/account/, () => "#/account"), - cases: urlPattern(/\/cases/, () => "#/cases"), + profile: urlPattern(/\/profile/, () => "#/profile"), + investigation: urlPattern(/\/investigation/, () => "#/investigation"), + active: urlPattern(/\/active/, () => "#/active"), caseUpdate: urlPattern<{ cid: string; type: string }>( /\/case\/(?<cid>[a-zA-Z0-9]+)\/new\/(?<type>[a-zA-Z0-9_.]+)/, ({ cid, type }) => `#/case/${cid}/new/${type}`, @@ -114,8 +115,9 @@ function PrivateRouting(): VNode { const { navigateTo } = useNavigationContext(); const location = useCurrentLocation(privatePages); useEffect(() => { - if (location === undefined) { - navigateTo(privatePages.account.url({})); + if (location.name === undefined) { + console.log("asd") + navigateTo(privatePages.profile.url({})); } }, [location]); @@ -123,7 +125,7 @@ function PrivateRouting(): VNode { case undefined: { return <Fragment />; } - case "account": { + case "profile": { return <Officer />; } case "caseDetails": { @@ -137,7 +139,10 @@ function PrivateRouting(): VNode { case "caseNew": { return <SelectForm account={location.values.cid} />; } - case "cases": { + case "investigation": { + return <Cases filtered/>; + } + case "active": { return <Cases />; } default: diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts index e652f233e..de8f1637e 100644 --- a/packages/aml-backoffice-ui/src/hooks/decisions.ts +++ b/packages/aml-backoffice-ui/src/hooks/decisions.ts @@ -37,7 +37,7 @@ export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1; * @param args * @returns */ -export function useCurrentDecisions() { +export function useCurrentDecisions(filtered?: boolean) { const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; const { @@ -46,13 +46,15 @@ export function useCurrentDecisions() { const [offset, setOffset] = useState<string>(); - async function fetcher([officer, offset]: [ + async function fetcher([officer, offset, investigation]: [ OfficerAccount, string | undefined, + boolean | undefined ]) { return await api.getAmlDecisions(officer, { - order: "asc", + order: "dec", offset, + investigation, active: true, limit: PAGINATED_LIST_REQUEST, }); @@ -62,7 +64,7 @@ export function useCurrentDecisions() { TalerExchangeResultByMethod<"getAmlDecisions">, TalerHttpError >( - !session ? undefined : [session, offset, "getAmlDecisions"], + !session ? undefined : [session, offset, filtered ? true : filtered, "getAmlDecisions"], fetcher, ); @@ -95,7 +97,7 @@ export function useAccountDecisions(accountStr: string) { string | undefined, ]) { return await api.getAmlDecisions(officer, { - order: "asc", + order: "dec", offset, account, limit: PAGINATED_LIST_REQUEST, diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index b26e6f430..b421bd8c5 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -51,6 +51,7 @@ import { preloadedForms } from "../forms/index.js"; import { useAccountInformation } from "../hooks/account.js"; import { useAccountDecisions } from "../hooks/decisions.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; +import { useOfficer } from "../hooks/officer.js"; export type AmlEvent = | AmlFormEvent @@ -175,6 +176,10 @@ export function CaseDetails({ account }: { account: string }) { justification: Justification; metadata: FormMetadata; }>(); + const { config, lib } = useExchangeApiContext() + const officer = useOfficer(); + const session = officer.state === "ready" ? officer.account : undefined; + const { i18n } = useTranslationContext(); const details = useAccountInformation(account); @@ -215,7 +220,8 @@ export function CaseDetails({ account }: { account: string }) { } } const { details: accountDetails } = details.body; - const activeDesicion = history.body.find(d => d.is_active) + const activeDecision = history.body.find(d => d.is_active) + const restDecisions = !activeDecision ? history.body : history.body.filter(d => d.rowid !== activeDecision.rowid) const events = getEventsFromAmlHistory( accountDetails, @@ -243,30 +249,9 @@ export function CaseDetails({ account }: { account: string }) { </DefaultForm> ); } - return ( - <div> - <a - // href={privatePages.caseNew.url({ cid: account })} - href="#" - class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" - > - <i18n.Translate>Freeze account</i18n.Translate> - </a> - <a - // href={privatePages.caseNew.url({ cid: account })} - href="#" - class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" - > - <i18n.Translate>New threshold</i18n.Translate> - </a> - <a - // href={privatePages.caseNew.url({ cid: account })} - href="#" - class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" - > - <i18n.Translate>Ask more information</i18n.Translate> - </a> + return ( + <div class="min-w-60"> <header class="flex items-center justify-between border-b border-white/5 px-4 py-4 sm:px-6 sm:py-6 lg:px-8"> <h1 class="text-base font-semibold leading-7 text-black"> <i18n.Translate> @@ -275,10 +260,112 @@ export function CaseDetails({ account }: { account: string }) { </i18n.Translate> </h1> </header> - {!activeDesicion ? <Attention title={i18n.str`No active decision found`} type="warning" /> : <Fragment> - {!activeDesicion.to_investigate ? undefined : <Attention title={i18n.str`Requires investigation`} type="warning" />} - <ShowActiveDecision decision={activeDesicion} /> + + {!activeDecision || !activeDecision.to_investigate ? undefined : <Attention title={i18n.str`Under investigation`} type="warning" > + <i18n.Translate>This account requires a manual review and is waiting for a decision to be made.</i18n.Translate> + </Attention>} + + <div> + <button + onClick={async () => { + if (!session) return; + lib.exchange.makeAmlDesicion(session, { + decision_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), + h_payto: account, + justification: "", + keep_investigating: false, + properties: {}, + new_rules: { + custom_measures: {}, + expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + rules: FREEZE_RULES(config.currency), + successor_measure: "verboten", + }, + }) + }} + class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" + > + <i18n.Translate>Freeze account</i18n.Translate> + </button> + <button + onClick={async () => { + if (!session) return; + lib.exchange.makeAmlDesicion(session, { + decision_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), + h_payto: account, + justification: "", + keep_investigating: false, + properties: {}, + new_rules: { + custom_measures: {}, + expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + rules: THRESHOLD_100_HOUR(config.currency), + successor_measure: "verboten", + }, + }) + }} + + class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" + > + <i18n.Translate>Set threshold to 100 / hour</i18n.Translate> + </button> + <button + onClick={async () => { + if (!session) return; + lib.exchange.makeAmlDesicion(session, { + decision_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), + h_payto: account, + justification: "", + keep_investigating: false, + properties: {}, + new_rules: { + custom_measures: {}, + expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + rules: THRESHOLD_2000_WEEK(config.currency), + successor_measure: "verboten", + }, + }) + }} + + class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" + > + <i18n.Translate>Set threshold to 2000 / week</i18n.Translate> + </button> + <button + onClick={async () => { + if (!session) return; + lib.exchange.makeAmlDesicion(session, { + decision_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()), + h_payto: account, + justification: "", + keep_investigating: false, + properties: {}, + new_measure: "onlyTalers", + new_rules: { + custom_measures: {}, + expiration_time: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + rules: FREEZE_RULES(config.currency), + successor_measure: "verboten", + }, + }) + }} + + class="m-4 rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" + > + <i18n.Translate>Ask for more information</i18n.Translate> + </button> + </div> + + {!activeDecision ? <Attention title={i18n.str`No active decision found`} type="warning" /> : <Fragment> + <h1 class="my-4 text-base font-semibold leading-6 text-black"> + <i18n.Translate>Current active rules</i18n.Translate> + </h1> + + <ShowDecisionInfo decision={activeDecision} /> </Fragment>} + <h1 class="my-4 text-base font-semibold leading-6 text-black"> + <i18n.Translate>KYC collection events</i18n.Translate> + </h1> <ShowTimeline history={events} onSelect={(e) => { @@ -299,25 +386,55 @@ export function CaseDetails({ account }: { account: string }) { /> {/* {selected && <ShowEventDetails event={selected} />} */} {selected && <ShowConsolidated history={events} until={selected} />} + {restDecisions.length > 0 ? + <Fragment> + <h1 class="my-4 text-base font-semibold leading-6 text-black"> + <i18n.Translate>Previous AML decisions</i18n.Translate> + </h1> + {restDecisions.map(d => { + return <ShowDecisionInfo decision={d} /> + })} + + </Fragment> : <div />} </div> ); } -function ShowActiveDecision({ decision }: { decision: TalerExchangeApi.AmlDecision }): VNode { +function ShowDecisionInfo({ decision }: { decision: TalerExchangeApi.AmlDecision }): VNode { const { i18n } = useTranslationContext(); const { config } = useExchangeApiContext() return <Fragment> - <h1 class="mt-4 text-base font-semibold leading-6 text-black"> - <i18n.Translate>Current active rules</i18n.Translate> - </h1> <div class="sm:col-span-5"> <label for="amount" class="block text-sm font-medium leading-6 text-gray-900" > - <b>Expiration time</b> + <b><i18n.Translate>Since</i18n.Translate></b> + <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> + + <div + class="p-4 disabled:bg-gray-200 rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6" + > + + <Time format="dd/MM/yyyy HH:mm:ss" timestamp={AbsoluteTime.fromProtocolTimestamp(decision.decision_time)} /> + </div> + </div> + + + </label> + </div> + + <div class="sm:col-span-5"> + <label + for="amount" + class="block text-sm font-medium leading-6 text-gray-900" + > + {AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(decision.limits.expiration_time)) ? + <b><i18n.Translate>Expired at</i18n.Translate></b> : + <b><i18n.Translate>Expires at</i18n.Translate></b> + } <div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600"> <div @@ -631,3 +748,243 @@ function parseJustification( }; } } + +const THRESHOLD_2000_WEEK: (currency: string) => TalerExchangeApi.KycRule[] = (currency) => [{ + "operation_type": "WITHDRAW", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "DEPOSIT", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "AGGREGATE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "MERGE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "BALANCE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "CLOSE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}] + +const THRESHOLD_100_HOUR: (currency: string) => TalerExchangeApi.KycRule[] = (currency) => [{ + "operation_type": "WITHDRAW", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "DEPOSIT", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "AGGREGATE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "MERGE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "BALANCE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "CLOSE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}] + + + +const FREEZE_RULES: (currency: string) => TalerExchangeApi.KycRule[] = (currency) => [{ + "operation_type": "WITHDRAW", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "DEPOSIT", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "AGGREGATE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "MERGE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "BALANCE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}, +{ + "operation_type": "CLOSE", + "threshold": `${currency}:0`, + "timeframe": { + "d_us": "forever" + }, + "measures": [ + "verboten" + ], + "display_priority": 1, + "exposed": true, + "is_and_combinator": true +}] + diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 9a569cd60..d79ce9006 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -36,6 +36,7 @@ import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js"; import { undefinedIfEmpty } from "./CreateAccount.js"; import { Officer } from "./Officer.js"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; +import { useState } from "preact/hooks"; type FormType = { // state: TalerExchangeApi.AmlState; @@ -43,15 +44,13 @@ type FormType = { export function CasesUI({ records, - // filter, - // onChangeFilter, onFirstPage, onNext, + filtered, }: { + filtered: boolean, onFirstPage?: () => void; onNext?: () => void; - // filter: TalerExchangeApi.AmlState; - // onChangeFilter: (f: TalerExchangeApi.AmlState) => void; records: TalerExchangeApi.AmlDecision[]; }): VNode { const { i18n } = useTranslationContext(); @@ -94,38 +93,27 @@ export function CasesUI({ 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} - converter={amlStateConverter} - choices={[ - { - label: i18n.str`Pending`, - value: "pending", - }, - { - label: i18n.str`Frozen`, - value: "frozen", - }, - { - label: i18n.str`Normal`, - value: "normal", - }, - ]} - /> */} - </div> + {filtered ? + <div class="px-2 sm:flex-auto"> + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Cases under investigation</i18n.Translate> + </h1> + <p class="mt-2 text-sm text-gray-700 w-80"> + <i18n.Translate> + A list of all the accounts which are waiting for a deicison to be made. + </i18n.Translate> + </p> + </div> : <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 known account by the exchange. + </i18n.Translate> + </p> + </div> + } </div> <div class="mt-8 flow-root"> <div class="overflow-x-auto"> @@ -167,7 +155,7 @@ export function CasesUI({ </div> </td> <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900"> - {r.to_investigate ? <ToInvestigateIcon /> : undefined} + {r.to_investigate ? <span title="require investigation"><ToInvestigateIcon /></span> : undefined} </td> </tr> ); @@ -182,19 +170,9 @@ export function CasesUI({ </div> ); } -function ToInvestigateIcon(): VNode { - return <svg title="requires investigation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 w-6"> - <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /> - </svg> -} - -export function Cases() { - // const [stateFilter, setStateFilter] = useState( - // TalerExchangeApi.AmlState.pending, - // ); - - const list = useCurrentDecisions(); +export function Cases({ filtered }: { filtered?: boolean }) { + const list = useCurrentDecisions(filtered); const { i18n } = useTranslationContext(); if (!list) { @@ -251,6 +229,7 @@ export function Cases() { return ( <CasesUI + filtered={!!filtered} records={list.body} onFirstPage={list.isFirstPage ? undefined : list.loadFirst} onNext={list.isLastPage ? undefined : list.loadNext} @@ -262,6 +241,18 @@ export function Cases() { ); } +// function ToInvestigateIcon(): VNode { +// return <svg title="requires investigation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 w-6"> +// <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /> +// </svg> +// } +export const ToInvestigateIcon = () => ( + <svg title="requires investigation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 w-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /> + </svg> +); + + export const PeopleIcon = () => ( <svg xmlns="http://www.w3.org/2000/svg" |