more ui: business and admin
This commit is contained in:
parent
062939d9cc
commit
0b7bbed99d
@ -33,12 +33,7 @@ export function Routing(): VNode {
|
|||||||
const backend = useBackendContext();
|
const backend = useBackendContext();
|
||||||
|
|
||||||
if (backend.state.status === "loggedOut") {
|
if (backend.state.status === "loggedOut") {
|
||||||
return <BankFrame
|
return <BankFrame >
|
||||||
account={undefined}
|
|
||||||
goToBusinessAccount={() => {
|
|
||||||
route("/business");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
@ -67,12 +62,7 @@ export function Routing(): VNode {
|
|||||||
const { isUserAdministrator, username } = backend.state
|
const { isUserAdministrator, username } = backend.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BankFrame
|
<BankFrame account={backend.state.username}>
|
||||||
account={backend.state.username}
|
|
||||||
goToBusinessAccount={() => {
|
|
||||||
route("/business");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Route
|
<Route
|
||||||
path="/test"
|
path="/test"
|
||||||
@ -121,6 +111,9 @@ export function Routing(): VNode {
|
|||||||
onPendingOperationFound={(wopid) => {
|
onPendingOperationFound={(wopid) => {
|
||||||
route(`/operation/${wopid}`);
|
route(`/operation/${wopid}`);
|
||||||
}}
|
}}
|
||||||
|
goToBusinessAccount={() => {
|
||||||
|
route("/business");
|
||||||
|
}}
|
||||||
onRegister={() => {
|
onRegister={() => {
|
||||||
route("/register");
|
route("/register");
|
||||||
}}
|
}}
|
||||||
|
@ -24,6 +24,6 @@ export function ShowInputErrorLabel({
|
|||||||
isDirty: boolean;
|
isDirty: boolean;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
if (message && isDirty)
|
if (message && isDirty)
|
||||||
return <div style={{ marginTop: 8, color: "red" }}>{message}</div>;
|
return <div class="text-base" style={{ color: "red" }}>{message}</div>;
|
||||||
return <Fragment />;
|
return <div class="text-base" style={{ }}> </div>;
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ export interface Props {
|
|||||||
onLoadNotOk: <T>(
|
onLoadNotOk: <T>(
|
||||||
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||||
) => VNode;
|
) => VNode;
|
||||||
|
goToBusinessAccount: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound;
|
export type State = State.Loading | State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound;
|
||||||
@ -51,10 +52,8 @@ export namespace State {
|
|||||||
status: "ready";
|
status: "ready";
|
||||||
error: undefined;
|
error: undefined;
|
||||||
account: string,
|
account: string,
|
||||||
payto: PaytoUriIBAN | PaytoUriTalerBank,
|
|
||||||
balance: AmountJson,
|
|
||||||
balanceIsDebit: boolean,
|
|
||||||
limit: AmountJson,
|
limit: AmountJson,
|
||||||
|
goToBusinessAccount: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvalidIban {
|
export interface InvalidIban {
|
||||||
|
@ -20,7 +20,7 @@ import { useBackendContext } from "../../context/backend.js";
|
|||||||
import { useAccountDetails } from "../../hooks/access.js";
|
import { useAccountDetails } from "../../hooks/access.js";
|
||||||
import { Props, State } from "./index.js";
|
import { Props, State } from "./index.js";
|
||||||
|
|
||||||
export function useComponentState({ account, onLoadNotOk }: Props): State {
|
export function useComponentState({ account, goToBusinessAccount }: Props): State {
|
||||||
const result = useAccountDetails(account);
|
const result = useAccountDetails(account);
|
||||||
const backend = useBackendContext();
|
const backend = useBackendContext();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
@ -60,7 +60,6 @@ export function useComponentState({ account, onLoadNotOk }: Props): State {
|
|||||||
const payto = parsePaytoUri(data.paytoUri);
|
const payto = parsePaytoUri(data.paytoUri);
|
||||||
|
|
||||||
if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) {
|
if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) {
|
||||||
console.log(payto)
|
|
||||||
return {
|
return {
|
||||||
status: "invalid-iban",
|
status: "invalid-iban",
|
||||||
error: result
|
error: result
|
||||||
@ -75,11 +74,9 @@ export function useComponentState({ account, onLoadNotOk }: Props): State {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: "ready",
|
status: "ready",
|
||||||
|
goToBusinessAccount,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
account,
|
account,
|
||||||
balance,
|
|
||||||
balanceIsDebit,
|
|
||||||
limit,
|
limit,
|
||||||
payto
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import { PaymentOptions } from "../PaymentOptions.js";
|
|||||||
import { State } from "./index.js";
|
import { State } from "./index.js";
|
||||||
import { CopyButton } from "../../components/CopyButton.js";
|
import { CopyButton } from "../../components/CopyButton.js";
|
||||||
import { bankUiSettings } from "../../settings.js";
|
import { bankUiSettings } from "../../settings.js";
|
||||||
|
import { useBusinessAccountDetails } from "../../hooks/circuit.js";
|
||||||
|
|
||||||
export function InvalidIbanView({ error }: State.InvalidIban) {
|
export function InvalidIbanView({ error }: State.InvalidIban) {
|
||||||
return (
|
return (
|
||||||
@ -77,11 +78,35 @@ function ImportantMessage(): VNode {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReadyView({ account, balance, balanceIsDebit, limit, payto }: State.Ready): VNode<{}> {
|
export function ReadyView({ account, limit, goToBusinessAccount }: State.Ready): VNode<{}> {
|
||||||
const { i18n } = useTranslationContext();
|
|
||||||
return <Fragment>
|
return <Fragment>
|
||||||
|
<MaybeBusinessButton account={account} onClick={goToBusinessAccount} />
|
||||||
<PaymentOptions limit={limit} />
|
<PaymentOptions limit={limit} />
|
||||||
<Transactions account={account} />
|
<Transactions account={account} />
|
||||||
</Fragment>;
|
</Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MaybeBusinessButton({
|
||||||
|
account,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
account: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const result = useBusinessAccountDetails(account);
|
||||||
|
if (!result.ok) return <Fragment />;
|
||||||
|
return (
|
||||||
|
<div class="w-full flex justify-end">
|
||||||
|
<button
|
||||||
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onClick()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Business Profile</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -39,36 +39,12 @@ const versionText = VERSION
|
|||||||
|
|
||||||
const logger = new Logger("BankFrame");
|
const logger = new Logger("BankFrame");
|
||||||
|
|
||||||
function MaybeBusinessButton({
|
|
||||||
account,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
account: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}): VNode {
|
|
||||||
const { i18n } = useTranslationContext();
|
|
||||||
const result = useBusinessAccountDetails(account);
|
|
||||||
if (!result.ok) return <Fragment />;
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="pure-button pure-button-primary"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onClick();
|
|
||||||
}}
|
|
||||||
>{i18n.str`Business Profile`}</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BankFrame({
|
export function BankFrame({
|
||||||
children,
|
children,
|
||||||
goToBusinessAccount,
|
|
||||||
account,
|
account,
|
||||||
}: {
|
}: {
|
||||||
account: string | undefined,
|
account?: string,
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
goToBusinessAccount?: () => void;
|
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const backend = useBackendContext();
|
const backend = useBackendContext();
|
||||||
@ -489,5 +465,9 @@ function AccountBalance({ account }: { account: string }): VNode {
|
|||||||
const result = useAccountDetails(account);
|
const result = useAccountDetails(account);
|
||||||
if (!result.ok) return <div />
|
if (!result.ok) return <div />
|
||||||
|
|
||||||
return <div>{result.data.balance.credit_debit_indicator === "debit" ? "-" : ""} {Amounts.currencyOf(result.data.balance.amount)} {Amounts.stringifyValue(result.data.balance.amount)}</div>
|
return <div>
|
||||||
|
{Amounts.currencyOf(result.data.balance.amount)}
|
||||||
|
{result.data.balance.credit_debit_indicator === "debit" ? "-" : ""}
|
||||||
|
{Amounts.stringifyValue(result.data.balance.amount)}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -31,14 +31,11 @@ import {
|
|||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { Loading } from "../components/Loading.js";
|
import { Loading } from "../components/Loading.js";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
|
||||||
import { getInitialBackendBaseURL } from "../hooks/backend.js";
|
import { getInitialBackendBaseURL } from "../hooks/backend.js";
|
||||||
import { useSettings } from "../hooks/settings.js";
|
import { useSettings } from "../hooks/settings.js";
|
||||||
import { AccountPage } from "./AccountPage/index.js";
|
import { AccountPage } from "./AccountPage/index.js";
|
||||||
import { AdminHome } from "./admin/Home.js";
|
|
||||||
import { LoginForm } from "./LoginForm.js";
|
import { LoginForm } from "./LoginForm.js";
|
||||||
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
|
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
|
||||||
import { error } from "console";
|
|
||||||
|
|
||||||
const logger = new Logger("AccountPage");
|
const logger = new Logger("AccountPage");
|
||||||
|
|
||||||
@ -56,10 +53,12 @@ export function HomePage({
|
|||||||
onRegister,
|
onRegister,
|
||||||
account,
|
account,
|
||||||
onPendingOperationFound,
|
onPendingOperationFound,
|
||||||
|
goToBusinessAccount,
|
||||||
}: {
|
}: {
|
||||||
account: string,
|
account: string,
|
||||||
onPendingOperationFound: (id: string) => void;
|
onPendingOperationFound: (id: string) => void;
|
||||||
onRegister: () => void;
|
onRegister: () => void;
|
||||||
|
goToBusinessAccount: () => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
@ -72,6 +71,7 @@ export function HomePage({
|
|||||||
return (
|
return (
|
||||||
<AccountPage
|
<AccountPage
|
||||||
account={account}
|
account={account}
|
||||||
|
goToBusinessAccount={goToBusinessAccount}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -33,76 +33,79 @@ export function PaymentOptions({ limit }: { limit: AmountJson }): VNode {
|
|||||||
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
|
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
|
||||||
// const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined);
|
// const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(undefined);
|
||||||
|
|
||||||
return (<fieldset>
|
return (
|
||||||
<legend class="px-4 text-base font-semibold leading-6 text-gray-900">
|
<fieldset>
|
||||||
<i18n.Translate>Send money to</i18n.Translate>
|
<legend class="px-4 text-base font-semibold leading-6 text-gray-900">
|
||||||
</legend>
|
<i18n.Translate>Send money to</i18n.Translate>
|
||||||
|
</legend>
|
||||||
|
|
||||||
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
|
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
|
||||||
{/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */}
|
{/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */}
|
||||||
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
|
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
|
||||||
<input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onClick={() => {
|
<input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onClick={() => {
|
||||||
setTab("charge-wallet")
|
setTab("charge-wallet")
|
||||||
}} />
|
}} />
|
||||||
<span class="flex flex-1">
|
<span class="flex flex-1">
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span id="project-type-0-label" class="block text-sm font-medium text-gray-900">
|
<span id="project-type-0-label" class="block text-sm font-medium text-gray-900">
|
||||||
<i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
|
<i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
|
||||||
</span>
|
</span>
|
||||||
<span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
<span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
||||||
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
|
<i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
</svg>
|
||||||
</svg>
|
</label>
|
||||||
</label>
|
|
||||||
|
|
||||||
|
|
||||||
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
|
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
|
||||||
<input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onClick={() => {
|
<input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onClick={() => {
|
||||||
setTab("wire-transfer")
|
setTab("wire-transfer")
|
||||||
}} />
|
}} />
|
||||||
<span class="flex flex-1">
|
<span class="flex flex-1">
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
|
<span id="project-type-1-label" class="block text-sm font-medium text-gray-900">
|
||||||
<i18n.Translate>another bank account</i18n.Translate>
|
<i18n.Translate>another bank account</i18n.Translate>
|
||||||
</span>
|
</span>
|
||||||
<span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
<span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500">
|
||||||
<i18n.Translate>Make a wire transfer to an account which you know the address.</i18n.Translate>
|
<i18n.Translate>Make a wire transfer to an account which you know the address.</i18n.Translate>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "wire-transfer" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "wire-transfer" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
</svg>
|
||||||
</svg>
|
</label>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
{tab === "charge-wallet" && (
|
||||||
{tab === "charge-wallet" && (
|
<WalletWithdrawForm
|
||||||
<WalletWithdrawForm
|
focus
|
||||||
focus
|
limit={limit}
|
||||||
limit={limit}
|
onSuccess={(id) => {
|
||||||
onSuccess={(id) => {
|
updateSettings("currentWithdrawalOperationId", id);
|
||||||
updateSettings("currentWithdrawalOperationId", id);
|
}}
|
||||||
}}
|
onCancel={() => {
|
||||||
onCancel={() => {
|
setTab(undefined)
|
||||||
setTab(undefined)
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
{tab === "wire-transfer" && (
|
||||||
{tab === "wire-transfer" && (
|
<PaytoWireTransferForm
|
||||||
<PaytoWireTransferForm
|
focus
|
||||||
focus
|
title={i18n.str`Transfer details`}
|
||||||
limit={limit}
|
limit={limit}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
notifyInfo(i18n.str`Wire transfer created!`);
|
notifyInfo(i18n.str`Wire transfer created!`);
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setTab(undefined)
|
setTab(undefined)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</fieldset>)
|
</fieldset>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -44,10 +44,12 @@ const logger = new Logger("PaytoWireTransferForm");
|
|||||||
|
|
||||||
export function PaytoWireTransferForm({
|
export function PaytoWireTransferForm({
|
||||||
focus,
|
focus,
|
||||||
|
title,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onCancel,
|
onCancel,
|
||||||
limit,
|
limit,
|
||||||
}: {
|
}: {
|
||||||
|
title: TranslatedString,
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
onCancel: (() => void) | undefined;
|
onCancel: (() => void) | undefined;
|
||||||
@ -158,7 +160,9 @@ export function PaytoWireTransferForm({
|
|||||||
|
|
||||||
return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
||||||
<div class="px-4 sm:px-0">
|
<div class="px-4 sm:px-0">
|
||||||
<h2 class="text-base font-semibold leading-7 text-gray-900"><i18n.Translate>Transfer details</i18n.Translate></h2>
|
<h2 class="text-base font-semibold leading-7 text-gray-900">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 sm:gap-x-4">
|
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 sm:gap-x-4">
|
||||||
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (!isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
|
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (!isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
|
||||||
|
@ -45,7 +45,7 @@ export function RegistrationPage({
|
|||||||
return <RegistrationForm onComplete={onComplete} />;
|
return <RegistrationForm onComplete={onComplete} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
|
export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect and submit registration data.
|
* Collect and submit registration data.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { VNode,h } from "preact";
|
import { VNode, h } from "preact";
|
||||||
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
|
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
@ -7,137 +7,161 @@ import { buildRequestErrorMessage } from "../utils.js";
|
|||||||
import { AccountForm } from "./admin/AccountForm.js";
|
import { AccountForm } from "./admin/AccountForm.js";
|
||||||
|
|
||||||
export function ShowAccountDetails({
|
export function ShowAccountDetails({
|
||||||
account,
|
account,
|
||||||
onClear,
|
onClear,
|
||||||
onUpdateSuccess,
|
onUpdateSuccess,
|
||||||
onLoadNotOk,
|
onLoadNotOk,
|
||||||
onChangePassword,
|
onChangePassword,
|
||||||
}: {
|
}: {
|
||||||
onLoadNotOk: <T>(
|
onLoadNotOk: <T>(
|
||||||
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||||
) => VNode;
|
) => VNode;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
onChangePassword: () => void;
|
onChangePassword: () => void;
|
||||||
onUpdateSuccess: () => void;
|
onUpdateSuccess: () => void;
|
||||||
account: string;
|
account: string;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const result = useBusinessAccountDetails(account);
|
const result = useBusinessAccountDetails(account);
|
||||||
const { updateAccount } = useAdminAccountAPI();
|
const { updateAccount } = useAdminAccountAPI();
|
||||||
const [update, setUpdate] = useState(false);
|
const [update, setUpdate] = useState(false);
|
||||||
const [submitAccount, setSubmitAccount] = useState<
|
const [submitAccount, setSubmitAccount] = useState<
|
||||||
SandboxBackend.Circuit.CircuitAccountData | undefined
|
SandboxBackend.Circuit.CircuitAccountData | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||||
return onLoadNotOk(result);
|
|
||||||
}
|
|
||||||
if (result.status === HttpStatusCode.NotFound) {
|
|
||||||
return <div>account not found</div>;
|
|
||||||
}
|
|
||||||
return onLoadNotOk(result);
|
return onLoadNotOk(result);
|
||||||
}
|
}
|
||||||
|
if (result.status === HttpStatusCode.NotFound) {
|
||||||
return (
|
return <div>account not found</div>;
|
||||||
<div>
|
}
|
||||||
<div>
|
return onLoadNotOk(result);
|
||||||
<h1 class="nav welcome-text">
|
}
|
||||||
<i18n.Translate>Business account details</i18n.Translate>
|
|
||||||
</h1>
|
async function doUpdate() {
|
||||||
</div>
|
if (!update) {
|
||||||
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
setUpdate(true);
|
||||||
<AccountForm
|
} else {
|
||||||
template={result.data}
|
if (!submitAccount) return;
|
||||||
purpose={update ? "update" : "show"}
|
try {
|
||||||
onChange={(a) => setSubmitAccount(a)}
|
await updateAccount(account, {
|
||||||
/>
|
cashout_address: submitAccount.cashout_address,
|
||||||
|
contact_data: submitAccount.contact_data,
|
||||||
<p class="buttons-account">
|
});
|
||||||
<div
|
onUpdateSuccess();
|
||||||
style={{
|
} catch (error) {
|
||||||
display: "flex",
|
if (error instanceof RequestError) {
|
||||||
justifyContent: "space-between",
|
notify(
|
||||||
flexFlow: "wrap-reverse",
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
}}
|
onClientError: (status) =>
|
||||||
>
|
status === HttpStatusCode.Forbidden
|
||||||
<div>
|
? i18n.str`The rights to change the account are not sufficient`
|
||||||
{onClear ? (
|
: status === HttpStatusCode.NotFound
|
||||||
<input
|
? i18n.str`The username was not found`
|
||||||
class="pure-button"
|
: undefined,
|
||||||
type="submit"
|
}),
|
||||||
value={i18n.str`Close`}
|
);
|
||||||
onClick={async (e) => {
|
} else {
|
||||||
e.preventDefault();
|
notifyError(
|
||||||
onClear();
|
i18n.str`Operation failed, please report`,
|
||||||
}}
|
(error instanceof Error
|
||||||
/>
|
? error.message
|
||||||
) : undefined}
|
: JSON.stringify(error)) as TranslatedString
|
||||||
</div>
|
)
|
||||||
<div style={{ display: "flex" }}>
|
}
|
||||||
<div>
|
}
|
||||||
<input
|
}
|
||||||
id="select-exchange"
|
}
|
||||||
class="pure-button pure-button-primary content"
|
|
||||||
disabled={update && !submitAccount}
|
return (
|
||||||
type="submit"
|
<div>
|
||||||
value={i18n.str`Change password`}
|
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
||||||
onClick={async (e) => {
|
<div class="px-4 sm:px-0">
|
||||||
e.preventDefault();
|
<h2 class="text-base font-semibold leading-7 text-gray-900">
|
||||||
onChangePassword();
|
{update ?
|
||||||
}}
|
<i18n.Translate>Update account</i18n.Translate>
|
||||||
/>
|
:
|
||||||
</div>
|
<i18n.Translate>Account details</i18n.Translate>
|
||||||
<div>
|
}
|
||||||
<input
|
</h2>
|
||||||
id="select-exchange"
|
<div class="mt-4">
|
||||||
class="pure-button pure-button-primary content"
|
<div class="flex items-center justify-between">
|
||||||
disabled={update && !submitAccount}
|
<span class="flex flex-grow flex-col">
|
||||||
type="submit"
|
<span class="text-sm text-black font-medium leading-6 " id="availability-label">
|
||||||
value={update ? i18n.str`Confirm` : i18n.str`Update`}
|
<i18n.Translate>change the account details</i18n.Translate>
|
||||||
onClick={async (e) => {
|
</span>
|
||||||
e.preventDefault();
|
</span>
|
||||||
|
<button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
|
||||||
if (!update) {
|
onClick={() => {
|
||||||
setUpdate(true);
|
setUpdate(!update)
|
||||||
} else {
|
}}>
|
||||||
if (!submitAccount) return;
|
<span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||||
try {
|
</button>
|
||||||
await updateAccount(account, {
|
|
||||||
cashout_address: submitAccount.cashout_address,
|
|
||||||
contact_data: submitAccount.contact_data,
|
|
||||||
});
|
|
||||||
onUpdateSuccess();
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof RequestError) {
|
|
||||||
notify(
|
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
|
||||||
onClientError: (status) =>
|
|
||||||
status === HttpStatusCode.Forbidden
|
|
||||||
? i18n.str`The rights to change the account are not sufficient`
|
|
||||||
: status === HttpStatusCode.NotFound
|
|
||||||
? i18n.str`The username was not found`
|
|
||||||
: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
notifyError(
|
|
||||||
i18n.str`Operation failed, please report`,
|
|
||||||
(error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: JSON.stringify(error)) as TranslatedString
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
}
|
</div>
|
||||||
|
<AccountForm
|
||||||
|
template={result.data}
|
||||||
|
purpose={update ? "update" : "show"}
|
||||||
|
onChange={(a) => setSubmitAccount(a)}
|
||||||
|
>
|
||||||
|
|
||||||
|
</AccountForm>
|
||||||
|
|
||||||
|
<p class="buttons-account">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexFlow: "wrap-reverse",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{onClear ? (
|
||||||
|
<input
|
||||||
|
class="pure-button"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Close`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClear();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="select-exchange"
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
disabled={update && !submitAccount}
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Change password`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onChangePassword();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="select-exchange"
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
disabled={update && !submitAccount}
|
||||||
|
type="submit"
|
||||||
|
value={update ? i18n.str`Confirm` : i18n.str`Update`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
doUpdate()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,131 +1,181 @@
|
|||||||
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
|
||||||
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
|
|
||||||
import { useState } from "preact/hooks";
|
|
||||||
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { VNode,h ,Fragment} from "preact";
|
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
import { Fragment, VNode, h } from "preact";
|
||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
||||||
|
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
|
||||||
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
|
|
||||||
export function UpdateAccountPassword({
|
export function UpdateAccountPassword({
|
||||||
account,
|
account,
|
||||||
onClear,
|
onCancel,
|
||||||
onUpdateSuccess,
|
onUpdateSuccess,
|
||||||
onLoadNotOk,
|
onLoadNotOk,
|
||||||
}: {
|
focus,
|
||||||
onLoadNotOk: <T>(
|
}: {
|
||||||
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
onLoadNotOk: <T>(
|
||||||
) => VNode;
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||||
onClear: () => void;
|
) => VNode;
|
||||||
onUpdateSuccess: () => void;
|
onCancel: () => void;
|
||||||
account: string;
|
focus?: boolean,
|
||||||
}): VNode {
|
onUpdateSuccess: () => void;
|
||||||
const { i18n } = useTranslationContext();
|
account: string;
|
||||||
const result = useBusinessAccountDetails(account);
|
}): VNode {
|
||||||
const { changePassword } = useAdminAccountAPI();
|
const { i18n } = useTranslationContext();
|
||||||
const [password, setPassword] = useState<string | undefined>();
|
const result = useBusinessAccountDetails(account);
|
||||||
const [repeat, setRepeat] = useState<string | undefined>();
|
const { changePassword } = useAdminAccountAPI();
|
||||||
|
const [password, setPassword] = useState<string | undefined>();
|
||||||
if (!result.ok) {
|
const [repeat, setRepeat] = useState<string | undefined>();
|
||||||
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
|
||||||
return onLoadNotOk(result);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
}
|
useEffect(() => {
|
||||||
if (result.status === HttpStatusCode.NotFound) {
|
if (focus) ref.current?.focus();
|
||||||
return <div>account not found</div>;
|
}, [focus]);
|
||||||
}
|
|
||||||
|
if (!result.ok) {
|
||||||
|
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||||
return onLoadNotOk(result);
|
return onLoadNotOk(result);
|
||||||
}
|
}
|
||||||
|
if (result.status === HttpStatusCode.NotFound) {
|
||||||
const errors = undefinedIfEmpty({
|
return <div>account not found</div>;
|
||||||
password: !password ? i18n.str`required` : undefined,
|
}
|
||||||
repeat: !repeat
|
return onLoadNotOk(result);
|
||||||
? i18n.str`required`
|
}
|
||||||
: password !== repeat
|
|
||||||
? i18n.str`password doesn't match`
|
const errors = undefinedIfEmpty({
|
||||||
: undefined,
|
password: !password ? i18n.str`required` : undefined,
|
||||||
});
|
repeat: !repeat
|
||||||
|
? i18n.str`required`
|
||||||
return (
|
: password !== repeat
|
||||||
<div>
|
? i18n.str`password doesn't match`
|
||||||
<div>
|
: undefined,
|
||||||
<h1 class="nav welcome-text">
|
});
|
||||||
<i18n.Translate>Update password for {account}</i18n.Translate>
|
|
||||||
</h1>
|
async function doChangePassword() {
|
||||||
</div>
|
if (!!errors || !password) return;
|
||||||
|
try {
|
||||||
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
const r = await changePassword(account, {
|
||||||
<form class="pure-form">
|
new_password: password,
|
||||||
<fieldset>
|
});
|
||||||
<label>{i18n.str`Password`}</label>
|
onUpdateSuccess();
|
||||||
<input
|
} catch (error) {
|
||||||
type="password"
|
if (error instanceof RequestError) {
|
||||||
value={password ?? ""}
|
notify(buildRequestErrorMessage(i18n, error.cause));
|
||||||
onChange={(e) => {
|
} else {
|
||||||
setPassword(e.currentTarget.value);
|
notifyError(i18n.str`Operation failed, please report`, (error instanceof Error
|
||||||
}}
|
? error.message
|
||||||
/>
|
: JSON.stringify(error)) as TranslatedString)
|
||||||
<ShowInputErrorLabel
|
}
|
||||||
message={errors?.password}
|
}
|
||||||
isDirty={password !== undefined}
|
}
|
||||||
/>
|
|
||||||
</fieldset>
|
return (
|
||||||
<fieldset>
|
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
||||||
<label>{i18n.str`Repeat password`}</label>
|
<div class="px-4 sm:px-0">
|
||||||
<input
|
<h2 class="text-base font-semibold leading-7 text-gray-900">
|
||||||
type="password"
|
<i18n.Translate>Update password for account "{account}"</i18n.Translate>
|
||||||
value={repeat ?? ""}
|
</h2>
|
||||||
onChange={(e) => {
|
|
||||||
setRepeat(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.repeat}
|
|
||||||
isDirty={repeat !== undefined}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
<p>
|
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
class="pure-button"
|
|
||||||
type="submit"
|
|
||||||
value={i18n.str`Close`}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onClear();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
id="select-exchange"
|
|
||||||
class="pure-button pure-button-primary content"
|
|
||||||
disabled={!!errors}
|
|
||||||
type="submit"
|
|
||||||
value={i18n.str`Confirm`}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!!errors || !password) return;
|
|
||||||
try {
|
|
||||||
const r = await changePassword(account, {
|
|
||||||
new_password: password,
|
|
||||||
});
|
|
||||||
onUpdateSuccess();
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof RequestError) {
|
|
||||||
notify(buildRequestErrorMessage(i18n, error.cause));
|
|
||||||
} else {
|
|
||||||
notifyError(i18n.str`Operation failed, please report`, (error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: JSON.stringify(error)) as TranslatedString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<form
|
||||||
}
|
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="off"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="px-4 py-6 sm:p-8">
|
||||||
|
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
|
||||||
|
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium leading-6 text-gray-900"
|
||||||
|
for="password"
|
||||||
|
>
|
||||||
|
{i18n.str`New password`}
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
type="password"
|
||||||
|
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
data-error={!!errors?.password && password !== undefined}
|
||||||
|
value={password ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPassword(e.currentTarget.value)
|
||||||
|
}}
|
||||||
|
// placeholder=""
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.password}
|
||||||
|
isDirty={password !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* <p class="mt-2 text-sm text-gray-500" >
|
||||||
|
<i18n.Translate>user </i18n.Translate>
|
||||||
|
</p> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium leading-6 text-gray-900"
|
||||||
|
for="repeat"
|
||||||
|
>
|
||||||
|
{i18n.str`Type it again`}
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
name="repeat"
|
||||||
|
id="repeat"
|
||||||
|
data-error={!!errors?.repeat && repeat !== undefined}
|
||||||
|
value={repeat ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRepeat(e.currentTarget.value)
|
||||||
|
}}
|
||||||
|
// placeholder=""
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.repeat}
|
||||||
|
isDirty={repeat !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500" >
|
||||||
|
<i18n.Translate>repeat the same password</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
|
||||||
|
{onCancel ?
|
||||||
|
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
: <div />
|
||||||
|
}
|
||||||
|
<button type="submit"
|
||||||
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
disabled={!!errors}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
doChangePassword()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Change</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
@ -7,50 +7,30 @@ import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
|
|||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
|
||||||
export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
|
export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const r = useBackendContext();
|
const r = useBackendContext();
|
||||||
const account = r.state.status === "loggedIn" ? r.state.username : "admin";
|
const account = r.state.status === "loggedIn" ? r.state.username : "admin";
|
||||||
const result = useAccountDetails(account);
|
const result = useAccountDetails(account);
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
return handleNotOkResult(i18n, onRegister)(result);
|
return handleNotOkResult(i18n, onRegister)(result);
|
||||||
}
|
|
||||||
const { data } = result;
|
|
||||||
const balance = Amounts.parseOrThrow(data.balance.amount);
|
|
||||||
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
|
|
||||||
const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
|
|
||||||
const limit = balanceIsDebit
|
|
||||||
? Amounts.sub(debitThreshold, balance).amount
|
|
||||||
: Amounts.add(balance, debitThreshold).amount;
|
|
||||||
if (!balance) return <Fragment />;
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<section id="assets">
|
|
||||||
<div class="asset-summary">
|
|
||||||
<h2>{i18n.str`Bank account balance`}</h2>
|
|
||||||
{!balance ? (
|
|
||||||
<div class="large-amount" style={{ color: "gray" }}>
|
|
||||||
Waiting server response...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div class="large-amount amount">
|
|
||||||
{balanceIsDebit ? <b>-</b> : null}
|
|
||||||
<span class="value">{`${Amounts.stringifyValue(balance)}`}</span>
|
|
||||||
|
|
||||||
<span class="currency">{`${balance.currency}`}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<PaytoWireTransferForm
|
|
||||||
focus
|
|
||||||
limit={limit}
|
|
||||||
onSuccess={() => {
|
|
||||||
notifyInfo(i18n.str`Wire transfer created!`);
|
|
||||||
}}
|
|
||||||
onCancel={undefined}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const { data } = result;
|
||||||
|
const balance = Amounts.parseOrThrow(data.balance.amount);
|
||||||
|
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
|
||||||
|
const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
|
||||||
|
const limit = balanceIsDebit
|
||||||
|
? Amounts.sub(debitThreshold, balance).amount
|
||||||
|
: Amounts.add(balance, debitThreshold).amount;
|
||||||
|
if (!balance) return <Fragment />;
|
||||||
|
return (
|
||||||
|
<PaytoWireTransferForm
|
||||||
|
title={i18n.str`Make a wire transfer`}
|
||||||
|
limit={limit}
|
||||||
|
onSuccess={() => {
|
||||||
|
notifyInfo(i18n.str`Wire transfer created!`);
|
||||||
|
}}
|
||||||
|
onCancel={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { VNode,h } from "preact";
|
import { ComponentChildren, VNode, h } from "preact";
|
||||||
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
|
||||||
import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
|
import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
|
||||||
import { useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { parsePaytoUri } from "@gnu-taler/taler-util";
|
import { buildPayto, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
|
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
|
||||||
const EMAIL_REGEX =
|
const EMAIL_REGEX =
|
||||||
@ -19,201 +19,301 @@ const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function AccountForm({
|
export function AccountForm({
|
||||||
template,
|
template,
|
||||||
purpose,
|
purpose,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
focus,
|
||||||
template: SandboxBackend.Circuit.CircuitAccountData | undefined;
|
children,
|
||||||
onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
|
}: {
|
||||||
purpose: "create" | "update" | "show";
|
focus?: boolean,
|
||||||
}): VNode {
|
children: ComponentChildren,
|
||||||
const initial = initializeFromTemplate(template);
|
template: SandboxBackend.Circuit.CircuitAccountData | undefined;
|
||||||
const [form, setForm] = useState(initial);
|
onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
|
||||||
const [errors, setErrors] = useState<
|
purpose: "create" | "update" | "show";
|
||||||
RecursivePartial<typeof initial> | undefined
|
}): VNode {
|
||||||
>(undefined);
|
const initial = initializeFromTemplate(template);
|
||||||
const { i18n } = useTranslationContext();
|
const [form, setForm] = useState(initial);
|
||||||
|
const [errors, setErrors] = useState<
|
||||||
function updateForm(newForm: typeof initial): void {
|
RecursivePartial<typeof initial> | undefined
|
||||||
const parsed = !newForm.cashout_address
|
>(undefined);
|
||||||
? undefined
|
const { i18n } = useTranslationContext();
|
||||||
: parsePaytoUri(newForm.cashout_address);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
|
if (focus) ref.current?.focus();
|
||||||
cashout_address: !newForm.cashout_address
|
}, [focus]);
|
||||||
|
|
||||||
|
function updateForm(newForm: typeof initial): void {
|
||||||
|
|
||||||
|
const parsed = !newForm.cashout_address
|
||||||
|
? undefined
|
||||||
|
: buildPayto("iban", newForm.cashout_address, undefined);;
|
||||||
|
|
||||||
|
const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
|
||||||
|
cashout_address: !newForm.cashout_address
|
||||||
|
? i18n.str`required`
|
||||||
|
: !parsed
|
||||||
|
? i18n.str`does not follow the pattern`
|
||||||
|
: !parsed.isKnown || parsed.targetType !== "iban"
|
||||||
|
? i18n.str`only "IBAN" target are supported`
|
||||||
|
: !IBAN_REGEX.test(parsed.iban)
|
||||||
|
? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||||
|
: validateIBAN(parsed.iban, i18n),
|
||||||
|
contact_data: undefinedIfEmpty({
|
||||||
|
email: !newForm.contact_data?.email
|
||||||
? i18n.str`required`
|
? i18n.str`required`
|
||||||
: !parsed
|
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
||||||
? i18n.str`does not follow the pattern`
|
? i18n.str`it should be an email`
|
||||||
: !parsed.isKnown || parsed.targetType !== "iban"
|
: undefined,
|
||||||
? i18n.str`only "IBAN" target are supported`
|
phone: !newForm.contact_data?.phone
|
||||||
: !IBAN_REGEX.test(parsed.iban)
|
? i18n.str`required`
|
||||||
? i18n.str`IBAN should have just uppercased letters and numbers`
|
: !newForm.contact_data.phone.startsWith("+")
|
||||||
: validateIBAN(parsed.iban, i18n),
|
? i18n.str`should start with +`
|
||||||
contact_data: undefinedIfEmpty({
|
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
||||||
email: !newForm.contact_data?.email
|
? i18n.str`phone number can't have other than numbers`
|
||||||
? i18n.str`required`
|
|
||||||
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
|
||||||
? i18n.str`it should be an email`
|
|
||||||
: undefined,
|
: undefined,
|
||||||
phone: !newForm.contact_data?.phone
|
}),
|
||||||
? i18n.str`required`
|
// iban: !newForm.iban
|
||||||
: !newForm.contact_data.phone.startsWith("+")
|
// ? undefined //optional field
|
||||||
? i18n.str`should start with +`
|
// : !IBAN_REGEX.test(newForm.iban)
|
||||||
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
// ? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||||
? i18n.str`phone number can't have other than numbers`
|
// : validateIBAN(newForm.iban, i18n),
|
||||||
: undefined,
|
name: !newForm.name ? i18n.str`required` : undefined,
|
||||||
}),
|
username: !newForm.username ? i18n.str`required` : undefined,
|
||||||
iban: !newForm.iban
|
});
|
||||||
? undefined //optional field
|
setErrors(errors);
|
||||||
: !IBAN_REGEX.test(newForm.iban)
|
setForm(newForm);
|
||||||
? i18n.str`IBAN should have just uppercased letters and numbers`
|
onChange(errors === undefined ? (newForm as any) : undefined);
|
||||||
: validateIBAN(newForm.iban, i18n),
|
|
||||||
name: !newForm.name ? i18n.str`required` : undefined,
|
|
||||||
username: !newForm.username ? i18n.str`required` : undefined,
|
|
||||||
});
|
|
||||||
setErrors(errors);
|
|
||||||
setForm(newForm);
|
|
||||||
onChange(errors === undefined ? (newForm as any) : undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form class="pure-form">
|
|
||||||
<fieldset>
|
|
||||||
<label for="username">
|
|
||||||
{i18n.str`Username`}
|
|
||||||
{purpose === "create" && <b style={{ color: "red" }}>*</b>}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="username"
|
|
||||||
type="text"
|
|
||||||
disabled={purpose !== "create"}
|
|
||||||
value={form.username}
|
|
||||||
onChange={(e) => {
|
|
||||||
form.username = e.currentTarget.value;
|
|
||||||
updateForm(structuredClone(form));
|
|
||||||
}}
|
|
||||||
/>{" "}
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.username}
|
|
||||||
isDirty={form.username !== undefined}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<label>
|
|
||||||
{i18n.str`Name`}
|
|
||||||
{purpose === "create" && <b style={{ color: "red" }}>*</b>}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
disabled={purpose !== "create"}
|
|
||||||
value={form.name ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
form.name = e.currentTarget.value;
|
|
||||||
updateForm(structuredClone(form));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.name}
|
|
||||||
isDirty={form.name !== undefined}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
{purpose !== "create" && (
|
|
||||||
<fieldset>
|
|
||||||
<label>{i18n.str`Internal IBAN`}</label>
|
|
||||||
<input
|
|
||||||
disabled={true}
|
|
||||||
value={form.iban ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
form.iban = e.currentTarget.value;
|
|
||||||
updateForm(structuredClone(form));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.iban}
|
|
||||||
isDirty={form.iban !== undefined}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
)}
|
|
||||||
<fieldset>
|
|
||||||
<label>
|
|
||||||
{i18n.str`Email`}
|
|
||||||
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
disabled={purpose === "show"}
|
|
||||||
value={form.contact_data.email ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
form.contact_data.email = e.currentTarget.value;
|
|
||||||
updateForm(structuredClone(form));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.contact_data?.email}
|
|
||||||
isDirty={form.contact_data.email !== undefined}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<label>
|
|
||||||
{i18n.str`Phone`}
|
|
||||||
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
disabled={purpose === "show"}
|
|
||||||
value={form.contact_data.phone ?? ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
form.contact_data.phone = e.currentTarget.value;
|
|
||||||
updateForm(structuredClone(form));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.contact_data?.phone}
|
|
||||||
isDirty={form.contact_data?.phone !== undefined}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<label>
|
|
||||||
{i18n.str`Cashout address`}
|
|
||||||
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
disabled={purpose === "show"}
|
|
||||||
value={(form.cashout_address ?? "").substring("payto://iban/".length)}
|
|
||||||
onChange={(e) => {
|
|
||||||
form.cashout_address = "payto://iban/" + e.currentTarget.value;
|
|
||||||
updateForm(structuredClone(form));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ShowInputErrorLabel
|
|
||||||
message={errors?.cashout_address}
|
|
||||||
isDirty={form.cashout_address !== undefined}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeFromTemplate(
|
return (
|
||||||
account: SandboxBackend.Circuit.CircuitAccountData | undefined,
|
<form
|
||||||
): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
|
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
|
||||||
const emptyAccount = {
|
autoCapitalize="none"
|
||||||
cashout_address: undefined,
|
autoCorrect="off"
|
||||||
iban: undefined,
|
onSubmit={e => {
|
||||||
name: undefined,
|
e.preventDefault()
|
||||||
username: undefined,
|
}}
|
||||||
contact_data: undefined,
|
>
|
||||||
};
|
<div class="px-4 py-6 sm:p-8">
|
||||||
const emptyContact = {
|
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
|
||||||
email: undefined,
|
|
||||||
phone: undefined,
|
|
||||||
};
|
<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
|
class="block text-sm font-medium leading-6 text-gray-900"
|
||||||
structuredClone(account) ?? emptyAccount;
|
for="username"
|
||||||
if (typeof initial.contact_data === "undefined") {
|
>
|
||||||
initial.contact_data = emptyContact;
|
{i18n.str`Username`}
|
||||||
}
|
{purpose === "create" && <b style={{ color: "red" }}> *</b>}
|
||||||
initial.contact_data.email;
|
</label>
|
||||||
return initial as any;
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
type="text"
|
||||||
|
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
data-error={!!errors?.username && form.username !== undefined}
|
||||||
|
disabled={purpose !== "create"}
|
||||||
|
value={form.username ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.username = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
// placeholder=""
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.username}
|
||||||
|
isDirty={form.username !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500" >
|
||||||
|
<i18n.Translate>account identification in the bank</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium leading-6 text-gray-900"
|
||||||
|
for="name"
|
||||||
|
>
|
||||||
|
{i18n.str`Name`}
|
||||||
|
{purpose === "create" && <b style={{ color: "red" }}> *</b>}
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
name="name"
|
||||||
|
data-error={!!errors?.name && form.name !== undefined}
|
||||||
|
id="name"
|
||||||
|
disabled={purpose !== "create"}
|
||||||
|
value={form.name ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.name = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
// placeholder=""
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.name}
|
||||||
|
isDirty={form.name !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500" >
|
||||||
|
<i18n.Translate>name of the person owner the account</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{purpose !== "create" && (<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium leading-6 text-gray-900"
|
||||||
|
for="internal-iban"
|
||||||
|
>
|
||||||
|
{i18n.str`Internal IBAN`}
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
name="internal-iban"
|
||||||
|
id="internal-iban"
|
||||||
|
disabled={true}
|
||||||
|
value={form.iban ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500" >
|
||||||
|
<i18n.Translate>international bank account number</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>)}
|
||||||
|
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium leading-6 text-gray-900"
|
||||||
|
for="email"
|
||||||
|
>
|
||||||
|
{i18n.str`Email`}
|
||||||
|
{purpose === "create" && <b style={{ color: "red" }}> *</b>}
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
data-error={!!errors?.contact_data?.email && form.contact_data.email !== undefined}
|
||||||
|
disabled={purpose !== "create"}
|
||||||
|
value={form.contact_data.email ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.contact_data.email = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.contact_data?.email}
|
||||||
|
isDirty={form.contact_data.email !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium leading-6 text-gray-900"
|
||||||
|
for="phone"
|
||||||
|
>
|
||||||
|
{i18n.str`Phone`}
|
||||||
|
{purpose === "create" && <b style={{ color: "red" }}> *</b>}
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
name="phone"
|
||||||
|
id="phone"
|
||||||
|
disabled={purpose !== "create"}
|
||||||
|
value={form.contact_data.phone ?? ""}
|
||||||
|
data-error={!!errors?.contact_data?.phone && form.contact_data.phone !== undefined}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.contact_data.phone = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
// placeholder=""
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.contact_data?.phone}
|
||||||
|
isDirty={form.contact_data.phone !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium leading-6 text-gray-900"
|
||||||
|
for="cashout"
|
||||||
|
>
|
||||||
|
{i18n.str`Cashout IBAN`}
|
||||||
|
{purpose !== "show" && <b style={{ color: "red" }}> *</b>}
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
data-error={!!errors?.cashout_address && form.cashout_address !== undefined}
|
||||||
|
class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
name="cashout"
|
||||||
|
id="cashout"
|
||||||
|
disabled={purpose === "show"}
|
||||||
|
value={form.cashout_address ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.cashout_address = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.cashout_address}
|
||||||
|
isDirty={form.cashout_address !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500" >
|
||||||
|
<i18n.Translate>account number where the money is going to be sent when doing cashouts</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeFromTemplate(
|
||||||
|
account: SandboxBackend.Circuit.CircuitAccountData | undefined,
|
||||||
|
): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
|
||||||
|
const emptyAccount = {
|
||||||
|
cashout_address: undefined,
|
||||||
|
iban: undefined,
|
||||||
|
name: undefined,
|
||||||
|
username: undefined,
|
||||||
|
contact_data: undefined,
|
||||||
|
};
|
||||||
|
const emptyContact = {
|
||||||
|
email: undefined,
|
||||||
|
phone: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
|
||||||
|
structuredClone(account) ?? emptyAccount;
|
||||||
|
if (typeof initial.contact_data === "undefined") {
|
||||||
|
initial.contact_data = emptyContact;
|
||||||
}
|
}
|
||||||
|
initial.contact_data.email;
|
||||||
|
return initial as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@ interface Props {
|
|||||||
onAction: (type: AccountAction, account: string) => void;
|
onAction: (type: AccountAction, account: string) => void;
|
||||||
account: string | undefined;
|
account: string | undefined;
|
||||||
onRegister: () => void;
|
onRegister: () => void;
|
||||||
|
onCreateAccount: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountList({ account, onAction, onRegister }: Props): VNode {
|
export function AccountList({ account, onAction, onCreateAccount, onRegister }: Props): VNode {
|
||||||
const result = useBusinessAccounts({ account });
|
const result = useBusinessAccounts({ account });
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
@ -22,48 +22,60 @@ export function AccountList({ account, onAction, onRegister }: Props): VNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { customers } = result.data;
|
const { customers } = result.data;
|
||||||
return <section
|
return <div class="px-4 sm:px-6 lg:px-8">
|
||||||
id="main"
|
<div class="sm:flex sm:items-center">
|
||||||
style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
|
<div class="sm:flex-auto">
|
||||||
>
|
<h1 class="text-base font-semibold leading-6 text-gray-900">
|
||||||
{!customers.length ? (
|
<i18n.Translate>Accounts</i18n.Translate>
|
||||||
<div></div>
|
</h1>
|
||||||
) : (
|
<p class="mt-2 text-sm text-gray-700">
|
||||||
<article>
|
<i18n.Translate>A list of all business account in the bank.</i18n.Translate>
|
||||||
<h2>{i18n.str`Accounts:`}</h2>
|
</p>
|
||||||
<div class="results">
|
</div>
|
||||||
<table class="pure-table pure-table-striped">
|
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||||
<thead>
|
<button type="button" class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
<tr>
|
onClick={(e) => {
|
||||||
<th>{i18n.str`Username`}</th>
|
e.preventDefault()
|
||||||
<th>{i18n.str`Name`}</th>
|
onCreateAccount()
|
||||||
<th>{i18n.str`Balance`}</th>
|
}}>
|
||||||
<th>{i18n.str`Actions`}</th>
|
<i18n.Translate>Create account</i18n.Translate>
|
||||||
</tr>
|
</button>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
</div>
|
||||||
{customers.map((item, idx) => {
|
<div class="mt-8 flow-root">
|
||||||
const balance = !item.balance
|
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
? undefined
|
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||||
: Amounts.parse(item.balance.amount);
|
{!customers.length ? (
|
||||||
const balanceIsDebit =
|
<div></div>
|
||||||
item.balance &&
|
) : (
|
||||||
item.balance.credit_debit_indicator == "debit";
|
<table class="min-w-full divide-y divide-gray-300">
|
||||||
return (
|
<thead>
|
||||||
<tr key={idx}>
|
<tr>
|
||||||
<td>
|
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">{i18n.str`Username`}</th>
|
||||||
<a
|
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Name`}</th>
|
||||||
href="#"
|
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Balance`}</th>
|
||||||
onClick={(e) => {
|
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
|
||||||
e.preventDefault();
|
<span class="sr-only">{i18n.str`Actions`}</span>
|
||||||
onAction("show-details", item.username)
|
</th>
|
||||||
}}
|
</tr>
|
||||||
>
|
</thead>
|
||||||
{item.username}
|
<tbody class="divide-y divide-gray-200">
|
||||||
</a>
|
{customers.map((item, idx) => {
|
||||||
|
const balance = !item.balance
|
||||||
|
? undefined
|
||||||
|
: Amounts.parse(item.balance.amount);
|
||||||
|
const balanceIsDebit =
|
||||||
|
item.balance &&
|
||||||
|
item.balance.credit_debit_indicator == "debit";
|
||||||
|
|
||||||
|
return <tr key={idx}>
|
||||||
|
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
|
||||||
|
{item.username}
|
||||||
</td>
|
</td>
|
||||||
<td>{item.name}</td>
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
<td>
|
{item.name}
|
||||||
|
</td>
|
||||||
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
{!balance ? (
|
{!balance ? (
|
||||||
i18n.str`unknown`
|
i18n.str`unknown`
|
||||||
) : (
|
) : (
|
||||||
@ -77,9 +89,8 @@ export function AccountList({ account, onAction, onRegister }: Props): VNode {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||||
<a
|
<a href="#" class="text-indigo-600 hover:text-indigo-900"
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onAction("update-password", item.username)
|
onAction("update-password", item.username)
|
||||||
@ -87,34 +98,71 @@ export function AccountList({ account, onAction, onRegister }: Props): VNode {
|
|||||||
>
|
>
|
||||||
change password
|
change password
|
||||||
</a>
|
</a>
|
||||||
|
<br/>
|
||||||
<a
|
<a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
|
||||||
href="#"
|
e.preventDefault();
|
||||||
onClick={(e) => {
|
onAction("show-cashout", item.username)
|
||||||
e.preventDefault();
|
}}
|
||||||
onAction("show-cashout", item.username)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
cashouts
|
cashouts
|
||||||
</a>
|
</a>
|
||||||
|
<br/>
|
||||||
<a
|
<a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
|
||||||
href="#"
|
e.preventDefault();
|
||||||
onClick={(e) => {
|
onAction("remove-account", item.username)
|
||||||
e.preventDefault();
|
}}
|
||||||
onAction("remove-account", item.username)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
remove
|
remove
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
</tbody>
|
{/* <!-- More people... --> */}
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
|
// return <section
|
||||||
|
// id="main"
|
||||||
|
// style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
|
||||||
|
// >
|
||||||
|
// <article>
|
||||||
|
// <h2>{i18n.str`Accounts:`}</h2>
|
||||||
|
// <div class="results">
|
||||||
|
// <table class="pure-table pure-table-striped">
|
||||||
|
// <tbody>
|
||||||
|
// return (
|
||||||
|
// <tr key={idx}>
|
||||||
|
// <td>
|
||||||
|
// <a
|
||||||
|
// href="#"
|
||||||
|
// onClick={(e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// onAction("show-details", item.username)
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// {item.username}
|
||||||
|
// </a>
|
||||||
|
// </td>
|
||||||
|
// <td>{item.name}</td>
|
||||||
|
// <td>
|
||||||
|
//
|
||||||
|
// </td>
|
||||||
|
// <td>
|
||||||
|
|
||||||
|
// </td>
|
||||||
|
// </tr>
|
||||||
|
// );
|
||||||
|
// })}
|
||||||
|
// </tbody>
|
||||||
|
// </table>
|
||||||
|
// </div>
|
||||||
|
// </article>
|
||||||
|
// )}
|
||||||
|
// </section>
|
||||||
}
|
}
|
@ -8,100 +8,94 @@ import { getRandomPassword } from "../rnd.js";
|
|||||||
import { AccountForm } from "./AccountForm.js";
|
import { AccountForm } from "./AccountForm.js";
|
||||||
|
|
||||||
export function CreateNewAccount({
|
export function CreateNewAccount({
|
||||||
onClose,
|
onCancel,
|
||||||
onCreateSuccess,
|
onCreateSuccess,
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void;
|
onCancel: () => void;
|
||||||
onCreateSuccess: (password: string) => void;
|
onCreateSuccess: (password: string) => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const { createAccount } = useAdminAccountAPI();
|
const { createAccount } = useAdminAccountAPI();
|
||||||
const [submitAccount, setSubmitAccount] = useState<
|
const [submitAccount, setSubmitAccount] = useState<
|
||||||
SandboxBackend.Circuit.CircuitAccountData | undefined
|
SandboxBackend.Circuit.CircuitAccountData | undefined
|
||||||
>();
|
>();
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h1 class="nav welcome-text">
|
|
||||||
<i18n.Translate>New account</i18n.Translate>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
async function doCreate() {
|
||||||
<AccountForm
|
if (!submitAccount) return;
|
||||||
template={undefined}
|
try {
|
||||||
purpose="create"
|
const account: SandboxBackend.Circuit.CircuitAccountRequest =
|
||||||
onChange={(a) => {
|
{
|
||||||
setSubmitAccount(a);
|
cashout_address: submitAccount.cashout_address,
|
||||||
}}
|
contact_data: submitAccount.contact_data,
|
||||||
/>
|
internal_iban: submitAccount.iban,
|
||||||
|
name: submitAccount.name,
|
||||||
|
username: submitAccount.username,
|
||||||
|
password: getRandomPassword(),
|
||||||
|
};
|
||||||
|
|
||||||
<p>
|
await createAccount(account);
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
onCreateSuccess(account.password);
|
||||||
<div>
|
} catch (error) {
|
||||||
<input
|
if (error instanceof RequestError) {
|
||||||
class="pure-button"
|
notify(
|
||||||
type="submit"
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
value={i18n.str`Close`}
|
onClientError: (status) =>
|
||||||
onClick={async (e) => {
|
status === HttpStatusCode.Forbidden
|
||||||
e.preventDefault();
|
? i18n.str`The rights to perform the operation are not sufficient`
|
||||||
onClose();
|
: status === HttpStatusCode.BadRequest
|
||||||
}}
|
? i18n.str`Server replied that input data was invalid`
|
||||||
/>
|
: status === HttpStatusCode.Conflict
|
||||||
</div>
|
? i18n.str`At least one registration detail was not available`
|
||||||
<div>
|
: undefined,
|
||||||
<input
|
}),
|
||||||
id="select-exchange"
|
);
|
||||||
class="pure-button pure-button-primary content"
|
} else {
|
||||||
disabled={!submitAccount}
|
notifyError(
|
||||||
type="submit"
|
i18n.str`Operation failed, please report`,
|
||||||
value={i18n.str`Confirm`}
|
(error instanceof Error
|
||||||
onClick={async (e) => {
|
? error.message
|
||||||
e.preventDefault();
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!submitAccount) return;
|
return (
|
||||||
try {
|
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
||||||
const account: SandboxBackend.Circuit.CircuitAccountRequest =
|
<div class="px-4 sm:px-0">
|
||||||
{
|
<h2 class="text-base font-semibold leading-7 text-gray-900">
|
||||||
cashout_address: submitAccount.cashout_address,
|
<i18n.Translate>New business account</i18n.Translate>
|
||||||
contact_data: submitAccount.contact_data,
|
</h2>
|
||||||
internal_iban: submitAccount.iban,
|
</div>
|
||||||
name: submitAccount.name,
|
<AccountForm
|
||||||
username: submitAccount.username,
|
template={undefined}
|
||||||
password: getRandomPassword(),
|
purpose="create"
|
||||||
};
|
onChange={(a) => {
|
||||||
|
setSubmitAccount(a);
|
||||||
await createAccount(account);
|
}}
|
||||||
onCreateSuccess(account.password);
|
>
|
||||||
} catch (error) {
|
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
|
||||||
if (error instanceof RequestError) {
|
{onCancel ?
|
||||||
notify(
|
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
onClick={onCancel}
|
||||||
onClientError: (status) =>
|
>
|
||||||
status === HttpStatusCode.Forbidden
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
? i18n.str`The rights to perform the operation are not sufficient`
|
</button>
|
||||||
: status === HttpStatusCode.BadRequest
|
: <div />
|
||||||
? i18n.str`Input data was invalid`
|
}
|
||||||
: status === HttpStatusCode.Conflict
|
<button type="submit"
|
||||||
? i18n.str`At least one registration detail was not available`
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
: undefined,
|
disabled={!submitAccount}
|
||||||
}),
|
onClick={(e) => {
|
||||||
);
|
e.preventDefault()
|
||||||
} else {
|
doCreate()
|
||||||
notifyError(
|
}}
|
||||||
i18n.str`Operation failed, please report`,
|
>
|
||||||
(error instanceof Error
|
<i18n.Translate>Create</i18n.Translate>
|
||||||
? error.message
|
</button>
|
||||||
: JSON.stringify(error)) as TranslatedString
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
|
</AccountForm>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -17,17 +17,20 @@ import { RemoveAccount } from "./RemoveAccount.js";
|
|||||||
interface Props {
|
interface Props {
|
||||||
onRegister: () => void;
|
onRegister: () => void;
|
||||||
}
|
}
|
||||||
export type AccountAction = "show-details" |
|
export type AccountAction = "show-details" |
|
||||||
"show-cashout" |
|
"show-cashout" |
|
||||||
"update-password" |
|
"update-password" |
|
||||||
"remove-account" |
|
"remove-account" |
|
||||||
"show-cashouts-details";
|
"show-cashouts-details";
|
||||||
|
|
||||||
export function AdminHome({ onRegister }: Props): VNode {
|
export function AdminHome({ onRegister }: Props): VNode {
|
||||||
const [action, setAction] = useState<{
|
const [action, setAction] = useState<{
|
||||||
type: AccountAction,
|
type: AccountAction,
|
||||||
account: string
|
account: string
|
||||||
}>()
|
} | undefined>({
|
||||||
|
type:"remove-account",
|
||||||
|
account:"gnunet-at-sandbox"
|
||||||
|
})
|
||||||
|
|
||||||
const [createAccount, setCreateAccount] = useState(false);
|
const [createAccount, setCreateAccount] = useState(false);
|
||||||
|
|
||||||
@ -78,7 +81,7 @@ export function AdminHome({ onRegister }: Props): VNode {
|
|||||||
notifyInfo(i18n.str`Password changed`);
|
notifyInfo(i18n.str`Password changed`);
|
||||||
setAction(undefined);
|
setAction(undefined);
|
||||||
}}
|
}}
|
||||||
onClear={() => {
|
onCancel={() => {
|
||||||
setAction(undefined);
|
setAction(undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -89,7 +92,7 @@ export function AdminHome({ onRegister }: Props): VNode {
|
|||||||
notifyInfo(i18n.str`Account removed`);
|
notifyInfo(i18n.str`Account removed`);
|
||||||
setAction(undefined);
|
setAction(undefined);
|
||||||
}}
|
}}
|
||||||
onClear={() => {
|
onCancel={() => {
|
||||||
setAction(undefined);
|
setAction(undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -116,7 +119,7 @@ export function AdminHome({ onRegister }: Props): VNode {
|
|||||||
if (createAccount) {
|
if (createAccount) {
|
||||||
return (
|
return (
|
||||||
<CreateNewAccount
|
<CreateNewAccount
|
||||||
onClose={() => setCreateAccount(false)}
|
onCancel={() => setCreateAccount(false)}
|
||||||
onCreateSuccess={(password) => {
|
onCreateSuccess={(password) => {
|
||||||
notifyInfo(
|
notifyInfo(
|
||||||
i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
|
i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
|
||||||
@ -129,34 +132,18 @@ export function AdminHome({ onRegister }: Props): VNode {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div>
|
|
||||||
<h1 class="nav welcome-text">
|
|
||||||
<i18n.Translate>Admin panel</i18n.Translate>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
<AccountList
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
onCreateAccount={() => {
|
||||||
<div></div>
|
setCreateAccount(true);
|
||||||
<div>
|
}}
|
||||||
<input
|
account={undefined}
|
||||||
class="pure-button pure-button-primary content"
|
onAction={(type, account) => setAction({ account, type })}
|
||||||
type="submit"
|
onRegister={onRegister}
|
||||||
value={i18n.str`Create account`}
|
/>
|
||||||
onClick={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setCreateAccount(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<AdminAccount onRegister={onRegister} />
|
<AdminAccount onRegister={onRegister} />
|
||||||
|
|
||||||
<AccountList account={undefined} onAction={(type,account) => setAction({account, type})} onRegister={onRegister}/>
|
|
||||||
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,112 +1,218 @@
|
|||||||
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { VNode,h,Fragment } from "preact";
|
import { VNode, h, Fragment } from "preact";
|
||||||
import { useAccountDetails } from "../../hooks/access.js";
|
import { useAccountDetails } from "../../hooks/access.js";
|
||||||
import { useAdminAccountAPI } from "../../hooks/circuit.js";
|
import { useAdminAccountAPI } from "../../hooks/circuit.js";
|
||||||
import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
import { buildRequestErrorMessage } from "../../utils.js";
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js";
|
||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
|
||||||
|
|
||||||
export function RemoveAccount({
|
export function RemoveAccount({
|
||||||
account,
|
account,
|
||||||
onClear,
|
onCancel,
|
||||||
onUpdateSuccess,
|
onUpdateSuccess,
|
||||||
onLoadNotOk,
|
onLoadNotOk,
|
||||||
}: {
|
focus,
|
||||||
onLoadNotOk: <T>(
|
}: {
|
||||||
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
onLoadNotOk: <T>(
|
||||||
) => VNode;
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||||
onClear: () => void;
|
) => VNode;
|
||||||
onUpdateSuccess: () => void;
|
focus?: boolean;
|
||||||
account: string;
|
onCancel: () => void;
|
||||||
}): VNode {
|
onUpdateSuccess: () => void;
|
||||||
const { i18n } = useTranslationContext();
|
account: string;
|
||||||
const result = useAccountDetails(account);
|
}): VNode {
|
||||||
const { deleteAccount } = useAdminAccountAPI();
|
const { i18n } = useTranslationContext();
|
||||||
|
const result = useAccountDetails(account);
|
||||||
if (!result.ok) {
|
const [accountName, setAccountName] = useState<string | undefined>()
|
||||||
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
const { deleteAccount } = useAdminAccountAPI();
|
||||||
return onLoadNotOk(result);
|
|
||||||
}
|
if (!result.ok) {
|
||||||
if (result.status === HttpStatusCode.NotFound) {
|
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||||
return <div>account not found</div>;
|
|
||||||
}
|
|
||||||
return onLoadNotOk(result);
|
return onLoadNotOk(result);
|
||||||
}
|
}
|
||||||
|
if (result.status === HttpStatusCode.NotFound) {
|
||||||
const balance = Amounts.parse(result.data.balance.amount);
|
return <div>account not found</div>;
|
||||||
if (!balance) {
|
|
||||||
return <div>there was an error reading the balance</div>;
|
|
||||||
}
|
}
|
||||||
const isBalanceEmpty = Amounts.isZero(balance);
|
return onLoadNotOk(result);
|
||||||
return (
|
}
|
||||||
<div>
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
<div>
|
useEffect(() => {
|
||||||
<h1 class="nav welcome-text">
|
if (focus) ref.current?.focus();
|
||||||
<i18n.Translate>Remove account: {account}</i18n.Translate>
|
}, [focus]);
|
||||||
</h1>
|
|
||||||
</div>
|
const balance = Amounts.parse(result.data.balance.amount);
|
||||||
{/* {FXME: SHOW WARNING} */}
|
if (!balance) {
|
||||||
{/* {!isBalanceEmpty && (
|
return <div>there was an error reading the balance</div>;
|
||||||
<ErrorBannerFloat
|
}
|
||||||
error={{
|
const isBalanceEmpty = Amounts.isZero(balance);
|
||||||
title: i18n.str`Can't delete the account`,
|
if (!isBalanceEmpty) {
|
||||||
description: i18n.str`Balance is not empty`,
|
return <div>
|
||||||
}}
|
<div class="rounded-md bg-yellow-50 p-4">
|
||||||
onClear={() => saveError(undefined)}
|
<div class="flex">
|
||||||
/>
|
<div class="flex-shrink-0">
|
||||||
)} */}
|
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||||
<p>
|
</svg>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
</div>
|
||||||
<div>
|
<div class="ml-3">
|
||||||
<input
|
<h3 class="text-sm font-medium text-yellow-800">
|
||||||
class="pure-button"
|
<i18n.Translate>Can't delete the account</i18n.Translate>
|
||||||
type="submit"
|
</h3>
|
||||||
value={i18n.str`Cancel`}
|
<div class="mt-2 text-sm text-yellow-700">
|
||||||
onClick={async (e) => {
|
<p>
|
||||||
e.preventDefault();
|
<i18n.Translate>The account can be delete while still holding some balance. First make sure that the owner make a complete cashout.</i18n.Translate>
|
||||||
onClear();
|
</p>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
id="select-exchange"
|
|
||||||
class="pure-button pure-button-primary content"
|
|
||||||
disabled={!isBalanceEmpty}
|
|
||||||
type="submit"
|
|
||||||
value={i18n.str`Confirm`}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
const r = await deleteAccount(account);
|
|
||||||
onUpdateSuccess();
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof RequestError) {
|
|
||||||
notify(
|
|
||||||
buildRequestErrorMessage(i18n, error.cause, {
|
|
||||||
onClientError: (status) =>
|
|
||||||
status === HttpStatusCode.Forbidden
|
|
||||||
? i18n.str`The administrator specified a institutional username`
|
|
||||||
: status === HttpStatusCode.NotFound
|
|
||||||
? i18n.str`The username was not found`
|
|
||||||
: status === HttpStatusCode.PreconditionFailed
|
|
||||||
? i18n.str`Balance was not zero`
|
|
||||||
: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
notifyError(i18n.str`Operation failed, please report`,
|
|
||||||
(error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: JSON.stringify(error)) as TranslatedString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div class="mt-2 flex justify-end">
|
||||||
|
<button type="button" class="rounded-md ring-1 ring-gray-400 bg-white px-3 py-2 text-sm font-semibold shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
|
||||||
|
onClick={() => {
|
||||||
|
onCancel()
|
||||||
|
}}>
|
||||||
|
<i18n.Translate>Go back</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function doRemove() {
|
||||||
|
try {
|
||||||
|
const r = await deleteAccount(account);
|
||||||
|
onUpdateSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`The administrator specified a institutional username`
|
||||||
|
: status === HttpStatusCode.NotFound
|
||||||
|
? i18n.str`The username was not found`
|
||||||
|
: status === HttpStatusCode.PreconditionFailed
|
||||||
|
? i18n.str`Balance was not zero`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = undefinedIfEmpty({
|
||||||
|
accountName: !accountName
|
||||||
|
? i18n.str`required`
|
||||||
|
: account !== accountName
|
||||||
|
? i18n.str`name doesn't match`
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div class="rounded-md bg-yellow-50 p-4">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-bold text-yellow-800">
|
||||||
|
<i18n.Translate>You are going to remove the account</i18n.Translate>
|
||||||
|
</h3>
|
||||||
|
<div class="mt-2 text-sm text-yellow-700">
|
||||||
|
<p>
|
||||||
|
<i18n.Translate>This step can't be undone.</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
|
||||||
|
<div class="px-4 sm:px-0">
|
||||||
|
<h2 class="text-base font-semibold leading-7 text-gray-900">
|
||||||
|
<i18n.Translate>Deleting account "{account}"</i18n.Translate>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect="off"
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="px-4 py-6 sm:p-8">
|
||||||
|
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
|
||||||
|
|
||||||
|
<div class="sm:col-span-5">
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium leading-6 text-gray-900"
|
||||||
|
for="password"
|
||||||
|
>
|
||||||
|
{i18n.str`Verification`}
|
||||||
|
</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
type="text"
|
||||||
|
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
data-error={!!errors?.accountName && accountName !== undefined}
|
||||||
|
value={accountName ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAccountName(e.currentTarget.value)
|
||||||
|
}}
|
||||||
|
placeholder={account}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.accountName}
|
||||||
|
isDirty={accountName !== undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500" >
|
||||||
|
<i18n.Translate>enter the account name that is going to be deleted</i18n.Translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
|
||||||
|
{onCancel ?
|
||||||
|
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
: <div />
|
||||||
|
}
|
||||||
|
<button type="submit"
|
||||||
|
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||||
|
disabled={!!errors}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
doRemove()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Delete</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -109,7 +109,7 @@ export function BusinessAccount({
|
|||||||
notifyInfo(i18n.str`Password changed`);
|
notifyInfo(i18n.str`Password changed`);
|
||||||
setUpdatePassword(false);
|
setUpdatePassword(false);
|
||||||
}}
|
}}
|
||||||
onClear={() => {
|
onCancel={() => {
|
||||||
setUpdatePassword(false);
|
setUpdatePassword(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user