mirror of
https://github.com/siwa-net/my-finance-pal.git
synced 2024-09-20 04:11:06 +02: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"
|
variant="standard"
|
||||||
type="text"
|
type="text"
|
||||||
label="Budget name"
|
label="Budget name"
|
||||||
|
required
|
||||||
{...register('name', { required: true })}
|
{...register('name', { required: true })}
|
||||||
error={!!errors.name}
|
error={!!errors.name}
|
||||||
/>
|
/>
|
||||||
|
@ -40,7 +41,8 @@ export const AddBudgetForm = () => {
|
||||||
variant="standard"
|
variant="standard"
|
||||||
type="number"
|
type="number"
|
||||||
label="Limit"
|
label="Limit"
|
||||||
{...register('limit', { min: 0, required: true })}
|
required
|
||||||
|
{...register('limit', { min: 0, required: true, valueAsNumber: true })}
|
||||||
error={!!errors.limit}
|
error={!!errors.limit}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -4,19 +4,12 @@ import { z } from 'zod';
|
||||||
|
|
||||||
import { NewBudgetModel } from '../../models/budget';
|
import { NewBudgetModel } from '../../models/budget';
|
||||||
|
|
||||||
const budgetFormSchema = z
|
const budgetFormSchema = z.object({
|
||||||
.object({
|
name: z.string(),
|
||||||
name: z.string(),
|
limit: z.number(),
|
||||||
limit: z.string(),
|
startDate: z.date().optional(),
|
||||||
startDate: z.date().optional(),
|
endDate: 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) => {
|
export const resolver: Resolver<NewBudgetModel> = async (values: unknown) => {
|
||||||
const parseResult = budgetFormSchema.safeParse(values);
|
const parseResult = budgetFormSchema.safeParse(values);
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Button, Grid, TextField } from '@mui/material';
|
import { Button, Grid, TextField } from '@mui/material';
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { DatePicker } from './DatePicker/DatePicker';
|
import { resolver } from './resolver';
|
||||||
import { useAddExpense } from '../hooks/useAddExpense';
|
import { useAddExpense } from '../../hooks/useAddExpense';
|
||||||
import { NewExpenseModel } from '../models/expense';
|
import { NewExpenseModel } from '../../models/expense';
|
||||||
|
import { DatePicker } from '../DatePicker/DatePicker';
|
||||||
|
|
||||||
type AddExpenseFormProps = {
|
type AddExpenseFormProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -14,9 +15,9 @@ export const AddExpenseForm = ({ id }: AddExpenseFormProps) => {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { errors },
|
formState: { errors, isDirty, isValid },
|
||||||
control,
|
control,
|
||||||
} = useForm<NewExpenseModel>();
|
} = useForm<NewExpenseModel>({ resolver, defaultValues: { date: new Date() } });
|
||||||
|
|
||||||
const { mutate: addExpense, isLoading } = useAddExpense(id);
|
const { mutate: addExpense, isLoading } = useAddExpense(id);
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ export const AddExpenseForm = ({ id }: AddExpenseFormProps) => {
|
||||||
type="text"
|
type="text"
|
||||||
label="Description"
|
label="Description"
|
||||||
{...register('description', { required: true })}
|
{...register('description', { required: true })}
|
||||||
|
error={!!errors.description}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
@ -57,7 +59,7 @@ export const AddExpenseForm = ({ id }: AddExpenseFormProps) => {
|
||||||
type="submit"
|
type="submit"
|
||||||
size="large"
|
size="large"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={isLoading || Object.keys(errors).length > 0}
|
disabled={isLoading || Object.keys(errors).length > 0 || !isDirty || !isValid}
|
||||||
>
|
>
|
||||||
Add Expense
|
Add Expense
|
||||||
</Button>
|
</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 { faArrowLeft } from '@fortawesome/free-solid-svg-icons/faArrowLeft';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { List, ListItem, ListItemText } from '@mui/material';
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
|
|
||||||
import styles from './BudgetDetail.module.scss';
|
import styles from './BudgetDetail.module.scss';
|
||||||
|
import { ExpenseDescription } from './ExpenseDescription';
|
||||||
import { datesToDayRange, dateToFormattedDay } from '../../helpers/date';
|
import { datesToDayRange, dateToFormattedDay } from '../../helpers/date';
|
||||||
import { useBudgetSummaryQuery } from '../../hooks/useBudgetSummaryQuery';
|
import { useBudgetSummaryQuery } from '../../hooks/useBudgetSummaryQuery';
|
||||||
import { Card } from '../_design/Card/Card';
|
import { Card } from '../_design/Card/Card';
|
||||||
import { DetailWithTitle } from '../_design/DetailWithTitle/DetailWithTitle';
|
import { DetailWithTitle } from '../_design/DetailWithTitle/DetailWithTitle';
|
||||||
import { AddExpenseForm } from '../AddExpenseForm';
|
import { AddExpenseForm } from '../AddExpenseForm/AddExpenseForm';
|
||||||
|
|
||||||
type BudgetDetailProps = {
|
type BudgetDetailProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -45,13 +47,18 @@ export const BudgetDetail: FunctionComponent<BudgetDetailProps> = ({ id }) => {
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<h3>Expenses</h3>
|
<h3>Expenses</h3>
|
||||||
<ul>
|
<List>
|
||||||
{expenses.map((expense) => (
|
{expenses.map((expense) => (
|
||||||
<li key={expense.id}>
|
<ListItem key={expense.id}>
|
||||||
{dateToFormattedDay(expense.date)} - {expense.description}
|
<ListItemText
|
||||||
</li>
|
primary={
|
||||||
|
<ExpenseDescription amount={expense.amount} description={expense.description} />
|
||||||
|
}
|
||||||
|
secondary={dateToFormattedDay(expense.date)}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</List>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.DetailsContainerRight}>
|
<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 }) => {
|
export const BudgetItem: FC<BudgetItemProps> = ({ budget }) => {
|
||||||
const { id, spent, name, limit, startDate, endDate } = 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;
|
const isValidDateRange = is.date(startDate) && is.date(endDate) && endDate > startDate;
|
||||||
|
|
||||||
|
@ -28,12 +28,12 @@ export const BudgetItem: FC<BudgetItemProps> = ({ budget }) => {
|
||||||
{name} <FontAwesomeIcon icon={faCoins} />
|
{name} <FontAwesomeIcon icon={faCoins} />
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<DetailWithTitle title={'Remaining Budget'}>
|
<DetailWithTitle title="Spent Budget">
|
||||||
<ProgressBar percentage={remainingBudget} />
|
<ProgressBar percentage={spentBudget} />
|
||||||
</DetailWithTitle>
|
</DetailWithTitle>
|
||||||
|
|
||||||
{isValidDateRange && (
|
{isValidDateRange && (
|
||||||
<DetailWithTitle title={'Period'}>
|
<DetailWithTitle title="Period">
|
||||||
<p>{datesToDayRange(startDate, endDate)}</p>
|
<p>{datesToDayRange(startDate, endDate)}</p>
|
||||||
</DetailWithTitle>
|
</DetailWithTitle>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in a new issue