diff options
Diffstat (limited to 'packages/demobank-ui/src/pages/AdminPage.tsx')
-rw-r--r-- | packages/demobank-ui/src/pages/AdminPage.tsx | 1042 |
1 files changed, 0 insertions, 1042 deletions
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx deleted file mode 100644 index 18462bdc3..000000000 --- a/packages/demobank-ui/src/pages/AdminPage.tsx +++ /dev/null @@ -1,1042 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { Amounts, HttpStatusCode, TranslatedString, parsePaytoUri } from "@gnu-taler/taler-util"; -import { - ErrorType, - HttpResponsePaginated, - RequestError, - notify, - notifyError, - notifyInfo, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { Cashouts } from "../components/Cashouts/index.js"; -import { useBackendContext } from "../context/backend.js"; -import { useAccountDetails } from "../hooks/access.js"; -import { - useAdminAccountAPI, - useBusinessAccountDetails, - useBusinessAccounts, -} from "../hooks/circuit.js"; -import { - buildRequestErrorMessage, - PartialButDefined, - RecursivePartial, - undefinedIfEmpty, - validateIBAN, - WithIntermediate, -} from "../utils.js"; -import { ShowCashoutDetails } from "./BusinessAccount.js"; -import { handleNotOkResult } from "./HomePage.js"; -import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; -import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; - -const charset = - "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const upperIdx = charset.indexOf("A"); - -function randomPassword(): string { - const random = Array.from({ length: 16 }).map(() => { - return charset.charCodeAt(Math.random() * charset.length); - }); - // first char can't be upper - const charIdx = charset.indexOf(String.fromCharCode(random[0])); - random[0] = - charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0]; - return String.fromCharCode(...random); -} - -interface Props { - onRegister: () => void; -} -/** - * Query account information and show QR code if there is pending withdrawal - */ -export function AdminPage({ onRegister }: Props): VNode { - const [account, setAccount] = useState<string | undefined>(); - const [showDetails, setShowDetails] = useState<string | undefined>(); - const [showCashouts, setShowCashouts] = useState<string | undefined>(); - const [updatePassword, setUpdatePassword] = useState<string | undefined>(); - const [removeAccount, setRemoveAccount] = useState<string | undefined>(); - const [showCashoutDetails, setShowCashoutDetails] = useState< - string | undefined - >(); - - const [createAccount, setCreateAccount] = useState(false); - - const result = useBusinessAccounts({ account }); - const { i18n } = useTranslationContext(); - - if (result.loading) return <div />; - if (!result.ok) { - return handleNotOkResult(i18n, onRegister)(result); - } - - const { customers } = result.data; - - if (showCashoutDetails) { - return ( - <ShowCashoutDetails - id={showCashoutDetails} - onLoadNotOk={handleNotOkResult(i18n, onRegister)} - onCancel={() => { - setShowCashoutDetails(undefined); - }} - /> - ); - } - - if (showCashouts) { - return ( - <div> - <div> - <h1 class="nav welcome-text"> - <i18n.Translate>Cashout for account {showCashouts}</i18n.Translate> - </h1> - </div> - <Cashouts - account={showCashouts} - onSelected={(id) => { - setShowCashouts(id); - setShowCashouts(undefined); - }} - /> - <p> - <input - class="pure-button" - type="submit" - value={i18n.str`Close`} - onClick={async (e) => { - e.preventDefault(); - setShowCashouts(undefined); - }} - /> - </p> - </div> - ); - } - - if (showDetails) { - return ( - <ShowAccountDetails - account={showDetails} - onLoadNotOk={handleNotOkResult(i18n, onRegister)} - onChangePassword={() => { - setUpdatePassword(showDetails); - setShowDetails(undefined); - }} - onUpdateSuccess={() => { - notifyInfo(i18n.str`Account updated`); - setShowDetails(undefined); - }} - onClear={() => { - setShowDetails(undefined); - }} - /> - ); - } - if (removeAccount) { - return ( - <RemoveAccount - account={removeAccount} - onLoadNotOk={handleNotOkResult(i18n, onRegister)} - onUpdateSuccess={() => { - notifyInfo(i18n.str`Account removed`); - setRemoveAccount(undefined); - }} - onClear={() => { - setRemoveAccount(undefined); - }} - /> - ); - } - if (updatePassword) { - return ( - <UpdateAccountPassword - account={updatePassword} - onLoadNotOk={handleNotOkResult(i18n, onRegister)} - onUpdateSuccess={() => { - notifyInfo(i18n.str`Password changed`); - setUpdatePassword(undefined); - }} - onClear={() => { - setUpdatePassword(undefined); - }} - /> - ); - } - if (createAccount) { - return ( - <CreateNewAccount - onClose={() => setCreateAccount(false)} - onCreateSuccess={(password) => { - notifyInfo( - i18n.str`Account created with password "${password}". The user must change the password on the next login.`, - ); - setCreateAccount(false); - }} - /> - ); - } - - return ( - <Fragment> - <div> - <h1 class="nav welcome-text"> - <i18n.Translate>Admin panel</i18n.Translate> - </h1> - </div> - - <p> - <div style={{ display: "flex", justifyContent: "space-between" }}> - <div></div> - <div> - <input - class="pure-button pure-button-primary content" - type="submit" - value={i18n.str`Create account`} - onClick={async (e) => { - e.preventDefault(); - - setCreateAccount(true); - }} - /> - </div> - </div> - </p> - - <AdminAccount onRegister={onRegister} /> - <section - id="main" - style={{ width: 600, marginLeft: "auto", marginRight: "auto" }} - > - {!customers.length ? ( - <div></div> - ) : ( - <article> - <h2>{i18n.str`Accounts:`}</h2> - <div class="results"> - <table class="pure-table pure-table-striped"> - <thead> - <tr> - <th>{i18n.str`Username`}</th> - <th>{i18n.str`Name`}</th> - <th>{i18n.str`Balance`}</th> - <th>{i18n.str`Actions`}</th> - </tr> - </thead> - <tbody> - {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> - <a - href="#" - onClick={(e) => { - e.preventDefault(); - setShowDetails(item.username); - }} - > - {item.username} - </a> - </td> - <td>{item.name}</td> - <td> - {!balance ? ( - i18n.str`unknown` - ) : ( - <span class="amount"> - {balanceIsDebit ? <b>-</b> : null} - <span class="value">{`${Amounts.stringifyValue( - balance, - )}`}</span> - - <span class="currency">{`${balance.currency}`}</span> - </span> - )} - </td> - <td> - <a - href="#" - onClick={(e) => { - e.preventDefault(); - setUpdatePassword(item.username); - }} - > - change password - </a> - - <a - href="#" - onClick={(e) => { - e.preventDefault(); - setShowCashouts(item.username); - }} - > - cashouts - </a> - - <a - href="#" - onClick={(e) => { - e.preventDefault(); - setRemoveAccount(item.username); - }} - > - remove - </a> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - </article> - )} - </section> - </Fragment> - ); -} - -function AdminAccount({ onRegister }: { onRegister: () => void }): VNode { - const { i18n } = useTranslationContext(); - const r = useBackendContext(); - const account = r.state.status === "loggedIn" ? r.state.username : "admin"; - const result = useAccountDetails(account); - - if (!result.ok) { - 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 IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; -const EMAIL_REGEX = - /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/; - -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; -} - -export function UpdateAccountPassword({ - account, - onClear, - onUpdateSuccess, - onLoadNotOk, -}: { - onLoadNotOk: <T>( - error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, - ) => VNode; - onClear: () => void; - onUpdateSuccess: () => void; - account: string; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - const { changePassword } = useAdminAccountAPI(); - const [password, setPassword] = useState<string | undefined>(); - const [repeat, setRepeat] = useState<string | undefined>(); - - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return onLoadNotOk(result); - } - if (result.status === HttpStatusCode.NotFound) { - return <div>account not found</div>; - } - return onLoadNotOk(result); - } - - const errors = undefinedIfEmpty({ - password: !password ? i18n.str`required` : undefined, - repeat: !repeat - ? i18n.str`required` - : password !== repeat - ? i18n.str`password doesn't match` - : undefined, - }); - - return ( - <div> - <div> - <h1 class="nav welcome-text"> - <i18n.Translate>Update password for {account}</i18n.Translate> - </h1> - </div> - - <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> - <form class="pure-form"> - <fieldset> - <label>{i18n.str`Password`}</label> - <input - type="password" - value={password ?? ""} - onChange={(e) => { - setPassword(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.password} - isDirty={password !== undefined} - /> - </fieldset> - <fieldset> - <label>{i18n.str`Repeat password`}</label> - <input - type="password" - value={repeat ?? ""} - 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> - ); -} - -function CreateNewAccount({ - onClose, - onCreateSuccess, -}: { - onClose: () => void; - onCreateSuccess: (password: string) => void; -}): VNode { - const { i18n } = useTranslationContext(); - const { createAccount } = useAdminAccountAPI(); - const [submitAccount, setSubmitAccount] = useState< - 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" }}> - <AccountForm - template={undefined} - purpose="create" - onChange={(a) => { - setSubmitAccount(a); - }} - /> - - <p> - <div style={{ display: "flex", justifyContent: "space-between" }}> - <div> - <input - class="pure-button" - type="submit" - value={i18n.str`Close`} - onClick={async (e) => { - e.preventDefault(); - onClose(); - }} - /> - </div> - <div> - <input - id="select-exchange" - class="pure-button pure-button-primary content" - disabled={!submitAccount} - type="submit" - value={i18n.str`Confirm`} - onClick={async (e) => { - e.preventDefault(); - - if (!submitAccount) return; - try { - const account: SandboxBackend.Circuit.CircuitAccountRequest = - { - cashout_address: submitAccount.cashout_address, - contact_data: submitAccount.contact_data, - internal_iban: submitAccount.iban, - name: submitAccount.name, - username: submitAccount.username, - password: randomPassword(), - }; - - await createAccount(account); - onCreateSuccess(account.password); - } catch (error) { - if (error instanceof RequestError) { - notify( - buildRequestErrorMessage(i18n, error.cause, { - onClientError: (status) => - status === HttpStatusCode.Forbidden - ? i18n.str`The rights to perform the operation are not sufficient` - : status === HttpStatusCode.BadRequest - ? i18n.str`Input data was invalid` - : status === HttpStatusCode.Conflict - ? i18n.str`At least one registration detail was not available` - : undefined, - }), - ); - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } - }} - /> - </div> - </div> - </p> - </div> - </div> - ); -} - -export function ShowAccountDetails({ - account, - onClear, - onUpdateSuccess, - onLoadNotOk, - onChangePassword, -}: { - onLoadNotOk: <T>( - error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, - ) => VNode; - onClear?: () => void; - onChangePassword: () => void; - onUpdateSuccess: () => void; - account: string; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useBusinessAccountDetails(account); - const { updateAccount } = useAdminAccountAPI(); - const [update, setUpdate] = useState(false); - const [submitAccount, setSubmitAccount] = useState< - SandboxBackend.Circuit.CircuitAccountData | undefined - >(); - - if (!result.ok) { - 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 ( - <div> - <div> - <h1 class="nav welcome-text"> - <i18n.Translate>Business account details</i18n.Translate> - </h1> - </div> - <div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}> - <AccountForm - template={result.data} - purpose={update ? "update" : "show"} - onChange={(a) => setSubmitAccount(a)} - /> - - <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(); - - if (!update) { - setUpdate(true); - } else { - if (!submitAccount) return; - try { - 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> - ); -} - -function RemoveAccount({ - account, - onClear, - onUpdateSuccess, - onLoadNotOk, -}: { - onLoadNotOk: <T>( - error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, - ) => VNode; - onClear: () => void; - onUpdateSuccess: () => void; - account: string; -}): VNode { - const { i18n } = useTranslationContext(); - const result = useAccountDetails(account); - const { deleteAccount } = useAdminAccountAPI(); - - if (!result.ok) { - if (result.loading || result.type === ErrorType.TIMEOUT) { - return onLoadNotOk(result); - } - if (result.status === HttpStatusCode.NotFound) { - return <div>account not found</div>; - } - return onLoadNotOk(result); - } - - const balance = Amounts.parse(result.data.balance.amount); - if (!balance) { - return <div>there was an error reading the balance</div>; - } - const isBalanceEmpty = Amounts.isZero(balance); - return ( - <div> - <div> - <h1 class="nav welcome-text"> - <i18n.Translate>Remove account: {account}</i18n.Translate> - </h1> - </div> - {/* {FXME: SHOW WARNING} */} - {/* {!isBalanceEmpty && ( - <ErrorBannerFloat - error={{ - title: i18n.str`Can't delete the account`, - description: i18n.str`Balance is not empty`, - }} - onClear={() => saveError(undefined)} - /> - )} */} - - <p> - <div style={{ display: "flex", justifyContent: "space-between" }}> - <div> - <input - class="pure-button" - type="submit" - value={i18n.str`Cancel`} - onClick={async (e) => { - e.preventDefault(); - onClear(); - }} - /> - </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> - </p> - </div> - ); -} -/** - * Create valid account object to update or create - * Take template as initial values for the form - * Purpose indicate if all field al read only (show), part of them (update) - * or none (create) - * @param param0 - * @returns - */ -function AccountForm({ - template, - purpose, - onChange, -}: { - template: SandboxBackend.Circuit.CircuitAccountData | undefined; - onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void; - purpose: "create" | "update" | "show"; -}): VNode { - const initial = initializeFromTemplate(template); - const [form, setForm] = useState(initial); - const [errors, setErrors] = useState< - RecursivePartial<typeof initial> | undefined - >(undefined); - const { i18n } = useTranslationContext(); - - function updateForm(newForm: typeof initial): void { - const parsed = !newForm.cashout_address - ? undefined - : parsePaytoUri(newForm.cashout_address); - - 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` - : !EMAIL_REGEX.test(newForm.contact_data.email) - ? i18n.str`it should be an email` - : undefined, - phone: !newForm.contact_data?.phone - ? i18n.str`required` - : !newForm.contact_data.phone.startsWith("+") - ? i18n.str`should start with +` - : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) - ? i18n.str`phone number can't have other than numbers` - : undefined, - }), - iban: !newForm.iban - ? undefined //optional field - : !IBAN_REGEX.test(newForm.iban) - ? i18n.str`IBAN should have just uppercased letters and numbers` - : 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> - ); -} |