mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 18:40:34 -05:00
268 lines
9.2 KiB
TypeScript
268 lines
9.2 KiB
TypeScript
// @ts-strict-ignore
|
|
import { type FormEvent, useState } from 'react';
|
|
import { Form } from 'react-aria-components';
|
|
import { useTranslation, Trans } from 'react-i18next';
|
|
|
|
import { Button } from '@actual-app/components/button';
|
|
import { FormError } from '@actual-app/components/form-error';
|
|
import { InitialFocus } from '@actual-app/components/initial-focus';
|
|
import { InlineField } from '@actual-app/components/inline-field';
|
|
import { Input } from '@actual-app/components/input';
|
|
import { Select } from '@actual-app/components/select';
|
|
import { Text } from '@actual-app/components/text';
|
|
import { theme } from '@actual-app/components/theme';
|
|
import { View } from '@actual-app/components/view';
|
|
|
|
import { toRelaxedNumber } from 'loot-core/shared/util';
|
|
|
|
import { createAccount } from '@desktop-client/accounts/accountsSlice';
|
|
import { Link } from '@desktop-client/components/common/Link';
|
|
import {
|
|
Modal,
|
|
ModalButtons,
|
|
ModalCloseButton,
|
|
ModalHeader,
|
|
ModalTitle,
|
|
} from '@desktop-client/components/common/Modal';
|
|
import { Checkbox } from '@desktop-client/components/forms';
|
|
import { validateAccountName } from '@desktop-client/components/util/accountValidation';
|
|
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
|
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
|
import { closeModal } from '@desktop-client/modals/modalsSlice';
|
|
import { useDispatch } from '@desktop-client/redux';
|
|
|
|
export function CreateLocalAccountModal() {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const dispatch = useDispatch();
|
|
const accounts = useAccounts();
|
|
const [name, setName] = useState('');
|
|
const [offbudget, setOffbudget] = useState(false);
|
|
const [balance, setBalance] = useState('0');
|
|
const [accountType, setAccountType] = useState<
|
|
'checking' | 'savings' | 'credit' | 'investment' | 'mortgage' | 'loan'
|
|
>('checking');
|
|
const [interestRate, setInterestRate] = useState('');
|
|
|
|
const [nameError, setNameError] = useState(null);
|
|
const [balanceError, setBalanceError] = useState(false);
|
|
const [interestRateError, setInterestRateError] = useState(false);
|
|
|
|
const validateBalance = balance => !isNaN(parseFloat(balance));
|
|
|
|
const validateInterestRate = (rate: string) => {
|
|
if (!rate) return true; // Interest rate is optional
|
|
const num = parseFloat(rate);
|
|
return !isNaN(num) && num >= 0 && num <= 100;
|
|
};
|
|
|
|
const validateAndSetName = (name: string) => {
|
|
const nameError = validateAccountName(name, '', accounts);
|
|
if (nameError) {
|
|
setNameError(nameError);
|
|
} else {
|
|
setName(name);
|
|
setNameError(null);
|
|
}
|
|
};
|
|
|
|
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
|
|
const nameError = validateAccountName(name, '', accounts);
|
|
|
|
const balanceError = !validateBalance(balance);
|
|
setBalanceError(balanceError);
|
|
|
|
const interestRateError = !validateInterestRate(interestRate);
|
|
setInterestRateError(interestRateError);
|
|
|
|
if (!nameError && !balanceError && !interestRateError) {
|
|
dispatch(closeModal());
|
|
const id = await dispatch(
|
|
createAccount({
|
|
name,
|
|
balance: toRelaxedNumber(balance),
|
|
offBudget: offbudget,
|
|
accountType,
|
|
interestRate: interestRate ? toRelaxedNumber(interestRate) : null,
|
|
}),
|
|
).unwrap();
|
|
navigate('/accounts/' + id);
|
|
}
|
|
};
|
|
return (
|
|
<Modal name="add-local-account">
|
|
{({ state: { close } }) => (
|
|
<>
|
|
<ModalHeader
|
|
title={
|
|
<ModalTitle title={t('Create Local Account')} shrinkOnOverflow />
|
|
}
|
|
rightContent={<ModalCloseButton onPress={close} />}
|
|
/>
|
|
<View>
|
|
<Form onSubmit={onSubmit}>
|
|
<InlineField label={t('Name')} width="100%">
|
|
<InitialFocus>
|
|
<Input
|
|
name="name"
|
|
value={name}
|
|
onChangeValue={setName}
|
|
onUpdate={value => {
|
|
const name = value.trim();
|
|
validateAndSetName(name);
|
|
}}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
</InitialFocus>
|
|
</InlineField>
|
|
{nameError && (
|
|
<FormError style={{ marginLeft: 75, color: theme.warningText }}>
|
|
{nameError}
|
|
</FormError>
|
|
)}
|
|
|
|
<View
|
|
style={{
|
|
width: '100%',
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-end',
|
|
}}
|
|
>
|
|
<View style={{ flexDirection: 'column' }}>
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-end',
|
|
}}
|
|
>
|
|
<Checkbox
|
|
id="offbudget"
|
|
name="offbudget"
|
|
checked={offbudget}
|
|
onChange={() => setOffbudget(!offbudget)}
|
|
/>
|
|
<label
|
|
htmlFor="offbudget"
|
|
style={{
|
|
userSelect: 'none',
|
|
verticalAlign: 'center',
|
|
}}
|
|
>
|
|
<Trans>Off budget</Trans>
|
|
</label>
|
|
</View>
|
|
<div
|
|
style={{
|
|
textAlign: 'right',
|
|
fontSize: '0.7em',
|
|
color: theme.pageTextLight,
|
|
marginTop: 3,
|
|
}}
|
|
>
|
|
<Text>
|
|
<Trans>
|
|
This cannot be changed later. See{' '}
|
|
<Link
|
|
variant="external"
|
|
linkColor="muted"
|
|
to="https://actualbudget.org/docs/accounts/#off-budget-accounts"
|
|
>
|
|
Accounts Overview
|
|
</Link>{' '}
|
|
for more information.
|
|
</Trans>
|
|
</Text>
|
|
</div>
|
|
</View>
|
|
</View>
|
|
|
|
<InlineField label={t('Balance')} width="100%">
|
|
<Input
|
|
name="balance"
|
|
inputMode="decimal"
|
|
value={balance}
|
|
onChangeValue={setBalance}
|
|
onUpdate={value => {
|
|
const balance = value.trim();
|
|
setBalance(balance);
|
|
if (validateBalance(balance) && balanceError) {
|
|
setBalanceError(false);
|
|
}
|
|
}}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
</InlineField>
|
|
{balanceError && (
|
|
<FormError style={{ marginLeft: 75 }}>
|
|
<Trans>Balance must be a number</Trans>
|
|
</FormError>
|
|
)}
|
|
|
|
<InlineField label={t('Account Type')} width="100%">
|
|
<Select
|
|
value={accountType}
|
|
onChange={value =>
|
|
setAccountType(value as typeof accountType)
|
|
}
|
|
options={[
|
|
['checking', t('Checking')],
|
|
['savings', t('Savings')],
|
|
['credit', t('Credit Card')],
|
|
['investment', t('Investment')],
|
|
['mortgage', t('Mortgage')],
|
|
['loan', t('Loan')],
|
|
]}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
</InlineField>
|
|
|
|
{(accountType === 'mortgage' || accountType === 'loan') && (
|
|
<InlineField label={t('Interest Rate (%)')} width="100%">
|
|
<Input
|
|
name="interestRate"
|
|
inputMode="decimal"
|
|
value={interestRate}
|
|
onChangeValue={setInterestRate}
|
|
onUpdate={value => {
|
|
const rate = value.trim();
|
|
setInterestRate(rate);
|
|
if (validateInterestRate(rate) && interestRateError) {
|
|
setInterestRateError(false);
|
|
}
|
|
}}
|
|
style={{ flex: 1 }}
|
|
placeholder="0.00"
|
|
/>
|
|
</InlineField>
|
|
)}
|
|
|
|
{interestRateError && (
|
|
<FormError style={{ marginLeft: 75 }}>
|
|
<Trans>
|
|
Interest rate must be a number between 0 and 100
|
|
</Trans>
|
|
</FormError>
|
|
)}
|
|
|
|
<ModalButtons>
|
|
<Button onPress={close}>
|
|
<Trans>Back</Trans>
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
style={{ marginLeft: 10 }}
|
|
>
|
|
<Trans>Create</Trans>
|
|
</Button>
|
|
</ModalButtons>
|
|
</Form>
|
|
</View>
|
|
</>
|
|
)}
|
|
</Modal>
|
|
);
|
|
}
|