mirror of
https://github.com/siwa-net/my-finance-pal.git
synced 2024-11-10 00:51:56 +01:00
added resolver for add expense
This commit is contained in:
parent
e97a3b68cf
commit
3e82564694
8 changed files with 74 additions and 30 deletions
|
@ -29,6 +29,7 @@ export const AddBudgetForm = () => {
|
|||
variant="standard"
|
||||
type="text"
|
||||
label="Budget name"
|
||||
required
|
||||
{...register('name', { required: true })}
|
||||
error={!!errors.name}
|
||||
/>
|
||||
|
@ -40,7 +41,8 @@ export const AddBudgetForm = () => {
|
|||
variant="standard"
|
||||
type="number"
|
||||
label="Limit"
|
||||
{...register('limit', { min: 0, required: true })}
|
||||
required
|
||||
{...register('limit', { min: 0, required: true, valueAsNumber: true })}
|
||||
error={!!errors.limit}
|
||||
/>
|
||||
</Grid>
|
||||
|
|
|
@ -4,19 +4,12 @@ import { z } from 'zod';
|
|||
|
||||
import { NewBudgetModel } from '../../models/budget';
|
||||
|
||||
const budgetFormSchema = z
|
||||
.object({
|
||||
const budgetFormSchema = z.object({
|
||||
name: z.string(),
|
||||
limit: z.string(),
|
||||
limit: z.number(),
|
||||
startDate: z.date().optional(),
|
||||
endDate: z.date().optional(),
|
||||
})
|
||||
.transform<NewBudgetModel>((formValues) => ({
|
||||
...formValues,
|
||||
startDate: formValues.startDate,
|
||||
endDate: formValues.endDate,
|
||||
limit: parseInt(formValues.limit),
|
||||
}));
|
||||
});
|
||||
|
||||
export const resolver: Resolver<NewBudgetModel> = async (values: unknown) => {
|
||||
const parseResult = budgetFormSchema.safeParse(values);
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Button, Grid, TextField } from '@mui/material';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
|
||||
import { DatePicker } from './DatePicker/DatePicker';
|
||||
import { useAddExpense } from '../hooks/useAddExpense';
|
||||
import { NewExpenseModel } from '../models/expense';
|
||||
import { resolver } from './resolver';
|
||||
import { useAddExpense } from '../../hooks/useAddExpense';
|
||||
import { NewExpenseModel } from '../../models/expense';
|
||||
import { DatePicker } from '../DatePicker/DatePicker';
|
||||
|
||||
type AddExpenseFormProps = {
|
||||
id: string;
|
||||
|
@ -14,9 +15,9 @@ export const AddExpenseForm = ({ id }: AddExpenseFormProps) => {
|
|||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
formState: { errors, isDirty, isValid },
|
||||
control,
|
||||
} = useForm<NewExpenseModel>();
|
||||
} = useForm<NewExpenseModel>({ resolver, defaultValues: { date: new Date() } });
|
||||
|
||||
const { mutate: addExpense, isLoading } = useAddExpense(id);
|
||||
|
||||
|
@ -38,6 +39,7 @@ export const AddExpenseForm = ({ id }: AddExpenseFormProps) => {
|
|||
type="text"
|
||||
label="Description"
|
||||
{...register('description', { required: true })}
|
||||
error={!!errors.description}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
|
@ -57,7 +59,7 @@ export const AddExpenseForm = ({ id }: AddExpenseFormProps) => {
|
|||
type="submit"
|
||||
size="large"
|
||||
variant="contained"
|
||||
disabled={isLoading || Object.keys(errors).length > 0}
|
||||
disabled={isLoading || Object.keys(errors).length > 0 || !isDirty || !isValid}
|
||||
>
|
||||
Add Expense
|
||||
</Button>
|
25
src/components/AddExpenseForm/resolver.ts
Normal file
25
src/components/AddExpenseForm/resolver.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Resolver } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { NewExpenseModel } from '../../models/expense';
|
||||
|
||||
const expenseFormSchema = z.object({
|
||||
description: z.string(),
|
||||
amount: z.number(),
|
||||
date: z.date(),
|
||||
});
|
||||
|
||||
export const resolver: Resolver<NewExpenseModel> = async (values: unknown) => {
|
||||
const parseResult = expenseFormSchema.safeParse(values);
|
||||
if (!parseResult.success) {
|
||||
return {
|
||||
values: parseResult.error,
|
||||
errors: { root: { message: parseResult.error.message } },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
values: parseResult.data,
|
||||
errors: {},
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,15 +1,17 @@
|
|||
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons/faArrowLeft';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { List, ListItem, ListItemText } from '@mui/material';
|
||||
import is from '@sindresorhus/is';
|
||||
import Link from 'next/link';
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
import styles from './BudgetDetail.module.scss';
|
||||
import { ExpenseDescription } from './ExpenseDescription';
|
||||
import { datesToDayRange, dateToFormattedDay } from '../../helpers/date';
|
||||
import { useBudgetSummaryQuery } from '../../hooks/useBudgetSummaryQuery';
|
||||
import { Card } from '../_design/Card/Card';
|
||||
import { DetailWithTitle } from '../_design/DetailWithTitle/DetailWithTitle';
|
||||
import { AddExpenseForm } from '../AddExpenseForm';
|
||||
import { AddExpenseForm } from '../AddExpenseForm/AddExpenseForm';
|
||||
|
||||
type BudgetDetailProps = {
|
||||
id: string;
|
||||
|
@ -45,13 +47,18 @@ export const BudgetDetail: FunctionComponent<BudgetDetailProps> = ({ id }) => {
|
|||
</Card>
|
||||
<Card>
|
||||
<h3>Expenses</h3>
|
||||
<ul>
|
||||
<List>
|
||||
{expenses.map((expense) => (
|
||||
<li key={expense.id}>
|
||||
{dateToFormattedDay(expense.date)} - {expense.description}
|
||||
</li>
|
||||
<ListItem key={expense.id}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<ExpenseDescription amount={expense.amount} description={expense.description} />
|
||||
}
|
||||
secondary={dateToFormattedDay(expense.date)}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</ul>
|
||||
</List>
|
||||
</Card>
|
||||
</div>
|
||||
<div className={styles.DetailsContainerRight}>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.Wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
11
src/components/BudgetDetail/ExpenseDescription.tsx
Normal file
11
src/components/BudgetDetail/ExpenseDescription.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import styles from './ExpenseDescription.module.scss';
|
||||
|
||||
type ExpenseDescriptionProps = { amount: number; description: string };
|
||||
export const ExpenseDescription = ({ amount, description }: ExpenseDescriptionProps) => {
|
||||
return (
|
||||
<div className={styles.Wrapper}>
|
||||
<div>{description}</div>
|
||||
<div>{amount}€</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -17,7 +17,7 @@ type BudgetItemProps = { budget: BudgetModel };
|
|||
export const BudgetItem: FC<BudgetItemProps> = ({ budget }) => {
|
||||
const { id, spent, name, limit, startDate, endDate } = budget;
|
||||
|
||||
const remainingBudget = 100 - Math.trunc((100 * spent) / limit);
|
||||
const spentBudget = Math.trunc((100 * spent) / limit);
|
||||
|
||||
const isValidDateRange = is.date(startDate) && is.date(endDate) && endDate > startDate;
|
||||
|
||||
|
@ -28,12 +28,12 @@ export const BudgetItem: FC<BudgetItemProps> = ({ budget }) => {
|
|||
{name} <FontAwesomeIcon icon={faCoins} />
|
||||
</h3>
|
||||
|
||||
<DetailWithTitle title={'Remaining Budget'}>
|
||||
<ProgressBar percentage={remainingBudget} />
|
||||
<DetailWithTitle title="Spent Budget">
|
||||
<ProgressBar percentage={spentBudget} />
|
||||
</DetailWithTitle>
|
||||
|
||||
{isValidDateRange && (
|
||||
<DetailWithTitle title={'Period'}>
|
||||
<DetailWithTitle title="Period">
|
||||
<p>{datesToDayRange(startDate, endDate)}</p>
|
||||
</DetailWithTitle>
|
||||
)}
|
||||
|
|
Loading…
Reference in a new issue