feat(frontend): add expense target, form validation, request notification
This commit is contained in:
parent
f8d766beb0
commit
af72e8dc92
@ -8,11 +8,6 @@ export { useEditExpenseItem } from './hooks/useEditExpenseItem';
|
|||||||
|
|
||||||
export type { ExpenseListItem } from './types';
|
export type { ExpenseListItem } from './types';
|
||||||
|
|
||||||
export type { AddExpenseItemFormProps, AddExpenseItemFormValues } from './ui/AddExpenseItemForm/AddExpenseItemForm.ui';
|
export * from './ui/AddExpenseItemForm';
|
||||||
export { default as AddExpenseItemForm } from './ui/AddExpenseItemForm/AddExpenseItemForm.ui';
|
export * from './ui/DeleteExpenseItemForm';
|
||||||
|
export * from './ui/EditExpenseItemForm';
|
||||||
export type { DeleteExpenseItemFormProps } from './ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui';
|
|
||||||
export { default as DeleteExpenseItemForm } from './ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui';
|
|
||||||
|
|
||||||
export type { EditExpenseItemFormProps, EditExpenseItemFormValues } from './ui/EditExpenseItemForm/EditExpenseItemForm.ui';
|
|
||||||
export { default as EditExpenseItemForm } from './ui/EditExpenseItemForm/EditExpenseItemForm.ui';
|
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
export const AddExpenseItemFormSchema = z.object({
|
||||||
|
amount: z.number(),
|
||||||
|
'bank-card': z.string().optional(),
|
||||||
|
cash: z.string().optional(),
|
||||||
|
count: z.number(),
|
||||||
|
date: z.date(),
|
||||||
|
description: z.string().trim().optional(),
|
||||||
|
paymentType: z.enum(['BANK_CARD', 'CASH', 'none']),
|
||||||
|
title: z.string().trim().min(5),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AddExpenseItemFormValues = z.infer<typeof AddExpenseItemFormSchema>;
|
||||||
@ -55,10 +55,9 @@ describe('Test AddExpenseItemForm', () => {
|
|||||||
expect(onSubmit).toHaveBeenCalledWith({
|
expect(onSubmit).toHaveBeenCalledWith({
|
||||||
amount: 12,
|
amount: 12,
|
||||||
count: 1,
|
count: 1,
|
||||||
currency: 'RUB',
|
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||||
date: '2025-08-29T00:00:00.000Z',
|
|
||||||
description: 'Описание элемента',
|
description: 'Описание элемента',
|
||||||
payment: null,
|
paymentType: 'none',
|
||||||
title: 'Название элемента',
|
title: 'Название элемента',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -80,14 +79,11 @@ describe('Test AddExpenseItemForm', () => {
|
|||||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||||
expect(onSubmit).toHaveBeenCalledWith({
|
expect(onSubmit).toHaveBeenCalledWith({
|
||||||
amount: 12,
|
amount: 12,
|
||||||
|
'bank-card': bankCardList[1].id,
|
||||||
count: 1,
|
count: 1,
|
||||||
currency: 'RUB',
|
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||||
date: '2025-08-29T00:00:00.000Z',
|
|
||||||
description: 'Описание элемента',
|
description: 'Описание элемента',
|
||||||
payment: {
|
paymentType: 'BANK_CARD',
|
||||||
id: bankCardList[1].id,
|
|
||||||
type: 'BANK_CARD',
|
|
||||||
},
|
|
||||||
title: 'Название элемента',
|
title: 'Название элемента',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -106,14 +102,11 @@ describe('Test AddExpenseItemForm', () => {
|
|||||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||||
expect(onSubmit).toHaveBeenCalledWith({
|
expect(onSubmit).toHaveBeenCalledWith({
|
||||||
amount: 12,
|
amount: 12,
|
||||||
|
cash: cashList[1].id,
|
||||||
count: 1,
|
count: 1,
|
||||||
currency: 'RUB',
|
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||||
date: '2025-08-29T00:00:00.000Z',
|
|
||||||
description: 'Описание элемента',
|
description: 'Описание элемента',
|
||||||
payment: {
|
paymentType: 'CASH',
|
||||||
id: cashList[1].id,
|
|
||||||
type: 'CASH',
|
|
||||||
},
|
|
||||||
title: 'Название элемента',
|
title: 'Название элемента',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,54 +1,27 @@
|
|||||||
import type { SelectChangeEvent } from '@mui/material';
|
import type { FC, FormHTMLAttributes, RefObject } from 'react';
|
||||||
import type {
|
|
||||||
FC,
|
|
||||||
FormEventHandler,
|
|
||||||
FormHTMLAttributes,
|
|
||||||
RefObject,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import {
|
import { Button, Typography } from '@mui/material';
|
||||||
Button,
|
import { FormProvider } from 'react-hook-form';
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { DatePicker } from '@mui/x-date-pickers';
|
|
||||||
import {
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import type { ExpenseListItem } from '@/entity/expenses/item';
|
|
||||||
|
|
||||||
import { useBankCards } from '@/entity/cards';
|
|
||||||
import { useCash } from '@/entity/cash';
|
|
||||||
import { DataTestId } from '@/shared/constants';
|
import { DataTestId } from '@/shared/constants';
|
||||||
import { formatRub, getUTCDate } from '@/shared/utils';
|
|
||||||
|
import type { UseAddExpenseItemFormArgs } from './hooks/useAddExpenseItemForm';
|
||||||
|
|
||||||
import classes from './AddExpenseItemForm.module.css';
|
import classes from './AddExpenseItemForm.module.css';
|
||||||
|
import AmountField from './fields/AmountField';
|
||||||
|
import BankCardField from './fields/BankCardField';
|
||||||
|
import CashField from './fields/CashField';
|
||||||
|
import CountField from './fields/CountField';
|
||||||
|
import DateField from './fields/DateField';
|
||||||
|
import DescriptionField from './fields/DescriptionField';
|
||||||
|
import PaymentTypeField from './fields/PaymentTypeField';
|
||||||
|
import TitleField from './fields/TitleField';
|
||||||
|
import { useAddExpenseItemForm } from './hooks/useAddExpenseItemForm';
|
||||||
|
|
||||||
export type AddExpenseItemFormProps = BaseFormProps & {
|
export type AddExpenseItemFormProps = BaseFormProps & UseAddExpenseItemFormArgs & {
|
||||||
disabled?: boolean;
|
|
||||||
onSubmit?: (data: AddExpenseItemFormValues) => Promise<void> | void;
|
|
||||||
ref?: RefObject<HTMLFormElement>;
|
ref?: RefObject<HTMLFormElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddExpenseItemFormValues = {
|
|
||||||
amount: number;
|
|
||||||
count: number;
|
|
||||||
currency: string;
|
|
||||||
date: string;
|
|
||||||
description: string;
|
|
||||||
payment: null | {
|
|
||||||
id: string;
|
|
||||||
type: 'BANK_CARD' | 'CASH';
|
|
||||||
};
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||||
|
|
||||||
const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
||||||
@ -59,214 +32,55 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
|||||||
...formProps
|
...formProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [paymentType, setPaymentType] = useState<NonNullable<ExpenseListItem['payment']>['type']>(null);
|
const {
|
||||||
|
formState,
|
||||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
handleSubmit,
|
||||||
event.preventDefault();
|
methods,
|
||||||
const formData = new FormData(event.currentTarget);
|
onFormSubmitHandler,
|
||||||
|
watch,
|
||||||
const title = formData.get('title');
|
} = useAddExpenseItemForm({ disabled, onSubmit });
|
||||||
const date = formData.get('date');
|
|
||||||
const amount = formData.get('amount');
|
|
||||||
const description = formData.get('description');
|
|
||||||
const count = formData.get('count');
|
|
||||||
const paymentMethod = formData.get('paymentMethod');
|
|
||||||
const bankCardId = formData.get('bank-card');
|
|
||||||
const cashId = formData.get('cash');
|
|
||||||
|
|
||||||
const getPayment = () => {
|
|
||||||
switch (paymentMethod) {
|
|
||||||
case 'BANK_CARD':
|
|
||||||
return {
|
|
||||||
id: bankCardId,
|
|
||||||
type: 'BANK_CARD',
|
|
||||||
};
|
|
||||||
case 'CASH':
|
|
||||||
return {
|
|
||||||
id: cashId,
|
|
||||||
type: 'CASH',
|
|
||||||
};
|
|
||||||
case 'none':
|
|
||||||
case null:
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') {
|
|
||||||
onSubmit?.({
|
|
||||||
amount: Number(amount),
|
|
||||||
count: Number(count),
|
|
||||||
currency: 'RUB',
|
|
||||||
date: getUTCDate(date),
|
|
||||||
description,
|
|
||||||
// @ts-ignore todo
|
|
||||||
payment: getPayment(),
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const { data: bankCards } = useBankCards();
|
|
||||||
const { data: cash } = useCash();
|
|
||||||
|
|
||||||
const bankCardOptions = bankCards?.map((card) => (
|
|
||||||
<MenuItem key={card.id} value={card.id}>
|
|
||||||
{`${card.name} (${formatRub(card.balance)})`}
|
|
||||||
</MenuItem>
|
|
||||||
));
|
|
||||||
|
|
||||||
const cashOptions = cash?.map((cashItem) => (
|
|
||||||
<MenuItem key={cashItem.id} value={cashItem.id}>
|
|
||||||
{`${cashItem.name} (${formatRub(cashItem.balance)})`}
|
|
||||||
</MenuItem>
|
|
||||||
));
|
|
||||||
|
|
||||||
const paymentOptions = useMemo(() => {
|
|
||||||
const options = [<MenuItem key="none" value="none">Без оплаты</MenuItem>];
|
|
||||||
|
|
||||||
if (bankCardOptions && bankCardOptions.length > 0) {
|
|
||||||
options.push(<MenuItem key="BANK_CARD" value="BANK_CARD">Банковская карта</MenuItem>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cashOptions && cashOptions.length > 0) {
|
|
||||||
options.push(<MenuItem key="CASH" value="CASH">Наличные</MenuItem>);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}, [bankCardOptions, cashOptions]);
|
|
||||||
|
|
||||||
const onChangePaymentHandler = (event: SelectChangeEvent<'BANK_CARD' | 'CASH' | 'none'>) => {
|
|
||||||
const { value } = event.target;
|
|
||||||
if (value === 'none') {
|
|
||||||
setPaymentType(null);
|
|
||||||
} else {
|
|
||||||
// @ts-ignore todo
|
|
||||||
setPaymentType(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderCardSelect = () => (
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel id="bank-card-label">Карта</InputLabel>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
data-testid="bank-card-select"
|
|
||||||
defaultValue={bankCards?.[0].id}
|
|
||||||
disabled={disabled}
|
|
||||||
inputProps={{ 'data-testid': 'bank-card-select-input' }}
|
|
||||||
labelId="bank-card-label"
|
|
||||||
name="bank-card"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{bankCardOptions}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderCashSelect = () => (
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel id="cash-label">Наличные</InputLabel>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
data-testid="cash-select"
|
|
||||||
defaultValue={cash?.[0].id}
|
|
||||||
disabled={disabled}
|
|
||||||
inputProps={{ 'data-testid': 'cash-select-input' }}
|
|
||||||
labelId="cash-label"
|
|
||||||
name="cash"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{cashOptions ?? []}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<FormProvider {...methods}>
|
||||||
className={classes.form}
|
<form
|
||||||
data-testid={DataTestId.ADD_EXPENSE_ITEM_FORM}
|
className={classes.form}
|
||||||
onSubmit={onFormSubmitHandler}
|
data-testid={DataTestId.ADD_EXPENSE_ITEM_FORM}
|
||||||
ref={ref}
|
noValidate
|
||||||
{...formProps}
|
onSubmit={handleSubmit(onFormSubmitHandler)}
|
||||||
>
|
ref={ref}
|
||||||
<Typography variant="h6">
|
{...formProps}
|
||||||
Добавить список расходов
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
disabled={disabled}
|
|
||||||
label="Название"
|
|
||||||
name="title"
|
|
||||||
required
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
disabled={disabled}
|
|
||||||
label="Описание"
|
|
||||||
name="description"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DatePicker
|
|
||||||
defaultValue={new Date()}
|
|
||||||
disabled={disabled}
|
|
||||||
label="Дата"
|
|
||||||
name="date"
|
|
||||||
slotProps={{ textField: { required: true, size: 'small' } }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
disabled={disabled}
|
|
||||||
inputMode="numeric"
|
|
||||||
label="Сумма"
|
|
||||||
name="amount"
|
|
||||||
required
|
|
||||||
size="small"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
defaultValue={1}
|
|
||||||
disabled={disabled}
|
|
||||||
inputMode="numeric"
|
|
||||||
label="Количество"
|
|
||||||
name="count"
|
|
||||||
size="small"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormControl fullWidth>
|
|
||||||
<InputLabel id="payment-type-label">Способ оплаты</InputLabel>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
data-testid="payment-type-select"
|
|
||||||
defaultValue="none"
|
|
||||||
disabled={disabled}
|
|
||||||
inputProps={{ 'data-testid': 'payment-type-select-input' }}
|
|
||||||
labelId="payment-type-label"
|
|
||||||
name="paymentMethod"
|
|
||||||
onChange={onChangePaymentHandler}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{paymentOptions}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
{paymentType === 'BANK_CARD' && renderCardSelect()}
|
|
||||||
|
|
||||||
{paymentType === 'CASH' && renderCashSelect()}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
data-testid={DataTestId.ADD_EXPENSE_ITEM_SUBMIT_BUTTON}
|
|
||||||
disabled={disabled}
|
|
||||||
size="small"
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
>
|
>
|
||||||
Добавить
|
<Typography variant="h6">
|
||||||
</Button>
|
Добавить элемент расхода
|
||||||
</form>
|
</Typography>
|
||||||
|
|
||||||
|
<TitleField />
|
||||||
|
|
||||||
|
<DescriptionField />
|
||||||
|
|
||||||
|
<DateField />
|
||||||
|
|
||||||
|
<AmountField />
|
||||||
|
|
||||||
|
<CountField />
|
||||||
|
|
||||||
|
<PaymentTypeField />
|
||||||
|
|
||||||
|
{watch('paymentType') === 'BANK_CARD' && <BankCardField />}
|
||||||
|
|
||||||
|
{watch('paymentType') === 'CASH' && <CashField />}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
data-testid={DataTestId.ADD_EXPENSE_ITEM_SUBMIT_BUTTON}
|
||||||
|
disabled={formState.disabled || formState.isSubmitting}
|
||||||
|
loading={formState.isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const AmountField = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'amount'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="amount"
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
error={Boolean(formState.errors.amount)}
|
||||||
|
helperText={formState.errors.amount?.message}
|
||||||
|
inputMode="numeric"
|
||||||
|
label="Сумма"
|
||||||
|
onChange={(event) => {
|
||||||
|
field.onChange(Number(event.target.value));
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AmountField;
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useBankCards } from '@/entity/cards';
|
||||||
|
import { formatRub } from '@/shared/utils';
|
||||||
|
|
||||||
|
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const BankCardField = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'bank-card'>>();
|
||||||
|
|
||||||
|
const { data: bankCards } = useBankCards();
|
||||||
|
|
||||||
|
const bankCardOptions = bankCards?.map((card) => (
|
||||||
|
<MenuItem key={card.id} value={card.id}>
|
||||||
|
{`${card.name} (${formatRub(card.balance)})`}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={bankCards?.[0].id}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="bank-card"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
error={Boolean(formState.errors['bank-card'])}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel id="bank-card-label">Карта</InputLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
data-testid="bank-card-select"
|
||||||
|
inputProps={{ 'data-testid': 'bank-card-select-input' }}
|
||||||
|
labelId="bank-card-label"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{bankCardOptions}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<FormHelperText>
|
||||||
|
{formState.errors['bank-card']?.message}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BankCardField;
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useCash } from '@/entity/cash';
|
||||||
|
import { formatRub } from '@/shared/utils';
|
||||||
|
|
||||||
|
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const CashField = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'cash'>>();
|
||||||
|
const { data: cash } = useCash();
|
||||||
|
|
||||||
|
const cashOptions = cash?.map((cashItem) => (
|
||||||
|
<MenuItem key={cashItem.id} value={cashItem.id}>
|
||||||
|
{`${cashItem.name} (${formatRub(cashItem.balance)})`}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={cash?.[0].id}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="cash"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
error={Boolean(formState.errors.cash)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel id="cash-label">Наличные</InputLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
data-testid="cash-select"
|
||||||
|
inputProps={{ 'data-testid': 'cash-select-input' }}
|
||||||
|
labelId="cash-label"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{cashOptions}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<FormHelperText>
|
||||||
|
{formState.errors.cash?.message}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CashField;
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const CountField = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'count'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="count"
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
defaultValue={1}
|
||||||
|
error={Boolean(formState.errors.count)}
|
||||||
|
helperText={formState.errors.count?.message}
|
||||||
|
inputMode="numeric"
|
||||||
|
label="Количество"
|
||||||
|
onChange={(event) => {
|
||||||
|
field.onChange(Number(event.target.value));
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CountField;
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const DateField: FC = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'date'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="date"
|
||||||
|
render={({ field }) => (
|
||||||
|
<DatePicker
|
||||||
|
{...field}
|
||||||
|
disableFuture
|
||||||
|
format="dd.MM.yyyy"
|
||||||
|
label="Дата"
|
||||||
|
slotProps={{
|
||||||
|
textField: {
|
||||||
|
error: Boolean(formState.errors.date),
|
||||||
|
helperText: formState.errors.date?.message,
|
||||||
|
required: true,
|
||||||
|
size: 'small',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
timezone="UTC"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DateField;
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const DescriptionField: FC = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'description'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
error={Boolean(formState.errors.description)}
|
||||||
|
helperText={formState.errors.description?.message}
|
||||||
|
label="Описание"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DescriptionField;
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useBankCards } from '@/entity/cards';
|
||||||
|
import { useCash } from '@/entity/cash';
|
||||||
|
|
||||||
|
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const PaymentTypeField: FC = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'paymentType'>>();
|
||||||
|
|
||||||
|
const { data: bankCards } = useBankCards();
|
||||||
|
const { data: cash } = useCash();
|
||||||
|
|
||||||
|
const paymentTypeOptions = useMemo(() => {
|
||||||
|
const options = [<MenuItem key="none" value="none">Без оплаты</MenuItem>];
|
||||||
|
|
||||||
|
if (bankCards && bankCards.length > 0) {
|
||||||
|
options.push(<MenuItem key="BANK_CARD" value="BANK_CARD">Банковская карта</MenuItem>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cash && cash.length > 0) {
|
||||||
|
options.push(<MenuItem key="CASH" value="CASH">Наличные</MenuItem>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [bankCards, cash]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="paymentType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
error={Boolean(formState.errors.paymentType)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel id="payment-type-label">Способ оплаты</InputLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
data-testid="payment-type-select"
|
||||||
|
inputProps={{ 'data-testid': 'payment-type-select-input' }}
|
||||||
|
labelId="payment-type-label"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{paymentTypeOptions}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<FormHelperText>
|
||||||
|
{formState.errors.paymentType?.message}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentTypeField;
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const TitleField: FC = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'title'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
error={Boolean(formState.errors.title)}
|
||||||
|
helperText={formState.errors.title?.message}
|
||||||
|
label="Название"
|
||||||
|
required
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TitleField;
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
import { AddExpenseItemFormSchema } from '../AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
export type UseAddExpenseItemFormArgs = {
|
||||||
|
disabled?: boolean;
|
||||||
|
onSubmit: (data: AddExpenseItemFormValues) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddExpenseItemForm = (args: UseAddExpenseItemFormArgs) => {
|
||||||
|
const { disabled, onSubmit } = args;
|
||||||
|
|
||||||
|
const methods = useForm<AddExpenseItemFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
amount: 0,
|
||||||
|
count: 1,
|
||||||
|
date: new Date(Date.now()),
|
||||||
|
description: '',
|
||||||
|
paymentType: 'none',
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
disabled,
|
||||||
|
resolver: zodResolver(AddExpenseItemFormSchema),
|
||||||
|
shouldUnregister: true,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
formState,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
} = methods;
|
||||||
|
|
||||||
|
const onFormSubmitHandler: SubmitHandler<AddExpenseItemFormValues> = async (data) => {
|
||||||
|
await onSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
formState,
|
||||||
|
handleSubmit,
|
||||||
|
methods,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
watch,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,2 +1,5 @@
|
|||||||
export type { AddExpenseItemFormProps, AddExpenseItemFormValues } from './AddExpenseItemForm.ui';
|
export type { AddExpenseItemFormValues } from './AddExpenseItemForm.schema';
|
||||||
|
export { AddExpenseItemFormSchema } from './AddExpenseItemForm.schema';
|
||||||
|
|
||||||
|
export type { AddExpenseItemFormProps } from './AddExpenseItemForm.ui';
|
||||||
export { default as AddExpenseItemForm } from './AddExpenseItemForm.ui';
|
export { default as AddExpenseItemForm } from './AddExpenseItemForm.ui';
|
||||||
|
|||||||
@ -13,6 +13,6 @@ describe('Test DeleteExpenseItemForm', () => {
|
|||||||
await DeleteExpenseItemFormHelper.submitForm();
|
await DeleteExpenseItemFormHelper.submitForm();
|
||||||
|
|
||||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||||
expect(onSubmit).toHaveBeenCalledWith(DeleteExpenseItemFormHelper.expenseItem);
|
expect(onSubmit).toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,20 +1,14 @@
|
|||||||
import type {
|
import type { FC, FormHTMLAttributes, RefObject } from 'react';
|
||||||
FC,
|
|
||||||
FormEventHandler,
|
|
||||||
FormHTMLAttributes,
|
|
||||||
RefObject,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { Button, Typography } from '@mui/material';
|
import { Button, Typography } from '@mui/material';
|
||||||
|
|
||||||
import { DataTestId } from '@/shared/constants';
|
import { DataTestId } from '@/shared/constants';
|
||||||
|
|
||||||
import type { ExpenseListItem } from '../../types';
|
import type { UseDeleteExpenseItemFormArgs } from './hooks/useDeleteExpenseItemForm';
|
||||||
|
|
||||||
export type DeleteExpenseItemFormProps = BaseFormProps & {
|
import { useDeleteExpenseItemForm } from './hooks/useDeleteExpenseItemForm';
|
||||||
disabled?: boolean;
|
|
||||||
item: ExpenseListItem;
|
export type DeleteExpenseItemFormProps = BaseFormProps & UseDeleteExpenseItemFormArgs & {
|
||||||
onSubmit?: (item: ExpenseListItem) => Promise<void> | void;
|
|
||||||
ref?: RefObject<HTMLFormElement>;
|
ref?: RefObject<HTMLFormElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,20 +17,17 @@ type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
|||||||
const DeleteExpenseItemForm: FC<DeleteExpenseItemFormProps> = (props) => {
|
const DeleteExpenseItemForm: FC<DeleteExpenseItemFormProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
disabled,
|
disabled,
|
||||||
item,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
ref,
|
ref,
|
||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
|
||||||
event.preventDefault();
|
const { formState, handleSubmit, onFormSubmitHandler } = useDeleteExpenseItemForm({ disabled, onSubmit });
|
||||||
onSubmit?.(item);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
data-testid={DataTestId.DELETE_EXPENSE_ITEM_FORM}
|
data-testid={DataTestId.DELETE_EXPENSE_ITEM_FORM}
|
||||||
onSubmit={onFormSubmitHandler}
|
onSubmit={handleSubmit(onFormSubmitHandler)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
@ -47,9 +38,8 @@ const DeleteExpenseItemForm: FC<DeleteExpenseItemFormProps> = (props) => {
|
|||||||
<Button
|
<Button
|
||||||
color="error"
|
color="error"
|
||||||
data-testid={DataTestId.DELETE_EXPENSE_ITEM_SUBMIT_BUTTON}
|
data-testid={DataTestId.DELETE_EXPENSE_ITEM_SUBMIT_BUTTON}
|
||||||
disabled={disabled}
|
disabled={formState.disabled || formState.isSubmitting}
|
||||||
fullWidth
|
loading={formState.isSubmitting}
|
||||||
sx={{ mt: 2 }}
|
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
export type UseDeleteExpenseItemFormArgs = {
|
||||||
|
disabled?: boolean;
|
||||||
|
onSubmit: () => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteExpenseItemForm = (args: UseDeleteExpenseItemFormArgs) => {
|
||||||
|
const { disabled, onSubmit } = args;
|
||||||
|
|
||||||
|
const onFormSubmitHandler: SubmitHandler<never> = async () => {
|
||||||
|
await onSubmit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = useForm<never>({
|
||||||
|
disabled,
|
||||||
|
shouldUnregister: true,
|
||||||
|
});
|
||||||
|
const { formState, handleSubmit } = methods;
|
||||||
|
|
||||||
|
return {
|
||||||
|
formState,
|
||||||
|
handleSubmit,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export type { DeleteExpenseItemFormProps } from './DeleteExpenseItemForm.ui';
|
||||||
|
export { default as DeleteExpenseItemForm } from './DeleteExpenseItemForm.ui';
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
export const EditExpenseItemFormSchema = z.object({
|
||||||
|
amount: z.number(),
|
||||||
|
'bank-card': z.string().optional(),
|
||||||
|
cash: z.string().optional(),
|
||||||
|
count: z.number(),
|
||||||
|
date: z.date(),
|
||||||
|
description: z.string().trim().optional(),
|
||||||
|
paymentType: z.enum(['BANK_CARD', 'CASH', 'none']),
|
||||||
|
title: z.string().trim().min(5),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type EditExpenseItemFormValues = z.infer<typeof EditExpenseItemFormSchema>;
|
||||||
@ -20,7 +20,7 @@ describe('Test EditExpenseItemForm', () => {
|
|||||||
expect(titleInputElem).toHaveValue('Mock item');
|
expect(titleInputElem).toHaveValue('Mock item');
|
||||||
expect(descriptionInputElem).toHaveValue('Mock description');
|
expect(descriptionInputElem).toHaveValue('Mock description');
|
||||||
expect(dateInputElem).toHaveValue('22.08.2025');
|
expect(dateInputElem).toHaveValue('22.08.2025');
|
||||||
expect(amountInputElem).toHaveValue('12');
|
expect(amountInputElem).toHaveValue(12);
|
||||||
expect(countInputElem).toHaveValue(12);
|
expect(countInputElem).toHaveValue(12);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -29,8 +29,14 @@ describe('Test EditExpenseItemForm', () => {
|
|||||||
['disabled', { disabled: true }, (el: HTMLElement) => { expect(el).toBeDisabled(); }],
|
['disabled', { disabled: true }, (el: HTMLElement) => { expect(el).toBeDisabled(); }],
|
||||||
])('should render fields %s', (_state, options, assertion) => {
|
])('should render fields %s', (_state, options, assertion) => {
|
||||||
EditExpenseItemFormHelper.renderForm(options);
|
EditExpenseItemFormHelper.renderForm(options);
|
||||||
const { formElem, ...formElems } = EditExpenseItemFormHelper.getFormElements();
|
const elems = EditExpenseItemFormHelper.getFormElements();
|
||||||
Object.values(formElems).forEach(assertion);
|
Object.values([
|
||||||
|
elems.amountInputElem,
|
||||||
|
elems.dateInputElem,
|
||||||
|
elems.descriptionInputElem,
|
||||||
|
elems.submitButtonElem,
|
||||||
|
elems.titleInputElem,
|
||||||
|
]).forEach(assertion);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call onSubmit with values', async () => {
|
test('should call onSubmit with values', async () => {
|
||||||
@ -41,6 +47,7 @@ describe('Test EditExpenseItemForm', () => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
date: '29.08.2025',
|
date: '29.08.2025',
|
||||||
description: 'Описание элемента',
|
description: 'Описание элемента',
|
||||||
|
paymentType: 'none',
|
||||||
title: 'Название элемента',
|
title: 'Название элемента',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,9 +55,9 @@ describe('Test EditExpenseItemForm', () => {
|
|||||||
expect(onSubmit).toHaveBeenCalledWith({
|
expect(onSubmit).toHaveBeenCalledWith({
|
||||||
amount: 12,
|
amount: 12,
|
||||||
count: 1,
|
count: 1,
|
||||||
currency: 'RUB',
|
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||||
date: '2025-08-29T00:00:00.000Z',
|
|
||||||
description: 'Описание элемента',
|
description: 'Описание элемента',
|
||||||
|
paymentType: 'none',
|
||||||
title: 'Название элемента',
|
title: 'Название элемента',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,35 +1,31 @@
|
|||||||
import type {
|
import type {
|
||||||
FC,
|
FC,
|
||||||
FormEventHandler,
|
|
||||||
FormHTMLAttributes,
|
FormHTMLAttributes,
|
||||||
RefObject,
|
RefObject,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { Button, TextField, Typography } from '@mui/material';
|
import { Button, Typography } from '@mui/material';
|
||||||
import { DatePicker } from '@mui/x-date-pickers';
|
import { FormProvider } from 'react-hook-form';
|
||||||
|
|
||||||
import { DataTestId } from '@/shared/constants';
|
import { DataTestId } from '@/shared/constants';
|
||||||
import { getUTCDate } from '@/shared/utils';
|
|
||||||
|
|
||||||
import type { ExpenseListItem } from '../../types';
|
import type { UseEditExpenseItemFormArgs } from './hooks/useEditExpenseItemForm';
|
||||||
|
|
||||||
import classes from './EditExpenseItemForm.module.css';
|
import classes from './EditExpenseItemForm.module.css';
|
||||||
|
import AmountField from './fields/AmountField';
|
||||||
|
import BankCardField from './fields/BankCardField';
|
||||||
|
import CashField from './fields/CashField';
|
||||||
|
import CountField from './fields/CountField';
|
||||||
|
import DateField from './fields/DateField';
|
||||||
|
import DescriptionField from './fields/DescriptionField';
|
||||||
|
import PaymentTypeField from './fields/PaymentTypeField';
|
||||||
|
import TitleField from './fields/TitleField';
|
||||||
|
import { useEditExpenseItemForm } from './hooks/useEditExpenseItemForm';
|
||||||
|
|
||||||
export type EditExpenseItemFormProps = BaseFormProps & {
|
export type EditExpenseItemFormProps = BaseFormProps & UseEditExpenseItemFormArgs & {
|
||||||
disabled?: boolean;
|
|
||||||
item: ExpenseListItem;
|
|
||||||
onSubmit?: (data: EditExpenseItemFormValues) => Promise<void> | void;
|
|
||||||
ref?: RefObject<HTMLFormElement>;
|
ref?: RefObject<HTMLFormElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditExpenseItemFormValues = {
|
|
||||||
amount: number;
|
|
||||||
count: number;
|
|
||||||
currency: string;
|
|
||||||
date: string;
|
|
||||||
description: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||||
|
|
||||||
const EditExpenseItemForm: FC<EditExpenseItemFormProps> = (props) => {
|
const EditExpenseItemForm: FC<EditExpenseItemFormProps> = (props) => {
|
||||||
@ -41,93 +37,55 @@ const EditExpenseItemForm: FC<EditExpenseItemFormProps> = (props) => {
|
|||||||
...formProps
|
...formProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = async (event) => {
|
const {
|
||||||
event.preventDefault();
|
formState,
|
||||||
|
handleSubmit,
|
||||||
const formData = new FormData(event.currentTarget);
|
methods,
|
||||||
const title = formData.get('title');
|
onFormSubmitHandler,
|
||||||
const date = formData.get('date');
|
watch,
|
||||||
const amount = formData.get('amount');
|
} = useEditExpenseItemForm({ disabled, item, onSubmit });
|
||||||
const description = formData.get('description');
|
|
||||||
const count = formData.get('count');
|
|
||||||
|
|
||||||
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') {
|
|
||||||
await onSubmit?.({
|
|
||||||
amount: Number(amount),
|
|
||||||
count: Number(count),
|
|
||||||
currency: 'RUB',
|
|
||||||
date: getUTCDate(date),
|
|
||||||
description,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<FormProvider {...methods}>
|
||||||
className={classes.form}
|
<form
|
||||||
data-testid={DataTestId.EDIT_EXPENSE_ITEM_FORM}
|
className={classes.form}
|
||||||
onSubmit={onFormSubmitHandler}
|
data-testid={DataTestId.EDIT_EXPENSE_ITEM_FORM}
|
||||||
ref={ref}
|
noValidate
|
||||||
{...formProps}
|
onSubmit={handleSubmit(onFormSubmitHandler)}
|
||||||
>
|
ref={ref}
|
||||||
<Typography variant="h6">
|
{...formProps}
|
||||||
Добавить список расходов
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
defaultValue={item.title}
|
|
||||||
disabled={disabled}
|
|
||||||
label="Название"
|
|
||||||
name="title"
|
|
||||||
required
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
defaultValue={item.description ?? ''}
|
|
||||||
disabled={disabled}
|
|
||||||
label="Описание"
|
|
||||||
name="description"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DatePicker
|
|
||||||
defaultValue={new Date(item.date)}
|
|
||||||
disabled={disabled}
|
|
||||||
label="Дата"
|
|
||||||
name="date"
|
|
||||||
slotProps={{ textField: { required: true, size: 'small' } }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
defaultValue={item.amount}
|
|
||||||
disabled={disabled}
|
|
||||||
inputMode="numeric"
|
|
||||||
label="Сумма"
|
|
||||||
name="amount"
|
|
||||||
required
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
defaultValue={item.count ?? 1}
|
|
||||||
disabled={disabled}
|
|
||||||
label="Количество"
|
|
||||||
name="count"
|
|
||||||
size="small"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
data-testid={DataTestId.EDIT_EXPENSE_ITEM_SUBMIT_BUTTON}
|
|
||||||
disabled={disabled}
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
>
|
>
|
||||||
Сохранить
|
<Typography variant="h6">
|
||||||
</Button>
|
Редактировать элемент расхода
|
||||||
</form>
|
</Typography>
|
||||||
|
|
||||||
|
<TitleField />
|
||||||
|
|
||||||
|
<DescriptionField />
|
||||||
|
|
||||||
|
<DateField />
|
||||||
|
|
||||||
|
<AmountField />
|
||||||
|
|
||||||
|
<CountField />
|
||||||
|
|
||||||
|
<PaymentTypeField />
|
||||||
|
|
||||||
|
{watch('paymentType') === 'BANK_CARD' && <BankCardField />}
|
||||||
|
|
||||||
|
{watch('paymentType') === 'CASH' && <CashField />}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
data-testid={DataTestId.EDIT_EXPENSE_ITEM_SUBMIT_BUTTON}
|
||||||
|
disabled={formState.disabled || formState.isSubmitting}
|
||||||
|
loading={formState.isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const AmountField = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'amount'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="amount"
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
error={Boolean(formState.errors.amount)}
|
||||||
|
helperText={formState.errors.amount?.message}
|
||||||
|
inputMode="numeric"
|
||||||
|
label="Сумма"
|
||||||
|
onChange={(event) => {
|
||||||
|
field.onChange(Number(event.target.value));
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AmountField;
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useBankCards } from '@/entity/cards';
|
||||||
|
import { formatRub } from '@/shared/utils';
|
||||||
|
|
||||||
|
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const BankCardField = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'bank-card'>>();
|
||||||
|
|
||||||
|
const { data: bankCards } = useBankCards();
|
||||||
|
|
||||||
|
const bankCardOptions = bankCards?.map((card) => (
|
||||||
|
<MenuItem key={card.id} value={card.id}>
|
||||||
|
{`${card.name} (${formatRub(card.balance)})`}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={bankCards?.[0].id}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="bank-card"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
error={Boolean(formState.errors['bank-card'])}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel id="bank-card-label">Карта</InputLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
data-testid="bank-card-select"
|
||||||
|
inputProps={{ 'data-testid': 'bank-card-select-input' }}
|
||||||
|
labelId="bank-card-label"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{bankCardOptions}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<FormHelperText>
|
||||||
|
{formState.errors['bank-card']?.message}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BankCardField;
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useCash } from '@/entity/cash';
|
||||||
|
import { formatRub } from '@/shared/utils';
|
||||||
|
|
||||||
|
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const CashField = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'cash'>>();
|
||||||
|
const { data: cash } = useCash();
|
||||||
|
|
||||||
|
const cashOptions = cash?.map((cashItem) => (
|
||||||
|
<MenuItem key={cashItem.id} value={cashItem.id}>
|
||||||
|
{`${cashItem.name} (${formatRub(cashItem.balance)})`}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue={cash?.[0].id}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="cash"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
error={Boolean(formState.errors.cash)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel id="cash-label">Наличные</InputLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
data-testid="cash-select"
|
||||||
|
inputProps={{ 'data-testid': 'cash-select-input' }}
|
||||||
|
labelId="cash-label"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{cashOptions}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<FormHelperText>
|
||||||
|
{formState.errors.cash?.message}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CashField;
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const CountField = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'count'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="count"
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
defaultValue={1}
|
||||||
|
error={Boolean(formState.errors.count)}
|
||||||
|
helperText={formState.errors.count?.message}
|
||||||
|
inputMode="numeric"
|
||||||
|
label="Количество"
|
||||||
|
onChange={(event) => {
|
||||||
|
field.onChange(Number(event.target.value));
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CountField;
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const DateField: FC = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'date'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="date"
|
||||||
|
render={({ field }) => (
|
||||||
|
<DatePicker
|
||||||
|
{...field}
|
||||||
|
disableFuture
|
||||||
|
format="dd.MM.yyyy"
|
||||||
|
label="Дата"
|
||||||
|
slotProps={{
|
||||||
|
textField: {
|
||||||
|
error: Boolean(formState.errors.date),
|
||||||
|
helperText: formState.errors.date?.message,
|
||||||
|
required: true,
|
||||||
|
size: 'small',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DateField;
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const DescriptionField: FC = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'description'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
error={Boolean(formState.errors.description)}
|
||||||
|
helperText={formState.errors.description?.message}
|
||||||
|
label="Описание"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DescriptionField;
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useBankCards } from '@/entity/cards';
|
||||||
|
import { useCash } from '@/entity/cash';
|
||||||
|
|
||||||
|
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const PaymentTypeField: FC = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'paymentType'>>();
|
||||||
|
|
||||||
|
const { data: bankCards } = useBankCards();
|
||||||
|
const { data: cash } = useCash();
|
||||||
|
|
||||||
|
const paymentTypeOptions = useMemo(() => {
|
||||||
|
const options = [<MenuItem key="none" value="none">Без оплаты</MenuItem>];
|
||||||
|
|
||||||
|
if (bankCards && bankCards.length > 0) {
|
||||||
|
options.push(<MenuItem key="BANK_CARD" value="BANK_CARD">Банковская карта</MenuItem>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cash && cash.length > 0) {
|
||||||
|
options.push(<MenuItem key="CASH" value="CASH">Наличные</MenuItem>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [bankCards, cash]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="paymentType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl
|
||||||
|
error={Boolean(formState.errors.paymentType)}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<InputLabel id="payment-type-label">Способ оплаты</InputLabel>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
data-testid="payment-type-select"
|
||||||
|
inputProps={{ 'data-testid': 'payment-type-select-input' }}
|
||||||
|
labelId="payment-type-label"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{paymentTypeOptions}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<FormHelperText>
|
||||||
|
{formState.errors.paymentType?.message}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentTypeField;
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { TextField } from '@mui/material';
|
||||||
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
const TitleField: FC = () => {
|
||||||
|
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'title'>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
disabled={formState.disabled}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
error={Boolean(formState.errors.title)}
|
||||||
|
helperText={formState.errors.title?.message}
|
||||||
|
label="Название"
|
||||||
|
required
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TitleField;
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import type { ExpenseListItem } from '@/entity/expenses/item';
|
||||||
|
|
||||||
|
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
import { EditExpenseItemFormSchema } from '../EditExpenseItemForm.schema';
|
||||||
|
|
||||||
|
export type UseEditExpenseItemFormArgs = {
|
||||||
|
disabled?: boolean;
|
||||||
|
item: ExpenseListItem;
|
||||||
|
onSubmit: (data: EditExpenseItemFormValues) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEditExpenseItemForm = (args: UseEditExpenseItemFormArgs) => {
|
||||||
|
const { disabled, item, onSubmit } = args;
|
||||||
|
|
||||||
|
const methods = useForm<EditExpenseItemFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
amount: item.amount,
|
||||||
|
// eslint-disable-next-line no-undefined
|
||||||
|
'bank-card': item.payment?.type === 'BANK_CARD' ? item.payment.id ?? undefined : undefined,
|
||||||
|
// eslint-disable-next-line no-undefined
|
||||||
|
cash: item.payment?.type === 'CASH' ? item.payment.id ?? undefined : undefined,
|
||||||
|
count: item.count ?? 1,
|
||||||
|
date: new Date(item.date),
|
||||||
|
description: item.description ?? '',
|
||||||
|
paymentType: item.payment?.type ?? 'none',
|
||||||
|
title: item.title,
|
||||||
|
},
|
||||||
|
disabled,
|
||||||
|
resolver: zodResolver(EditExpenseItemFormSchema),
|
||||||
|
shouldUnregister: true,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
formState,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
} = methods;
|
||||||
|
|
||||||
|
const onFormSubmitHandler: SubmitHandler<EditExpenseItemFormValues> = async (data) => {
|
||||||
|
await onSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
formState,
|
||||||
|
handleSubmit,
|
||||||
|
methods,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
watch,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export type { EditExpenseItemFormValues } from './EditExpenseItemForm.schema';
|
||||||
|
export { EditExpenseItemFormSchema } from './EditExpenseItemForm.schema';
|
||||||
|
export type { EditExpenseItemFormProps } from './EditExpenseItemForm.ui';
|
||||||
|
export { default as EditExpenseItemForm } from './EditExpenseItemForm.ui';
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
||||||
|
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import type { AddExpenseItemFormValues } from '@/entity/expenses/item';
|
import type { AddExpenseItemFormValues } from '@/entity/expenses/item';
|
||||||
@ -26,10 +27,30 @@ export const useAddExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFormSubmitHandler = async (data: AddExpenseItemFormValues) => {
|
const onFormSubmitHandler = async (data: AddExpenseItemFormValues) => {
|
||||||
// todo добавить обработку ошибок (код + тест)
|
try {
|
||||||
await addExpenseItemMutation(data);
|
const payment = data.paymentType === 'none'
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
id: data.paymentType === 'BANK_CARD' ? data['bank-card'] : data.cash,
|
||||||
|
type: data.paymentType,
|
||||||
|
};
|
||||||
|
await addExpenseItemMutation({
|
||||||
|
amount: data.amount,
|
||||||
|
count: data.count,
|
||||||
|
currency: 'RUB',
|
||||||
|
date: data.date.toISOString(),
|
||||||
|
description: data.description ?? null,
|
||||||
|
// @ts-ignore TODO
|
||||||
|
payment,
|
||||||
|
title: data.title,
|
||||||
|
});
|
||||||
|
|
||||||
setIsOpen(false);
|
enqueueSnackbar(`Расход "${data.title}" успешно добавлен`, { variant: 'success' });
|
||||||
|
|
||||||
|
setIsOpen(false);
|
||||||
|
} catch {
|
||||||
|
enqueueSnackbar('Ошибка при добавлении расхода', { variant: 'error' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { mockEnqueueSnackbar } from '@mocks/jest/helpers';
|
||||||
import { server } from '@mocks/jest/server';
|
import { server } from '@mocks/jest/server';
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import { AddExpenseItemButtonHelper, AddExpenseItemFormHelper } from '@tests/helpers';
|
import { AddExpenseItemButtonHelper, AddExpenseItemFormHelper } from '@tests/helpers';
|
||||||
@ -22,7 +23,7 @@ describe('Test AddExpenseItemButton', () => {
|
|||||||
expect(modalElem).toBeVisible();
|
expect(modalElem).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should close modal after submit and send request', () => {
|
describe('should close modal after success request', () => {
|
||||||
let modalElem: HTMLElement;
|
let modalElem: HTMLElement;
|
||||||
const requestSpy = jest.fn();
|
const requestSpy = jest.fn();
|
||||||
|
|
||||||
@ -36,11 +37,10 @@ describe('Test AddExpenseItemButton', () => {
|
|||||||
date: '29.08.2025',
|
date: '29.08.2025',
|
||||||
description: 'Описание элемента',
|
description: 'Описание элемента',
|
||||||
title: 'Название элемента',
|
title: 'Название элемента',
|
||||||
}, { autoSubmit: false });
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send correct request', async () => {
|
test('should send correct request', () => {
|
||||||
await AddExpenseItemFormHelper.submitForm();
|
|
||||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(requestSpy).toHaveBeenCalledWith({
|
expect(requestSpy).toHaveBeenCalledWith({
|
||||||
amount: 12,
|
amount: 12,
|
||||||
@ -54,10 +54,41 @@ describe('Test AddExpenseItemButton', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should close modal after submit', async () => {
|
test('should close modal after submit', async () => {
|
||||||
await AddExpenseItemFormHelper.submitForm();
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(modalElem).not.toBeInTheDocument();
|
expect(modalElem).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should show success notification', () => {
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Расход "Название элемента" успешно добавлен', { variant: 'success' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should not close modal after failed request', () => {
|
||||||
|
let modalElem: HTMLElement;
|
||||||
|
const requestSpy = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
server.use(AddExpenseItemButtonHelper.submitHandler(requestSpy, 500));
|
||||||
|
modalElem = (await AddExpenseItemButtonHelper.openModal()).modalElem;
|
||||||
|
|
||||||
|
await AddExpenseItemFormHelper.fillForm({
|
||||||
|
amount: '12',
|
||||||
|
count: '1',
|
||||||
|
date: '29.08.2025',
|
||||||
|
description: 'Описание элемента',
|
||||||
|
title: 'Название элемента',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not close modal after submit', () => {
|
||||||
|
expect(modalElem).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error notification', () => {
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при добавлении расхода', { variant: 'error' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const AddExpenseItemButton: FC<AddExpenseItemButtonProps> = (props) => {
|
|||||||
Добавить элемент
|
Добавить элемент
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
<Modal onClose={onCloseModalHandler} open={isOpen || isPending}>
|
||||||
<AddExpenseItemForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
<AddExpenseItemForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { MouseEventHandler } from 'react';
|
import type { MouseEventHandler } from 'react';
|
||||||
|
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { type ExpenseListItem, useDeleteExpenseItem } from '@/entity/expenses/item';
|
import { type ExpenseListItem, useDeleteExpenseItem } from '@/entity/expenses/item';
|
||||||
@ -27,8 +28,14 @@ export const useDeleteExpenseItemButton = (args: UseDeleteExpenseItemButtonArgs)
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmitHandler = async () => {
|
const onFormSubmitHandler = async () => {
|
||||||
await deleteExpenseItemMutation();
|
try {
|
||||||
setIsOpen(false);
|
await deleteExpenseItemMutation();
|
||||||
|
|
||||||
|
enqueueSnackbar(`Расход "${item.title}" удален`, { variant: 'success' });
|
||||||
|
setIsOpen(false);
|
||||||
|
} catch {
|
||||||
|
enqueueSnackbar(`Ошибка при удалении расхода "${item.title}"`, { variant: 'error' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import { mockEnqueueSnackbar } from '@mocks/jest/helpers';
|
||||||
import { server } from '@mocks/jest/server';
|
import { server } from '@mocks/jest/server';
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
import { DeleteExpenseItemButtonHelper, DeleteExpenseItemFormHelper } from '@tests/helpers';
|
import { DeleteExpenseItemButtonHelper, DeleteExpenseItemFormHelper } from '@tests/helpers';
|
||||||
|
|
||||||
describe('Test DeleteExpenseItemButton', () => {
|
describe('Test DeleteExpenseItemButton', () => {
|
||||||
@ -23,15 +23,51 @@ describe('Test DeleteExpenseItemButton', () => {
|
|||||||
expect(modalElem).toBeVisible();
|
expect(modalElem).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should close modal after submit', async () => {
|
describe('should close modal after success request', () => {
|
||||||
server.use(DeleteExpenseItemButtonHelper.submitHandler);
|
let modalElem: HTMLElement;
|
||||||
const { modalElem } = await DeleteExpenseItemButtonHelper.openModal();
|
const requestSpy = jest.fn();
|
||||||
const { submitButtonElem } = DeleteExpenseItemFormHelper.getFormElements();
|
|
||||||
|
|
||||||
expect(submitButtonElem).toBeEnabled();
|
beforeEach(async () => {
|
||||||
await userEvent.click(submitButtonElem);
|
server.use(DeleteExpenseItemButtonHelper.submitHandler(requestSpy));
|
||||||
await waitFor(() => {
|
modalElem = (await DeleteExpenseItemButtonHelper.openModal()).modalElem;
|
||||||
expect(modalElem).not.toBeInTheDocument();
|
|
||||||
|
await DeleteExpenseItemFormHelper.submitForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send correct request', () => {
|
||||||
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should close modal after submit', async () => {
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(modalElem).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show success notification', () => {
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Расход "Mock item" удален', { variant: 'success' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should not close modal after failed request', () => {
|
||||||
|
let modalElem: HTMLElement;
|
||||||
|
const requestSpy = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
server.use(DeleteExpenseItemButtonHelper.submitHandler(requestSpy, 500));
|
||||||
|
modalElem = (await DeleteExpenseItemButtonHelper.openModal()).modalElem;
|
||||||
|
|
||||||
|
await DeleteExpenseItemFormHelper.submitForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not close modal after submit', () => {
|
||||||
|
expect(modalElem).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error notification', () => {
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при удалении расхода "Mock item"', { variant: 'error' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const DeleteExpenseItemButton: FC<DeleteExpenseItemButtonProps> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
||||||
<DeleteExpenseItemForm disabled={isPending} item={item} onSubmit={onFormSubmitHandler} />
|
<DeleteExpenseItemForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { MouseEventHandler } from 'react';
|
import type { MouseEventHandler } from 'react';
|
||||||
|
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { type EditExpenseItemFormValues, type ExpenseListItem, useEditExpenseItem } from '@/entity/expenses/item';
|
import { type EditExpenseItemFormValues, type ExpenseListItem, useEditExpenseItem } from '@/entity/expenses/item';
|
||||||
@ -27,9 +28,30 @@ export const useEditExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFormSubmitHandler = async (data: EditExpenseItemFormValues) => {
|
const onFormSubmitHandler = async (data: EditExpenseItemFormValues) => {
|
||||||
// todo добавить обработку ошибок (код + тест)
|
try {
|
||||||
await editExpenseItemMutation(data);
|
const payment = data.paymentType === 'none'
|
||||||
setIsOpen(false);
|
? null
|
||||||
|
: {
|
||||||
|
id: data.paymentType === 'BANK_CARD' ? data['bank-card'] : data.cash,
|
||||||
|
type: data.paymentType,
|
||||||
|
};
|
||||||
|
await editExpenseItemMutation({
|
||||||
|
amount: data.amount,
|
||||||
|
count: data.count,
|
||||||
|
currency: 'RUB',
|
||||||
|
date: data.date.toISOString(),
|
||||||
|
description: data.description ?? null,
|
||||||
|
// @ts-ignore TODO
|
||||||
|
payment,
|
||||||
|
title: data.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
enqueueSnackbar(`Расход "${data.title}" успешно сохранен`, { variant: 'success' });
|
||||||
|
|
||||||
|
setIsOpen(false);
|
||||||
|
} catch {
|
||||||
|
enqueueSnackbar('Ошибка при сохранении расхода', { variant: 'error' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { mockEnqueueSnackbar } from '@mocks/jest/helpers';
|
||||||
import { server } from '@mocks/jest/server';
|
import { server } from '@mocks/jest/server';
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
import { EditExpenseItemButtonHelper, EditExpenseItemFormHelper } from '@tests/helpers';
|
import { EditExpenseItemButtonHelper, EditExpenseItemFormHelper } from '@tests/helpers';
|
||||||
@ -22,7 +23,7 @@ describe('TestEditExpenseItemButton', () => {
|
|||||||
expect(modalElem).toBeVisible();
|
expect(modalElem).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should close modal after submit and send request', () => {
|
describe('should close modal after success request', () => {
|
||||||
let modalElem: HTMLElement;
|
let modalElem: HTMLElement;
|
||||||
const requestSpy = jest.fn();
|
const requestSpy = jest.fn();
|
||||||
|
|
||||||
@ -35,12 +36,12 @@ describe('TestEditExpenseItemButton', () => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
date: '29.08.2025',
|
date: '29.08.2025',
|
||||||
description: 'Описание элемента',
|
description: 'Описание элемента',
|
||||||
|
paymentType: 'none',
|
||||||
title: 'Название элемента',
|
title: 'Название элемента',
|
||||||
}, { autoSubmit: false });
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send correct request', async () => {
|
test('should send correct request', () => {
|
||||||
await EditExpenseItemFormHelper.submitForm();
|
|
||||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||||
expect(requestSpy).toHaveBeenCalledWith({
|
expect(requestSpy).toHaveBeenCalledWith({
|
||||||
amount: 12,
|
amount: 12,
|
||||||
@ -48,15 +49,48 @@ describe('TestEditExpenseItemButton', () => {
|
|||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
date: '2025-08-29T00:00:00.000Z',
|
date: '2025-08-29T00:00:00.000Z',
|
||||||
description: 'Описание элемента',
|
description: 'Описание элемента',
|
||||||
|
payment: null,
|
||||||
title: 'Название элемента',
|
title: 'Название элемента',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should close modal after submit', async () => {
|
test('should close modal after submit', async () => {
|
||||||
await EditExpenseItemFormHelper.submitForm();
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(modalElem).not.toBeInTheDocument();
|
expect(modalElem).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should show success notification', () => {
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Расход "Название элемента" успешно сохранен', { variant: 'success' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should not close modal after failed request', () => {
|
||||||
|
let modalElem: HTMLElement;
|
||||||
|
const requestSpy = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
server.use(EditExpenseItemButtonHelper.submitHandler(requestSpy, 500));
|
||||||
|
modalElem = (await EditExpenseItemButtonHelper.openModal()).modalElem;
|
||||||
|
|
||||||
|
await EditExpenseItemFormHelper.fillForm({
|
||||||
|
amount: 12,
|
||||||
|
count: 1,
|
||||||
|
date: '29.08.2025',
|
||||||
|
description: 'Описание элемента',
|
||||||
|
paymentType: 'none',
|
||||||
|
title: 'Название элемента',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not close modal after submit', () => {
|
||||||
|
expect(modalElem).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error notification', () => {
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при сохранении расхода', { variant: 'error' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const EditExpenseItemButton: FC<EditExpenseItemButtonProps> = (props) => {
|
|||||||
Редактировать
|
Редактировать
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
<Modal onClose={onCloseModalHandler} open={isOpen || isPending}>
|
||||||
<EditExpenseItemForm disabled={isPending} item={item} onSubmit={onFormSubmitHandler} />
|
<EditExpenseItemForm disabled={isPending} item={item} onSubmit={onFormSubmitHandler} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -32,7 +32,12 @@ const ExpensePage = () => {
|
|||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">
|
||||||
Списки расходов
|
Элементы расхода
|
||||||
|
{' '}
|
||||||
|
|
||||||
|
"
|
||||||
|
{expenseList?.title}
|
||||||
|
"
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<AddExpenseItemButton listId={id} sx={{ mb: 2, mt: 2 }} />
|
<AddExpenseItemButton listId={id} sx={{ mb: 2, mt: 2 }} />
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const expenseItem = {
|
|||||||
count: 12,
|
count: 12,
|
||||||
createdAt: '2025-08-29T07:57:40.503Z',
|
createdAt: '2025-08-29T07:57:40.503Z',
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
date: '2025-08-22T00:00:03.000Z',
|
date: '2025-08-22T00:00:00.000Z',
|
||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
@ -48,7 +48,7 @@ const openModal = async () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitHandler = (requestSpy: jest.Mock) => http.post<
|
const submitHandler = (requestSpy: jest.Mock, status = 200) => http.post<
|
||||||
AddExpenseItemPath,
|
AddExpenseItemPath,
|
||||||
AddExpenseItemBody,
|
AddExpenseItemBody,
|
||||||
AddExpenseItemSuccessResponse
|
AddExpenseItemSuccessResponse
|
||||||
@ -58,7 +58,7 @@ const submitHandler = (requestSpy: jest.Mock) => http.post<
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
requestSpy(body);
|
requestSpy(body);
|
||||||
|
|
||||||
return HttpResponse.json(expenseItem);
|
return HttpResponse.json(expenseItem, { status });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const expenseItem = {
|
|||||||
count: 12,
|
count: 12,
|
||||||
createdAt: '2025-08-29T07:57:40.503Z',
|
createdAt: '2025-08-29T07:57:40.503Z',
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
date: '2025-08-22T00:00:03.000Z',
|
date: '2025-08-22T00:00:00.000Z',
|
||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
@ -45,9 +45,13 @@ const openModal = async () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitHandler = http.delete<DeleteExpenseItemPath>(
|
const submitHandler = (requestSpy: jest.Mock, status = 200) => http.delete<DeleteExpenseItemPath>(
|
||||||
`/expenses/list/${expenseItem.expenseListId}/items/${expenseItem.id}`,
|
`/expenses/list/${expenseItem.expenseListId}/items/${expenseItem.id}`,
|
||||||
() => HttpResponse.json(expenseItem),
|
() => {
|
||||||
|
requestSpy(expenseItem);
|
||||||
|
|
||||||
|
return HttpResponse.json(expenseItem, { status });
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const DeleteExpenseItemButtonHelper = {
|
export const DeleteExpenseItemButtonHelper = {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const expenseItem = {
|
|||||||
count: 12,
|
count: 12,
|
||||||
createdAt: '2025-08-29T07:57:40.503Z',
|
createdAt: '2025-08-29T07:57:40.503Z',
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
date: '2025-08-22T00:00:03.000Z',
|
date: '2025-08-22T00:00:00.000Z',
|
||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
@ -25,7 +25,6 @@ const renderForm = (props?: Partial<DeleteExpenseItemFormProps>) => {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<DeleteExpenseItemForm
|
<DeleteExpenseItemForm
|
||||||
item={expenseItem}
|
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
{...props}
|
{...props}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { getBankCardList, getCashList } from '@mocks/browser/handlers';
|
||||||
|
import { server } from '@mocks/jest/server';
|
||||||
import { createAppWrapper } from '@mocks/jest/wrappers';
|
import { createAppWrapper } from '@mocks/jest/wrappers';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
@ -17,7 +19,7 @@ const expenseItem = {
|
|||||||
count: 12,
|
count: 12,
|
||||||
createdAt: '2025-08-29T07:57:40.503Z',
|
createdAt: '2025-08-29T07:57:40.503Z',
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
date: '2025-08-22T00:00:03.000Z',
|
date: '2025-08-22T00:00:00.000Z',
|
||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
@ -27,6 +29,7 @@ const expenseItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderButton = () => {
|
const renderButton = () => {
|
||||||
|
server.use(getBankCardList, getCashList);
|
||||||
render(
|
render(
|
||||||
<EditExpenseItemButton item={expenseItem} />,
|
<EditExpenseItemButton item={expenseItem} />,
|
||||||
{ wrapper: createAppWrapper() },
|
{ wrapper: createAppWrapper() },
|
||||||
@ -49,7 +52,7 @@ const openModal = async () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitHandler = (requestSpy: jest.Mock) => http.patch<
|
const submitHandler = (requestSpy: jest.Mock, status = 200) => http.patch<
|
||||||
EditExpenseItemPath,
|
EditExpenseItemPath,
|
||||||
EditExpenseItemBody,
|
EditExpenseItemBody,
|
||||||
EditExpenseItemSuccessResponse
|
EditExpenseItemSuccessResponse
|
||||||
@ -59,7 +62,7 @@ const submitHandler = (requestSpy: jest.Mock) => http.patch<
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
requestSpy(body);
|
requestSpy(body);
|
||||||
|
|
||||||
return HttpResponse.json(expenseItem);
|
return HttpResponse.json(expenseItem, { status });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import { getBankCardList, getCashList } from '@mocks/browser/handlers';
|
||||||
import { typeDateField, typeTextField } from '@mocks/jest/helpers';
|
import { typeDateField, typeTextField } from '@mocks/jest/helpers';
|
||||||
|
import { server } from '@mocks/jest/server';
|
||||||
import { createAppWrapper } from '@mocks/jest/wrappers';
|
import { createAppWrapper } from '@mocks/jest/wrappers';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
@ -13,7 +15,7 @@ const expenseItem = {
|
|||||||
count: 12,
|
count: 12,
|
||||||
createdAt: '2025-08-22T07:57:40.503Z',
|
createdAt: '2025-08-22T07:57:40.503Z',
|
||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
date: '2025-08-22T00:00:03.000Z',
|
date: '2025-08-22T00:00:00.000Z',
|
||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
@ -23,6 +25,7 @@ const expenseItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderForm = (props?: Partial<EditExpenseItemFormProps>) => {
|
const renderForm = (props?: Partial<EditExpenseItemFormProps>) => {
|
||||||
|
server.use(getBankCardList, getCashList);
|
||||||
const onSubmit = jest.fn();
|
const onSubmit = jest.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@ -46,14 +49,27 @@ const getFormElements = () => {
|
|||||||
const dateInputElem = screen.getAllByLabelText(/Дата/i)[1];
|
const dateInputElem = screen.getAllByLabelText(/Дата/i)[1];
|
||||||
const amountInputElem = screen.getByLabelText(/Сумма/);
|
const amountInputElem = screen.getByLabelText(/Сумма/);
|
||||||
const countInputElem = screen.getByLabelText(/Количество/);
|
const countInputElem = screen.getByLabelText(/Количество/);
|
||||||
const submitButtonElem = screen.getByRole('button', { name: /Сохранить/i });
|
const submitButtonElem = screen.getByTestId(DataTestId.EDIT_EXPENSE_ITEM_SUBMIT_BUTTON);
|
||||||
|
|
||||||
|
const paymentTypeSelect = screen.getByTestId('payment-type-select');
|
||||||
|
|
||||||
|
const bankCardSelect = screen.queryByTestId('bank-card-select');
|
||||||
|
const bankCardSelectInput = screen.queryByTestId('bank-card-select-input');
|
||||||
|
|
||||||
|
const cashSelect = screen.queryByTestId('cash-select');
|
||||||
|
const cashSelectInput = screen.queryByTestId('cash-select-input');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
amountInputElem,
|
amountInputElem,
|
||||||
|
bankCardSelect,
|
||||||
|
bankCardSelectInput,
|
||||||
|
cashSelect,
|
||||||
|
cashSelectInput,
|
||||||
countInputElem,
|
countInputElem,
|
||||||
dateInputElem,
|
dateInputElem,
|
||||||
descriptionInputElem,
|
descriptionInputElem,
|
||||||
formElem,
|
formElem,
|
||||||
|
paymentTypeSelect,
|
||||||
submitButtonElem,
|
submitButtonElem,
|
||||||
titleInputElem,
|
titleInputElem,
|
||||||
};
|
};
|
||||||
@ -65,7 +81,7 @@ const submitForm = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fillForm = async (
|
const fillForm = async (
|
||||||
data: Omit<EditExpenseItemFormValues, 'currency'>,
|
data: Omit<EditExpenseItemFormValues, 'currency' | 'date'> & { date: string },
|
||||||
options: { autoSubmit?: boolean } = {},
|
options: { autoSubmit?: boolean } = {},
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user