feat: add estimated expense.

This commit is contained in:
elforjani13
2022-06-30 21:59:12 +02:00
parent cd08d0ee16
commit 6134ad5598
14 changed files with 550 additions and 4 deletions

View File

@@ -1,7 +1,17 @@
import intl from 'react-intl-universal';
export const modalChargeOptions = [
{ name: 'Hourly rate', value: 'Hourly rate' },
{ name: 'Fixed price', value: 'Fixed price' },
{ name: 'Non-chargeable', value: 'Non-chargeable' },
export const taskChargeOptions = [
{ name: intl.get('task.dialog.hourly_rate'), value: 'Hourly rate' },
{ name: intl.get('task.dialog.fixed_price'), value: 'Fixed price' },
{ name: intl.get('task.dialog.non_chargeable'), value: 'Non-chargeable' },
];
export const expenseChargeOption = [
{
name: intl.get('expenses.dialog.markup'),
value: '% markup',
},
{ name: intl.get('expenses.dialog.pass_cost_on'), value: 'Pass cost on' },
{ name: intl.get('expemses.dialog.custom_price'), value: 'Custom Pirce' },
{ name: intl.get('expenses.dialog.non_chargeable'), value: 'Non-chargeable' },
];

View File

@@ -43,6 +43,8 @@ import VendorOpeningBalanceDialog from '../containers/Dialogs/VendorOpeningBalan
import ProjectFormDialog from '../containers/Projects/containers/ProjectFormDialog';
import TaskFormDialog from '../containers/Projects/containers/TaskFormDialog';
import TimeEntryFormDialog from '../containers/Projects/containers/TimeEntryFormDialog';
import ExpenseFormDialog from '../containers/Projects/containers/ExpenseFormDialog';
import EstimatedExpenseFormDialog from '../containers/Projects/containers/EstimatedExpenseFormDialog';
/**
* Dialogs container.
@@ -96,6 +98,8 @@ export default function DialogsContainer() {
<ProjectFormDialog dialogName={'project-form'} />
<TaskFormDialog dialogName={'task-form'} />
<TimeEntryFormDialog dialogName={'time-entry-form'} />
<ExpenseFormDialog dialogName={'expense-form'} />
<EstimatedExpenseFormDialog dialogName={'estimated-expense-form'} />
</div>
);
}

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from 'components';
/**
*
* @param {*}
* @param {*} param1
* @returns
*/
const chargeItemRenderer = (item, { handleClick, modifiers, query }) => {
return (
<MenuItem
label={item.label}
key={item.name}
onClick={handleClick}
text={item.name}
/>
);
};
const chargeItemSelectProps = {
itemRenderer: chargeItemRenderer,
valueAccessor: 'value',
labelAccessor: 'name',
};
/**
*
* @param param0
* @returns
*/
export function ChargeSelect({ items, ...rest }) {
return (
<FSelect
{...chargeItemSelectProps}
{...rest}
items={items}
input={ChargeSelectButton}
/>
);
}
/**
*
* @param param0
* @returns
*/
function ChargeSelectButton({ label }) {
return <Button text={label} />;
}

View File

@@ -0,0 +1,67 @@
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from 'components';
/**
*
* @param query
* @param expense
* @param _index
* @param exactMatch
*/
const expenseItemPredicate = (query, expense, _index, exactMatch) => {
const normalizedTitle = expense.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${expense.name}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param expense
* @param param1
* @returns
*/
const expenseItemRenderer = (expense, { handleClick, modifiers, query }) => {
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
key={expense.id}
onClick={handleClick}
text={expense.name}
/>
);
};
const expenseSelectProps = {
itemPredicate: expenseItemPredicate,
itemRenderer: expenseItemRenderer,
valueAccessor: 'id',
labelAccessor: 'name',
};
export function ExpenseSelect({ expenses, defaultText, ...rest }) {
return (
<FSelect
items={expenses}
{...expenseSelectProps}
{...rest}
input={ExpenseSelectButton}
/>
);
}
function ExpenseSelectButton({ label, ...rest }) {
return (
<Button
text={label ? label : intl.get('choose_an_estimated_expense')}
{...rest}
/>
);
}

View File

@@ -0,0 +1,2 @@
export * from './ExpenseSelect';
export * from './ChargeSelect';

View File

@@ -0,0 +1,19 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({
estimatedExpense: Yup.number().label(
intl.get('estimated_expense.schema.label.estimated_expense'),
),
quantity: Yup.number().label(
intl.get('estimated_expense.schema.label.quantity'),
),
unitPrice: Yup.number().label(
intl.get('estimated_expense.schema.label.unit_price'),
),
total: Yup.number(),
charge: Yup.string(),
});
export const CreateEstimatedExpenseFormSchema = Schema;

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { Formik } from 'formik';
import { AppToaster } from 'components';
import { CreateEstimatedExpenseFormSchema } from './EstimatedExpense.schema';
import EstimatedExpenseFormConent from './EstimatedExpenseFormConent';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const defaultInitialValues = {
estimatedExpense: '',
unitPrice: '',
quantity: 1,
charge: '% markup',
percentage: '',
};
/**
* Estimated expense form dialog.
* @returns
*/
function EstimatedExpenseForm({
//#withDialogActions
closeDialog,
}) {
const initialValues = {
...defaultInitialValues,
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({});
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
};
};
return (
<Formik
validationSchema={CreateEstimatedExpenseFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
component={EstimatedExpenseFormConent}
/>
);
}
export default compose(withDialogActions)(EstimatedExpenseForm);

View File

@@ -0,0 +1,54 @@
//@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('estimated_expenses.dialog.percentage')}
name={'percentage'}
>
<FInputGroup name="percentage" />
</FFormGroup>
);
}
function CustomPirceField() {
return (
<ControlGroup className={Classes.FILL}>
<FFormGroup
name={'unitPrice'}
label={intl.get('estimated_expenses.dialog.unit_price')}
>
<FInputGroup name="unitPrice" />
</FFormGroup>
<FFormGroup
name={'unitPrice'}
label={intl.get('estimated_expenses.dialog.total')}
>
<FInputGroup name="total" />
</FFormGroup>
</ControlGroup>
);
}
/**
* estimate expense form charge fields.
* @returns
*/
export default function EstimatedExpenseFormChargeFields() {
const { values } = useFormikContext();
return (
<Choose>
<Choose.When condition={values.charge === '% markup'}>
<PercentageFormField />
</Choose.When>
<Choose.When condition={values.charge === 'Custom Pirce'}>
<CustomPirceField />
</Choose.When>
</Choose>
);
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Form } from 'formik';
import EstimatedExpenseFormFields from './EstimatedExpenseFormFields';
import EstimatedExpenseFormFloatingActions from './EstimatedExpenseFormFloatingActions';
/**
* Estimated expense form content.
* @returns
*/
export default function EstimatedExpenseFormConent() {
return (
<Form>
<EstimatedExpenseFormFields />
<EstimatedExpenseFormFloatingActions />
</Form>
);
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { EstimatedExpenseFormProvider } from './EstimatedExpenseFormProvider';
import EstimatedExpenseForm from './EstimatedExpenseForm';
/**
* Estimate expense form dialog.
* @return
*/
export default function EstimatedExpenseFormDialogContent({
//#ownProps
dialogName,
estimatedExpense,
}) {
return (
<EstimatedExpenseFormProvider
dialogName={dialogName}
estimatedExpenseId={estimatedExpense}
>
<EstimatedExpenseForm />
</EstimatedExpenseFormProvider>
);
}

View File

@@ -0,0 +1,111 @@
//@ts-nocheck
import React from 'react';
import styled from 'styled-components';
import intl from 'react-intl-universal';
import { Classes, ControlGroup } from '@blueprintjs/core';
import classNames from 'classnames';
import {
FFormGroup,
FInputGroup,
FormattedMessage as T,
FieldRequiredHint,
} from 'components';
import { ExpenseSelect } from '../../components';
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
import EstimatedExpenseFormChargeFields from './EstimatedExpenseFormChargeFields';
import { ChargeSelect } from '../../components';
import { expenseChargeOption } from 'common/modalChargeOptions';
/**
* Estimated expense form fields.
* @returns
*/
export default function EstimatedExpenseFormFields() {
return (
<div className={Classes.DIALOG_BODY}>
{/*------------ Estimated Expense -----------*/}
<FFormGroup
name={'estimatedExpense'}
label={intl.get('estimated_expenses.dialog.estimated_expense')}
className={classNames('form-group--select-list', Classes.FILL)}
>
<ExpenseSelect
name={'estimatedExpense'}
popoverProps={{ minimal: true }}
expenses={[]}
/>
</FFormGroup>
{/*------------ Quantity -----------*/}
<FFormGroup
label={intl.get('estimated_expenses.dialog.quantity')}
name={'quantity'}
>
<FInputGroup name="quantity" />
</FFormGroup>
<MetaLineLabel>Cost to you</MetaLineLabel>
{/*------------ Unit Price -----------*/}
<ControlGroup className={Classes.FILL}>
<FFormGroup
name={'unitPrice'}
label={intl.get('estimated_expenses.dialog.unit_price')}
>
<FInputGroup name="unitPrice" />
</FFormGroup>
<FFormGroup
name={'unitPrice'}
label={intl.get('estimated_expenses.dialog.total')}
>
<FInputGroup label="Total" name="total" />
</FFormGroup>
</ControlGroup>
<MetaLineLabel>What you'll charge</MetaLineLabel>
{/*------------ Charge -----------*/}
<FFormGroup
name={'charge'}
label={<T id={'estimated_expenses.dialog.charge'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<ChargeSelect
name="charge"
items={expenseChargeOption}
popoverProps={{ minimal: true }}
filterable={false}
/>
</FFormGroup>
<EstimatedExpenseFormChargeFields />
{/*------------ Estimated Amount -----------*/}
<EstimatedAmountWrap>
<EstimatedAmountLabel>
<T id={'estimated_expenses.dialog.estimated_amount'} />
</EstimatedAmountLabel>
<EstimatedAmount>0.00</EstimatedAmount>
</EstimatedAmountWrap>
</div>
);
}
const MetaLineLabel = styled.div`
font-size: 14px;
line-height: 1.5rem;
font-weight: 500;
margin-bottom: 8px;
`;
const EstimatedAmountWrap = styled.div`
display: block;
text-align: right;
`;
const EstimatedAmountLabel = styled.span`
font-size: 14px;
line-height: 1.5rem;
opacity: 0.75;
`;
const EstimatedAmount = styled.span`
font-size: 15px;
font-weight: 700;
padding-left: 14px;
line-height: 2rem;
`;

View File

@@ -0,0 +1,48 @@
//@ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Estimated expense form floating actions.
* @returns
*/
function EstimatedExpenseFormFloatingActions({
// #withDialogActions
closeDialog,
}) {
// Formik context.
const { isSubmitting } = useFormikContext();
// expense form dialog context.
const { dialogName } = useEstimatedExpenseFormContext();
// 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)(EstimatedExpenseFormFloatingActions);

View File

@@ -0,0 +1,31 @@
//@ts-nocheck
import React from 'react';
import { DialogContent } from 'components';
const EstimatedExpenseFormContext = React.createContext();
/**
* Estimated expense form provider.
* @returns
*/
function EstimatedExpenseFormProvider({
//#OwnProps
dialogName,
estimatedExpenseId,
...props
}) {
// state provider.
const provider = {
dialogName,
};
return (
<DialogContent>
<EstimatedExpenseFormContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useEstimatedExpenseFormContext = () =>
React.useContext(EstimatedExpenseFormContext);
export { EstimatedExpenseFormProvider, useEstimatedExpenseFormContext };

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 EstimatedExpenseFormDialogContent = React.lazy(
() => import('./EstimatedExpenseFormDialogContent'),
);
/**
* Estimate expense form dialog.
* @returns
*/
function EstimatedExpenseFormDialog({
dialogName,
payload: { projectId = null },
isOpen,
}) {
return (
<EstimateExpenseFormDialogRoot
name={dialogName}
title={<T id={'estimated_expenses.dialog.label'} />}
isOpen={isOpen}
autoFocus={true}
canEscapeKeyClose={true}
style={{ width: '400px' }}
>
<DialogSuspense>
<EstimatedExpenseFormDialogContent
dialogName={dialogName}
estimatedExpense={projectId}
/>
</DialogSuspense>
</EstimateExpenseFormDialogRoot>
);
}
export default compose(withDialogRedux())(EstimatedExpenseFormDialog);
const EstimateExpenseFormDialogRoot = 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;
}
`;