feat: add expense form.

This commit is contained in:
elforjani13
2022-06-30 22:03:50 +02:00
parent 6134ad5598
commit 6f2a456a56
9 changed files with 446 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({
expenseName: Yup.string().label(
intl.get('expense.schema.label.expense_name'),
),
estimatedExpense: Yup.number().label(
intl.get('expense.schema.label.estimated_expense'),
),
expemseDate: Yup.date(),
expenseQuantity: Yup.number().label(intl.get('expense.schema.label.quantity')),
expenseUnitPrice: Yup.number().label(
intl.get('expense.schema.label.unitPrice'),
),
expenseTotal: Yup.number(),
expenseCharge: Yup.string(),
});
export const CreateExpenseFormSchema = Schema;

View File

@@ -0,0 +1,64 @@
import React from 'react';
import moment from 'moment';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { AppToaster } from 'components';
import { CreateExpenseFormSchema } from './ExpenseForm.schema';
import ExpenseFormContent from './ExpenseFormContent';
import { useExpenseFormContext } from './ExpenseFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const defaultInitialValues = {
expenseName: '',
estimatedExpense: '',
expemseDate: moment(new Date()).format('YYYY-MM-DD'),
expenseUnitPrice: '',
expenseQuantity: 1,
expenseCharge: '% markup',
percentage: '',
expenseTotal: '',
};
/**
* Expense form.
* @returns
*/
function ExpenseForm({
//#withDialogActions
closeDialog,
}) {
const initialValues = {
...defaultInitialValues,
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {};
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({});
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
};
};
return (
<Formik
validationSchema={CreateExpenseFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
component={ExpenseFormContent}
/>
);
}
export default compose(withDialogActions)(ExpenseForm);

View File

@@ -0,0 +1,55 @@
//@ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Classes, ControlGroup } from '@blueprintjs/core';
import { FFormGroup, FInputGroup, Choose } from 'components';
import { useFormikContext } from 'formik';
function PercentageFormField() {
return (
<FFormGroup
label={intl.get('expenses.dialog.percentage')}
name={'percentage'}
>
<FInputGroup name="percentage" />
</FFormGroup>
);
}
function CustomPirceField() {
return (
<ControlGroup className={Classes.FILL}>
<FFormGroup
name={'expenseUnitPrice'}
label={intl.get('expenses.dialog.unit_price')}
>
<FInputGroup name="expenseUnitPrice" />
</FFormGroup>
<FFormGroup
name={'expenseTotal'}
label={intl.get('expenses.dialog.total')}
>
<FInputGroup name="expenseTotal" />
</FFormGroup>
</ControlGroup>
);
}
/**
* Expense form charge fields.
* @returns
*/
export default function ExpenseFormChargeFields() {
const { values } = useFormikContext();
return (
<Choose>
<Choose.When condition={values.expenseCharge === '% markup'}>
<PercentageFormField />
</Choose.When>
<Choose.When condition={values.expenseCharge === 'Custom Pirce'}>
<CustomPirceField />
</Choose.When>
</Choose>
);
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Form } from 'formik';
import ExpenseFormFields from './ExpenseFormFields';
import ExpneseFormFloatingActions from './ExpneseFormFloatingActions';
/**
* Expense form content.
* @returns
*/
export default function ExpenseFormContent() {
return (
<Form>
<ExpenseFormFields />
<ExpneseFormFloatingActions />
</Form>
);
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { ExpenseFormProvider } from './ExpenseFormProvider';
import ExpenseForm from './ExpenseForm';
/**
* Expense form dialog content.
* @returns
*/
export default function ExpenseFormDialogContent({
// #ownProps
dialogName,
expense,
}) {
return (
<ExpenseFormProvider dialogName={dialogName} expenseId={expense}>
<ExpenseForm />
</ExpenseFormProvider>
);
}

View File

@@ -0,0 +1,141 @@
//@ts-nocheck
import React from 'react';
import styled from 'styled-components';
import intl from 'react-intl-universal';
import { Classes, Position, ControlGroup } from '@blueprintjs/core';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import {
FFormGroup,
FInputGroup,
FDateInput,
FormattedMessage as T,
} from 'components';
import { ExpenseSelect } from '../../components';
import ExpenseFormChargeFields from './ExpenseFormChargeFields';
import { momentFormatter } from 'utils';
import { useExpenseFormContext } from './ExpenseFormProvider';
import { ChargeSelect } from '../../components';
import { expenseChargeOption } from 'common/modalChargeOptions';
/**
* Expense form fields.
* @returns
*/
export default function ExpenseFormFields() {
return (
<div className={Classes.DIALOG_BODY}>
{/*------------ Expense Name -----------*/}
<FFormGroup
label={intl.get('expenses.dialog.expense_name')}
name={'expenseName'}
>
<FInputGroup name="expenseName" />
</FFormGroup>
{/*------------ Track to Expense -----------*/}
<FFormGroup
name={'estimatedExpense'}
label={intl.get('expenses.dialog.track_expense')}
className={classNames('form-group--select-list', Classes.FILL)}
>
<ExpenseSelect
name={'estimatedExpense'}
popoverProps={{ minimal: true }}
expenses={[{ id: 1, name: 'Expense 1' }]}
/>
</FFormGroup>
{/*------------ Extimated Date -----------*/}
<FFormGroup
label={intl.get('expenses.dialog.expense_date')}
name={'expemseDate'}
className={classNames(CLASSES.FILL, 'form-group--date')}
>
<FDateInput
{...momentFormatter('YYYY/MM/DD')}
name="expemseDate"
formatDate={(date) => date.toLocaleString()}
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
/>
</FFormGroup>
{/*------------ Quantity -----------*/}
<FFormGroup
label={intl.get('expenses.dialog.quantity')}
name={'expenseQuantity'}
>
<FInputGroup name="expenseQuantity" />
</FFormGroup>
<MetaLineLabel>Cost to you</MetaLineLabel>
{/*------------ Unit Price -----------*/}
<ControlGroup className={Classes.FILL}>
<FFormGroup
name={'unitPrice'}
label={intl.get('expenses.dialog.unit_price')}
>
<FInputGroup name="expenseUnitPrice" />
</FFormGroup>
<FFormGroup
name={'expenseTotal'}
label={intl.get('expenses.dialog.expense_total')}
>
<FInputGroup name="expenseTotal" />
</FFormGroup>
</ControlGroup>
<MetaLineLabel>What you'll charge</MetaLineLabel>
{/*------------ Charge -----------*/}
<FFormGroup
name={'expenseCharge'}
label={<T id={'expenses.dialog.charge'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<ChargeSelect
name="expenseCharge"
items={expenseChargeOption}
popoverProps={{ minimal: true }}
filterable={false}
/>
</FFormGroup>
{/*------------ Charge Fields -----------*/}
<ExpenseFormChargeFields />
{/*------------ Total -----------*/}
<ExpenseTotalBase>
<ExpenseTotalLabel>
<T id={'expenses.dialog.total'} />
</ExpenseTotalLabel>
<ExpenseTotal>0.00</ExpenseTotal>
</ExpenseTotalBase>
</div>
);
}
const MetaLineLabel = styled.div`
font-size: 14px;
line-height: 1.5rem;
font-weight: 500;
margin-bottom: 8px;
`;
const ExpenseTotalBase = styled.div`
display: block;
text-align: right;
`;
const ExpenseTotalLabel = styled.div`
font-size: 14px;
line-height: 1.5rem;
opacity: 0.75;
`;
const ExpenseTotal = styled.div`
font-size: 15px;
font-weight: 700;
padding-left: 14px;
line-height: 2rem;
`;

View File

@@ -0,0 +1,30 @@
//@ts-nocheck
import React from 'react';
import { DialogContent } from 'components';
const ExpenseFormContext = React.createContext();
/**
* Expense form provider.
* @returns
*/
function ExpenseFormProvider({
//#OwnProps
dialogName,
expenseId,
...props
}) {
// state provider.
const provider = {
dialogName,
};
return (
<DialogContent>
<ExpenseFormContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useExpenseFormContext = () => React.useContext(ExpenseFormContext);
export { ExpenseFormProvider, useExpenseFormContext };

View File

@@ -0,0 +1,44 @@
//@ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { useExpenseFormContext } from './ExpenseFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
function ExpneseFormFloatingActions({
// #withDialogActions
closeDialog,
}) {
// Formik context.
const { isSubmitting } = useFormikContext();
// expense form dialog context.
const { dialogName } = useExpenseFormContext();
// Handle close button click.
const handleCancelBtnClick = () => {
closeDialog(dialogName);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
<T id={'cancel'} />
</Button>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '75px' }}
type="submit"
>
{<T id={'save'} />}
</Button>
</div>
</div>
);
}
export default compose(withDialogActions)(ExpneseFormFloatingActions);

View File

@@ -0,0 +1,55 @@
import React from 'react';
import styled from 'styled-components';
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const ExpenseFormeDialogContent = React.lazy(
() => import('./ExpenseFormDialogContent'),
);
/**
* Expense form dialog.
* @returns
*/
function ExpenseFormDialog({
dialogName,
payload: { projectId = null },
isOpen,
}) {
return (
<ExpenseFormDialogRoot
name={dialogName}
title={<T id={'expenses.dialog.label'} />}
isOpen={isOpen}
autoFocus={true}
canEscapeKeyClose={true}
style={{ width: '400px' }}
>
<DialogSuspense>
<ExpenseFormeDialogContent
dialogName={dialogName}
expense={projectId}
/>
</DialogSuspense>
</ExpenseFormDialogRoot>
);
}
export default compose(withDialogRedux())(ExpenseFormDialog);
const ExpenseFormDialogRoot = styled(Dialog)`
.bp3-dialog-body {
.bp3-form-group {
margin-bottom: 15px;
label.bp3-label {
margin-bottom: 3px;
font-size: 13px;
}
}
}
.bp3-dialog-footer {
padding-top: 10px;
}
`;