Compare commits

...

12 Commits

Author SHA1 Message Date
elforjani13
754618aa7a fix edit project. 2022-07-15 20:56:52 +02:00
elforjani13
50c905eabb feat: add empty status & fix edit project. 2022-07-15 20:46:04 +02:00
elforjani13
709e06a646 feat: add project api 2022-07-14 18:29:22 +02:00
elforjani13
8826d2bc5b fix: notes. 2022-07-10 17:41:50 +02:00
elforjani13
38a961b899 fix: additional notes. 2022-07-06 13:44:00 +02:00
elforjani13
7ef7e126e5 fix: rename time entry form to project time entry form. 2022-07-06 13:42:15 +02:00
elforjani13
bcf0ec25b8 fix: rename task form to project task form. 2022-07-06 13:41:17 +02:00
elforjani13
965a8966f6 fix: rename expense form to project expense form. 2022-07-06 13:39:33 +02:00
elforjani13
b030d6ea37 fix: add estimated & expense dialog. 2022-07-04 11:14:24 +02:00
elforjani13
31fef21362 feat: add purchases & sales tables. 2022-06-30 22:05:35 +02:00
elforjani13
6f2a456a56 feat: add expense form. 2022-06-30 22:03:50 +02:00
elforjani13
6134ad5598 feat: add estimated expense. 2022-06-30 21:59:12 +02:00
92 changed files with 2271 additions and 613 deletions

View File

@@ -1,7 +0,0 @@
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' },
];

View File

@@ -19,6 +19,8 @@ export const TABLES = {
WAREHOUSE_TRANSFERS: 'warehouse_transfers', WAREHOUSE_TRANSFERS: 'warehouse_transfers',
PROJECTS: 'projects', PROJECTS: 'projects',
TIMESHEETS: 'timesheets', TIMESHEETS: 'timesheets',
PURCHASES: 'purchases',
SALES: 'sales',
}; };
export const TABLE_SIZE = { export const TABLE_SIZE = {

View File

@@ -88,23 +88,27 @@ export default function TableHeader() {
}, },
} = useContext(TableContext); } = useContext(TableContext);
// Can't contiunue if the thead is disabled.
if (hideTableHeader) {
return null;
}
if (headerLoading && TableHeaderSkeletonRenderer) { if (headerLoading && TableHeaderSkeletonRenderer) {
return <TableHeaderSkeletonRenderer />; return <TableHeaderSkeletonRenderer />;
} }
return ( return (
!hideTableHeader && ( <ScrollSyncPane>
<ScrollSyncPane> <div className="thead">
<div className="thead"> <div className={'thead-inner'}>
<div className={'thead-inner'}> {headerGroups.map((headerGroup, index) => (
{headerGroups.map((headerGroup, index) => ( <TableHeaderGroup key={index} headerGroup={headerGroup} />
<TableHeaderGroup key={index} headerGroup={headerGroup} /> ))}
))} <If condition={progressBarLoading}>
<If condition={progressBarLoading}> <MaterialProgressBar />
<MaterialProgressBar /> </If>
</If>
</div>
</div> </div>
</ScrollSyncPane> </div>
) </ScrollSyncPane>
); );
} }

View File

@@ -41,8 +41,10 @@ import WarehouseActivateDialog from '../containers/Dialogs/WarehouseActivateDial
import CustomerOpeningBalanceDialog from '../containers/Dialogs/CustomerOpeningBalanceDialog'; import CustomerOpeningBalanceDialog from '../containers/Dialogs/CustomerOpeningBalanceDialog';
import VendorOpeningBalanceDialog from '../containers/Dialogs/VendorOpeningBalanceDialog'; import VendorOpeningBalanceDialog from '../containers/Dialogs/VendorOpeningBalanceDialog';
import ProjectFormDialog from '../containers/Projects/containers/ProjectFormDialog'; import ProjectFormDialog from '../containers/Projects/containers/ProjectFormDialog';
import TaskFormDialog from '../containers/Projects/containers/TaskFormDialog'; import ProjectTaskFormDialog from '../containers/Projects/containers/ProjectTaskFormDialog';
import TimeEntryFormDialog from '../containers/Projects/containers/TimeEntryFormDialog'; import ProjectTimeEntryFormDialog from '../containers/Projects/containers/ProjectTimeEntryFormDialog';
import ProjectExpenseForm from '../containers/Projects/containers/ProjectExpenseForm';
import EstimatedExpenseFormDialog from '../containers/Projects/containers/EstimatedExpenseFormDialog';
/** /**
* Dialogs container. * Dialogs container.
@@ -94,8 +96,10 @@ export default function DialogsContainer() {
<CustomerOpeningBalanceDialog dialogName={'customer-opening-balance'} /> <CustomerOpeningBalanceDialog dialogName={'customer-opening-balance'} />
<VendorOpeningBalanceDialog dialogName={'vendor-opening-balance'} /> <VendorOpeningBalanceDialog dialogName={'vendor-opening-balance'} />
<ProjectFormDialog dialogName={'project-form'} /> <ProjectFormDialog dialogName={'project-form'} />
<TaskFormDialog dialogName={'task-form'} /> <ProjectTaskFormDialog dialogName={'project-task-form'} />
<TimeEntryFormDialog dialogName={'time-entry-form'} /> <ProjectTimeEntryFormDialog dialogName={'project-time-entry-form'} />
<ProjectExpenseForm dialogName={'project-expense-form'} />
<EstimatedExpenseFormDialog dialogName={'estimated-expense-form'} />
</div> </div>
); );
} }

View File

@@ -23,6 +23,7 @@ import TransactionsLockingAlerts from '../TransactionsLocking/TransactionsLockin
import WarehousesAlerts from '../Preferences/Warehouses/WarehousesAlerts'; import WarehousesAlerts from '../Preferences/Warehouses/WarehousesAlerts';
import WarehousesTransfersAlerts from '../WarehouseTransfers/WarehousesTransfersAlerts'; import WarehousesTransfersAlerts from '../WarehouseTransfers/WarehousesTransfersAlerts';
import BranchesAlerts from '../Preferences/Branches/BranchesAlerts'; import BranchesAlerts from '../Preferences/Branches/BranchesAlerts';
import ProjectAlerts from '../../containers/Projects/containers/ProjectAlerts';
export default [ export default [
...AccountsAlerts, ...AccountsAlerts,
@@ -50,4 +51,5 @@ export default [
...WarehousesAlerts, ...WarehousesAlerts,
...WarehousesTransfersAlerts, ...WarehousesTransfersAlerts,
...BranchesAlerts, ...BranchesAlerts,
...ProjectAlerts,
]; ];

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { MenuItem, Button } from '@blueprintjs/core'; import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from 'components'; import { FSelect } from '../../../components';
/** /**
* *
@@ -8,7 +8,7 @@ import { FSelect } from 'components';
* @param {*} param1 * @param {*} param1
* @returns * @returns
*/ */
const taskModalChargeRenderer = (item, { handleClick, modifiers, query }) => { const chargeTypeItemRenderer = (item, { handleClick, modifiers, query }) => {
return ( return (
<MenuItem <MenuItem
label={item.label} label={item.label}
@@ -19,8 +19,8 @@ const taskModalChargeRenderer = (item, { handleClick, modifiers, query }) => {
); );
}; };
const taskModalChargeSelectProps = { const chargeTypeSelectProps = {
itemRenderer: taskModalChargeRenderer, itemRenderer: chargeTypeItemRenderer,
valueAccessor: 'value', valueAccessor: 'value',
labelAccessor: 'name', labelAccessor: 'name',
}; };
@@ -30,22 +30,21 @@ const taskModalChargeSelectProps = {
* @param param0 * @param param0
* @returns * @returns
*/ */
export function TaskModalChargeSelect({ items, ...rest }) { export function ChangeTypesSelect({ items, ...rest }) {
return ( return (
<FSelect <FSelect
{...taskModalChargeSelectProps} {...chargeTypeSelectProps}
{...rest} {...rest}
items={items} items={items}
input={TaskModalChargeSelectButton} input={ChargeTypeSelectButton}
/> />
); );
} }
/** /**
* *
* @param param0 * @param param0
* @returns * @returns
*/ */
function TaskModalChargeSelectButton({ label }) { function ChargeTypeSelectButton({ label }) {
return <Button text={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,20 @@
//@ts-nocheck
import React from 'react';
import { FInputGroup } from 'components';
import { useFormikContext } from 'formik';
export function FInputGroupComponent({ toField, ...props }) {
const { values, setFieldValue } = useFormikContext();
const { expenseQuantity, expenseUnitPrice } = values;
const total = expenseQuantity * expenseUnitPrice;
const handleBlur = () => {
setFieldValue(toField, total);
};
const inputGroupProps = {
onBlur: handleBlur,
...props,
};
return <FInputGroup {...inputGroupProps} />;
}

View File

@@ -11,7 +11,7 @@ import { FSelect } from 'components';
* @param {*} exactMatch * @param {*} exactMatch
* @returns * @returns
*/ */
const projectItemPredicate = (query, project, _index, exactMatch) => { const projectsItemPredicate = (query, project, _index, exactMatch) => {
const normalizedTitle = project.name.toLowerCase(); const normalizedTitle = project.name.toLowerCase();
const normalizedQuery = query.toLowerCase(); const normalizedQuery = query.toLowerCase();
@@ -28,7 +28,7 @@ const projectItemPredicate = (query, project, _index, exactMatch) => {
* @param {*} param1 * @param {*} param1
* @returns * @returns
*/ */
const projectItemRenderer = (project, { handleClick, modifiers, query }) => { const projectsItemRenderer = (project, { handleClick, modifiers, query }) => {
return ( return (
<MenuItem <MenuItem
active={modifiers.active} active={modifiers.active}
@@ -41,13 +41,13 @@ const projectItemRenderer = (project, { handleClick, modifiers, query }) => {
}; };
const projectSelectProps = { const projectSelectProps = {
itemPredicate: projectItemPredicate, itemPredicate: projectsItemPredicate,
itemRenderer: projectItemRenderer, itemRenderer: projectsItemRenderer,
valueAccessor: 'id', valueAccessor: 'id',
labelAccessor: 'name', labelAccessor: 'name',
}; };
export function ProjectSelect({ projects, ...rest }) { export function ProjectsSelect({ projects, ...rest }) {
return ( return (
<FSelect <FSelect
items={projects} items={projects}

View File

@@ -1,7 +1,8 @@
// @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core'; import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from '../../../../../components/Forms'; import { FSelect } from 'components';
/** /**
* *

View File

@@ -0,0 +1,5 @@
export * from './ExpenseSelect';
export * from './ChangeTypesSelect';
export * from './TaskSelect';
export * from './ProjectsSelect';
export * from './FInputGroupComponent';

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'),
),
expenseTotal: 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,115 @@
//@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, FInputGroupComponent } from '../../components';
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
import EstimatedExpenseFormChargeFields from './EstimatedExpenseFormChargeFields';
import { ChangeTypesSelect } from '../../components';
import { expenseChargeOption } from 'containers/Projects/containers/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'}
>
<FInputGroupComponent name="quantity" />
</FFormGroup>
<MetaLineLabel>
<T id={'estimated_expenses.dialog.cost_to_you'} />
</MetaLineLabel>
{/*------------ Unit Price -----------*/}
<ControlGroup className={Classes.FILL}>
<FFormGroup
name={'unitPrice'}
label={intl.get('estimated_expenses.dialog.unit_price')}
>
<FInputGroupComponent name="unitPrice" />
</FFormGroup>
<FFormGroup
name={'unitPrice'}
label={intl.get('estimated_expenses.dialog.total')}
>
<FInputGroup name="expenseTotal" />
</FFormGroup>
</ControlGroup>
<MetaLineLabel>
<T id={'estimated_expenses.dialog.what_you_ll_charge'} />
</MetaLineLabel>
{/*------------ Charge -----------*/}
<FFormGroup
name={'charge'}
label={<T id={'estimated_expenses.dialog.charge'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<ChangeTypesSelect
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;
}
`;

View File

@@ -0,0 +1,79 @@
//@ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { FormattedMessage as T, FormattedHTMLMessage } from 'components';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import { useDeleteProject } from '../../hooks';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import { compose } from 'utils';
/**
* Project delete alert.
*/
function ProjectDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { projectId },
// #withAlertActions
closeAlert,
// #withDrawerActions
closeDrawer,
}) {
const { mutateAsync: deleteProjectMutate, isLoading } = useDeleteProject();
// handle cancel delete project alert.
const handleCancelDeleteAlert = () => {
closeAlert(name);
};
// handleConfirm delete project
const handleConfirmProjectDelete = () => {
deleteProjectMutate(projectId)
.then(() => {
AppToaster.show({
message: intl.get('projects.alert.delete_message'),
intent: Intent.SUCCESS,
});
})
.catch(
({
response: {
data: { errors },
},
}) => {},
)
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelDeleteAlert}
onConfirm={handleConfirmProjectDelete}
loading={isLoading}
>
<p>
<FormattedHTMLMessage id={'projects.alert.once_delete_this_project'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
)(ProjectDeleteAlert);

View File

@@ -0,0 +1,8 @@
import React from 'react';
const ProjectDeleteAlert = React.lazy(() => import('./ProjectDeleteAlert'));
/**
* Project alerts.
*/
export default [{ name: 'project-delete', component: ProjectDeleteAlert }];

View File

@@ -14,10 +14,11 @@ import {
FormattedMessage as T, FormattedMessage as T,
DashboardRowsHeightButton, DashboardRowsHeightButton,
} from 'components'; } from 'components';
import { TransactionSelect } from './components'; import { ProjectTransactionsSelect } from './components';
import withSettings from '../../../Settings/withSettings'; import withSettings from '../../../Settings/withSettings';
import withSettingsActions from '../../../Settings/withSettingsActions'; import withSettingsActions from '../../../Settings/withSettingsActions';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import { projectTranslations } from './common';
import { useProjectDetailContext } from './ProjectDetailProvider'; import { useProjectDetailContext } from './ProjectDetailProvider';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -40,7 +41,13 @@ function ProjectDetailActionsBar({
// Handle new transaction button click. // Handle new transaction button click.
const handleNewTransactionBtnClick = ({ path }) => { const handleNewTransactionBtnClick = ({ path }) => {
history.push(`/${path}`); switch (path) {
case 'expense':
openDialog('project-expense-form', { projectId });
break;
case 'estimated_expense':
openDialog('estimated-expense-form', { projectId });
}
}; };
const handleEditProjectBtnClick = () => { const handleEditProjectBtnClick = () => {
@@ -50,11 +57,13 @@ function ProjectDetailActionsBar({
}; };
// Handle table row size change. // Handle table row size change.
const handleTableRowSizeChange = (size) => { const handleTableRowSizeChange = (size) => {
addSetting('timesheets', 'tableSize', size); addSetting('timesheets', 'tableSize', size) &&
addSetting('sales', 'tableSize', size) &&
addSetting('purchases', 'tableSize', size);
}; };
const handleTimeEntryBtnClick = () => { const handleTimeEntryBtnClick = () => {
openDialog('time-entry-form', { openDialog('project-time-entry-form', {
projectId, projectId,
}); });
}; };
@@ -65,11 +74,8 @@ function ProjectDetailActionsBar({
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
<TransactionSelect <ProjectTransactionsSelect
transactions={[ transactions={projectTranslations}
{ name: 'Invoice', path: 'invoices/new' },
{ name: 'Expenses', path: 'expenses/new' },
]}
onItemSelect={handleNewTransactionBtnClick} onItemSelect={handleNewTransactionBtnClick}
/> />
<Button <Button
@@ -105,8 +111,6 @@ function ProjectDetailActionsBar({
initialValue={timesheetsTableSize} initialValue={timesheetsTableSize}
onChange={handleTableRowSizeChange} onChange={handleTableRowSizeChange}
/> />
<NavbarDivider />
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
</NavbarGroup> </NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}> <NavbarGroup align={Alignment.RIGHT}>
<Button <Button

View File

@@ -2,8 +2,9 @@ import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Tabs, Tab } from '@blueprintjs/core'; import { Tabs, Tab } from '@blueprintjs/core';
import ProjectTimeSheets from './ProjectTimeSheets';
import ProjectTimesheet from './ProjectTimesheet'; import ProjectPurchasesTable from './ProjectPurchasesTable';
import ProjectSalesTable from './ProjectSalesTable';
/** /**
* Project detail tabs. * Project detail tabs.
@@ -16,19 +17,24 @@ export default function ProjectDetailTabs() {
animate={true} animate={true}
large={true} large={true}
renderActiveTabPanelOnly={true} renderActiveTabPanelOnly={true}
defaultSelectedTabId={'timesheet'} defaultSelectedTabId={'purchases'}
> >
<Tab id="overview" title={intl.get('project_details.label.overview')} /> <Tab id="overview" title={intl.get('project_details.label.overview')} />
<Tab <Tab
id="timesheet" id="timesheet"
title={intl.get('project_details.label.timesheet')} title={intl.get('project_details.label.timesheet')}
panel={<ProjectTimesheet />} panel={<ProjectTimeSheets />}
/> />
<Tab <Tab
id="purchases" id="purchases"
title={intl.get('project_details.label.purchases')} title={intl.get('project_details.label.purchases')}
panel={<ProjectPurchasesTable />}
/>
<Tab
id="sales"
title={intl.get('project_details.label.sales')}
panel={<ProjectSalesTable />}
/> />
<Tab id="sales" title={intl.get('project_details.label.sales')} />
<Tab id="journals" title={intl.get('project_details.label.journals')} /> <Tab id="journals" title={intl.get('project_details.label.journals')} />
</Tabs> </Tabs>
</ProjectTabsContent> </ProjectTabsContent>
@@ -42,6 +48,10 @@ const ProjectTabsContent = styled.div`
background-color: #fff; background-color: #fff;
border-bottom: 1px solid #d2dce2; border-bottom: 1px solid #d2dce2;
> *:not(:last-child) {
margin-right: 0;
}
&.bp3-large > .bp3-tab { &.bp3-large > .bp3-tab {
font-size: 15px; font-size: 15px;
font-weight: 400; font-weight: 400;
@@ -59,9 +69,11 @@ const ProjectTabsContent = styled.div`
} }
} }
.bp3-tab-panel { .bp3-tab-panel {
margin-top: 20px; /* margin: 20px 32px; */
/* margin: 20px; */
/* margin-top: 20px;
margin-bottom: 20px; margin-bottom: 20px;
padding: 0 25px; padding: 0 25px; */
} }
} }
`; `;

View File

@@ -0,0 +1,57 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { DataTable } from 'components';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { TABLES } from 'common/tables';
import { useMemorizedColumnsWidths } from 'hooks';
import { ActionMenu } from './components';
import { useProjectPurchasesColumns } from './hooks';
import withSettings from '../../../../Settings/withSettings';
import { compose } from 'utils';
/**
* Project Purchases DataTable.
* @returns
*/
function ProjectPurchasesTableRoot({
// #withSettings
purchasesTableSize,
}) {
// Retrieve purchases table columns.
const columns = useProjectPurchasesColumns();
// Handle delete purchase.
const handleDeletePurchase = () => {};
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.PURCHASES);
return (
<DataTable
columns={columns}
data={[]}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
ContextMenu={ActionMenu}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={purchasesTableSize}
payload={{
onDelete: handleDeletePurchase,
}}
/>
);
}
export const ProjectPurchasesTable = compose(
withSettings(({ purchasesSettings }) => ({
purchasesTableSize: purchasesSettings?.tableSize,
})),
)(ProjectPurchasesTableRoot);

View File

@@ -0,0 +1,21 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Icon } from 'components';
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
import { safeCallback } from 'utils';
/**
* Table actions cell.
*/
export function ActionMenu({ payload: { onDelete }, row: { original } }) {
return (
<Menu>
<MenuItem
text={intl.get('purchases.action.delete')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}

View File

@@ -0,0 +1,73 @@
import { useMemo } from 'react';
import intl from 'react-intl-universal';
import clsx from 'classnames';
import { CLASSES } from 'common/classes';
import { FormatDateCell } from 'components';
export function useProjectPurchasesColumns() {
return useMemo(
() => [
{
id: 'date',
Header: intl.get('purchases.column.date'),
accessor: 'date',
Cell: FormatDateCell,
width: 120,
className: 'date',
clickable: true,
textOverview: true,
},
{
id: 'type',
Header: intl.get('purchases.column.type'),
accessor: 'type',
width: 120,
className: 'type',
clickable: true,
textOverview: true,
},
{
id: 'transaction_no',
Header: intl.get('purchases.column.transaction_no'),
accessor: 'transaction_no',
width: 120,
},
{
id: 'due_date',
Header: intl.get('purchases.column.due_date'),
accessor: 'due_date',
Cell: FormatDateCell,
width: 120,
className: 'due_date',
clickable: true,
textOverview: true,
},
{
id: 'balance',
Header: intl.get('purchases.column.balance'),
accessor: 'balance',
width: 120,
clickable: true,
align: 'right',
className: clsx(CLASSES.FONT_BOLD),
},
{
id: 'total',
Header: intl.get('purchases.column.total'),
accessor: 'total',
align: 'right',
width: 120,
className: clsx(CLASSES.FONT_BOLD),
},
{
id: 'status',
Header: intl.get('purchases.column.status'),
accessor: 'status',
width: 120,
className: 'status',
clickable: true,
},
],
[],
);
}

View File

@@ -0,0 +1,16 @@
import React from 'react';
import styled from 'styled-components';
import { ProjectPurchasesTable } from './ProjectPurchasesTable';
import { DashboardContentTable } from 'components';
/**
*
* @returns
*/
export default function ProjectPurchasesTableRoot() {
return (
<DashboardContentTable>
<ProjectPurchasesTable />
</DashboardContentTable>
);
}

View File

@@ -0,0 +1,56 @@
//@ts-nocheck
import React from 'react';
import { DataTable } from 'components';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { TABLES } from 'common/tables';
import { useMemorizedColumnsWidths } from 'hooks';
import { ActionMenu } from './components';
import { useProjectSalesColumns } from './hooks';
import withSettings from '../../../../Settings/withSettings';
import { compose } from 'utils';
/**
* Porject sales datatable.
* @returns
*/
function ProjectSalesTableRoot({
// #withSettings
salesTableSize,
}) {
// Retrieve project sales table columns.
const columns = useProjectSalesColumns();
// Handle delete sale.
const handleDeleteSale = () => {};
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.SALES);
return (
<DataTable
columns={columns}
data={[]}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
ContextMenu={ActionMenu}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={salesTableSize}
payload={{
onDelete: handleDeleteSale,
}}
/>
);
}
export const ProjectSalesTable = compose(
withSettings(({ salesSettings }) => ({
salesTableSize: salesSettings?.tableSize,
})),
)(ProjectSalesTableRoot);

View File

@@ -0,0 +1,22 @@
import React from 'react';
import intl from 'react-intl-universal';
import { FormatDateCell, Icon, FormattedMessage as T } from 'components';
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
import { safeCallback } from 'utils';
/**
* Table actions cell.
*/
export function ActionMenu({ payload: { onDelete }, row: { original } }) {
return (
<Menu>
<MenuItem
text={intl.get('sales.action.delete')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}

View File

@@ -0,0 +1,73 @@
import { useMemo } from 'react';
import intl from 'react-intl-universal';
import clsx from 'classnames';
import { CLASSES } from 'common/classes';
import { FormatDateCell } from 'components';
export function useProjectSalesColumns() {
return useMemo(
() => [
{
id: 'date',
Header: intl.get('sales.column.date'),
accessor: 'date',
Cell: FormatDateCell,
width: 120,
className: 'date',
clickable: true,
textOverview: true,
},
{
id: 'type',
Header: intl.get('sales.column.type'),
accessor: 'type',
width: 120,
className: 'type',
clickable: true,
textOverview: true,
},
{
id: 'transaction_no',
Header: intl.get('sales.column.transaction_no'),
accessor: 'transaction_no',
width: 120,
},
{
id: 'due_date',
Header: intl.get('sales.column.due_date'),
accessor: 'due_date',
Cell: FormatDateCell,
width: 120,
className: 'due_date',
clickable: true,
textOverview: true,
},
{
id: 'balance',
Header: intl.get('sales.column.balance'),
accessor: 'balance',
width: 120,
clickable: true,
align: 'right',
className: clsx(CLASSES.FONT_BOLD),
},
{
id: 'total',
Header: intl.get('sales.column.total'),
accessor: 'total',
align: 'right',
width: 120,
className: clsx(CLASSES.FONT_BOLD),
},
{
id: 'status',
Header: intl.get('sales.column.status'),
accessor: 'status',
width: 120,
className: 'status',
clickable: true,
},
],
[],
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import styled from 'styled-components';
import { ProjectSalesTable } from './ProjectSalesTable';
import { DashboardContentTable } from 'components';
/**
* Project Sales Table.
* @returns
*/
export default function ProjectSalesTableRoot() {
return (
<ProjectSalesContentTable>
<ProjectSalesTable />
</ProjectSalesContentTable>
);
}
const ProjectSalesContentTable = styled(DashboardContentTable)``;

View File

@@ -13,10 +13,10 @@ import {
import { calculateStatus } from 'utils'; import { calculateStatus } from 'utils';
/** /**
* Timesheets header * Project Timesheets header
* @returns * @returns
*/ */
export default function TimesheetsHeader() { export function ProjectTimesheetsHeader() {
return ( return (
<DetailFinancialSection> <DetailFinancialSection>
<DetailFinancialCard label={'Project estimate'} value={'3.14'} /> <DetailFinancialCard label={'Project estimate'} value={'3.14'} />

View File

@@ -4,42 +4,25 @@ import styled from 'styled-components';
import { DataTable } from 'components'; import { DataTable } from 'components';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows'; import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton'; import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { useTimesheetColumns, ActionsMenu } from './components'; import { ActionsMenu } from './components';
import { useProjectTimesheetColumns } from './hooks';
import { TABLES } from 'common/tables'; import { TABLES } from 'common/tables';
import { useMemorizedColumnsWidths } from 'hooks'; import { useMemorizedColumnsWidths } from 'hooks';
import withSettings from '../../../../Settings/withSettings'; import withSettings from '../../../../Settings/withSettings';
import { compose } from 'utils'; import { compose } from 'utils';
const Timesheet = [
{
id: 1,
date: '2022-06-08T22:00:00.000Z',
name: 'Lighting',
display_name: 'Kyrie Rearden',
description: 'Laid paving stones',
duration: '12:00',
},
{
id: 2,
date: '2022-06-08T22:00:00.000Z',
name: 'Interior Decoration',
display_name: 'Project Sherwood',
description: 'Laid paving stones',
duration: '11:00',
},
];
/** /**
* Timesheet DataTable. * Timesheet DataTable.
* @returns * @returns
*/ */
function TimesheetsTable({ function ProjectTimesheetsTableRoot({
// #withSettings // #withSettings
timesheetsTableSize, timesheetsTableSize,
}) { }) {
// Retrieve timesheet table columns. // Retrieve project timesheet table columns.
const columns = useTimesheetColumns(); const columns = useProjectTimesheetColumns();
// Handle delete timesheet. // Handle delete timesheet.
const handleDeleteTimesheet = () => {}; const handleDeleteTimesheet = () => {};
@@ -49,12 +32,9 @@ function TimesheetsTable({
useMemorizedColumnsWidths(TABLES.TIMESHEETS); useMemorizedColumnsWidths(TABLES.TIMESHEETS);
return ( return (
<TimesheetDataTable <ProjectTimesheetDataTable
columns={columns} columns={columns}
data={Timesheet} data={[]}
// loading={}
// headerLoading={}
// progressBarLoading={}
manualSortBy={true} manualSortBy={true}
noInitialFetch={true} noInitialFetch={true}
sticky={true} sticky={true}
@@ -71,13 +51,13 @@ function TimesheetsTable({
/> />
); );
} }
export default compose( export const ProjectTimesheetsTable = compose(
withSettings(({ timesheetsSettings }) => ({ withSettings(({ timesheetsSettings }) => ({
timesheetsTableSize: timesheetsSettings?.tableSize, timesheetsTableSize: timesheetsSettings?.tableSize,
})), })),
)(TimesheetsTable); )(ProjectTimesheetsTableRoot);
const TimesheetDataTable = styled(DataTable)` const ProjectTimesheetDataTable = styled(DataTable)`
.table { .table {
.thead .tr .th { .thead .tr .th {
.resizer { .resizer {
@@ -87,38 +67,38 @@ const TimesheetDataTable = styled(DataTable)`
.tbody { .tbody {
.tr .td { .tr .td {
padding: 0.5rem 0.8rem;
} }
.avatar.td { .avatar.td {
.avatar { .cell-inner {
display: inline-block; .avatar {
background: #adbcc9; display: inline-block;
border-radius: 50%; background: #adbcc9;
text-align: center; border-radius: 50%;
font-weight: 400; text-align: center;
color: #fff; font-weight: 400;
color: #fff;
&[data-size='medium'] { &[data-size='medium'] {
height: 30px; height: 30px;
width: 30px; width: 30px;
line-height: 30px; line-height: 30px;
font-size: 14px; font-size: 14px;
} }
&[data-size='small'] { &[data-size='small'] {
height: 25px; height: 25px;
width: 25px; width: 25px;
line-height: 25px; line-height: 25px;
font-size: 12px; font-size: 12px;
}
} }
} }
} }
} }
}
.table-size--small { .table-size--small {
.tbody .tr { .tbody .tr {
height: 45px; height: 45px;
}
} }
} }
`; `;

View File

@@ -1,15 +1,13 @@
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import styled from 'styled-components'; import styled from 'styled-components';
import { FormatDate, Icon, FormattedMessage as T } from 'components'; import { FormatDate, Icon } from 'components';
import { Menu, MenuItem, Intent } from '@blueprintjs/core'; import { Menu, MenuItem, Intent } from '@blueprintjs/core';
import { safeCallback, firstLettersArgs } from 'utils'; import { safeCallback, firstLettersArgs } from 'utils';
import { chain } from 'lodash';
/** /**
* Table actions cell. * Table actions cell.
*/ */
export function ActionsMenu({ export function ActionsMenu({
payload: { onDelete, onViewDetails }, payload: { onDelete, onViewDetails },
row: { original }, row: { original },
@@ -78,43 +76,3 @@ const TimesheetDescription = styled.span`
margin: 0.3rem; margin: 0.3rem;
} }
`; `;
/**
* Retrieve timesheet list columns columns.
*/
export function useTimesheetColumns() {
return React.useMemo(
() => [
{
id: 'avatar',
Header: '',
Cell: AvatarCell,
className: 'avatar',
width: 45,
disableResizing: true,
disableSortBy: true,
clickable: true,
},
{
id: 'name',
Header: 'Header',
accessor: TimesheetAccessor,
width: 100,
className: 'name',
clickable: true,
textOverview: true,
},
{
id: 'duration',
Header: '',
accessor: 'duration',
width: 100,
className: 'duration',
align: 'right',
clickable: true,
textOverview: true,
},
],
[],
);
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import intl from 'react-intl-universal';
import { AvatarCell, TimesheetAccessor } from './components';
/**
* Retrieve project timesheet list columns.
*/
export function useProjectTimesheetColumns() {
return React.useMemo(
() => [
{
id: 'avatar',
Header: '',
Cell: AvatarCell,
className: 'avatar',
width: 45,
disableResizing: true,
disableSortBy: true,
clickable: true,
},
{
id: 'name',
Header: 'Header',
accessor: TimesheetAccessor,
width: 100,
className: 'name',
clickable: true,
textOverview: true,
},
{
id: 'duration',
Header: '',
accessor: 'duration',
width: 100,
className: 'duration',
align: 'right',
clickable: true,
textOverview: true,
},
],
[],
);
}

View File

@@ -0,0 +1,27 @@
import React from 'react';
import styled from 'styled-components';
import { ProjectTimesheetsTable } from './ProjectTimesheetsTable';
import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader';
/**
* Project Timesheets.
* @returns
*/
export default function ProjectTimeSheets() {
return (
<React.Fragment>
<ProjectTimesheetsHeader />
<ProjectTimesheetTableCard>
<ProjectTimesheetsTable />
</ProjectTimesheetTableCard>
</React.Fragment>
);
}
const ProjectTimesheetTableCard = styled.div`
margin: 22px 32px;
border: 1px solid #c8cad0;
border-radius: 3px;
background: #fff;
`;

View File

@@ -1,27 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import TimesheetsTable from './TimesheetsTable';
import TimesheetsHeader from './TimesheetsHeader';
/**
* Project Timesheet.
* @returns
*/
export default function ProjectTimesheet() {
return (
<React.Fragment>
<TimesheetsHeader />
<ProjectTimesheetTableCard>
<TimesheetsTable />
</ProjectTimesheetTableCard>
</React.Fragment>
);
}
const ProjectTimesheetTableCard = styled.div`
margin: 20px;
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
border-radius: 3px;
background: #fff;
`;

View File

@@ -0,0 +1,9 @@
import intl from 'react-intl-universal';
export const projectTranslations = [
{ name: intl.get('project_details.new_expenses'), path: 'expense' },
{
name: intl.get('project_details.new_estimated_expenses'),
path: 'estimated_expense',
},
];

View File

@@ -29,17 +29,16 @@ export const FinancialProgressBar = ({ ...rest }) => {
const FinancialSectionWrap = styled.div` const FinancialSectionWrap = styled.div`
display: flex; display: flex;
flex-wrap: wrap; margin: 22px 32px;
margin: 20px 20px 20px;
gap: 10px; gap: 10px;
`; `;
const FinancialSectionCard = styled.div` const FinancialSectionCard = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-shrink: 0; flex-shrink: 1;
border-radius: 3px; border-radius: 3px;
width: 220px; width: 230px;
height: 116px; height: 116px;
background-color: #fff; background-color: #fff;
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0 border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
@@ -47,7 +46,6 @@ const FinancialSectionCard = styled.div`
const FinancialSectionCardContent = styled.div` const FinancialSectionCardContent = styled.div`
margin: 16px; margin: 16px;
/* flex-direction: column; */
`; `;
const FinancialCardWrap = styled.div``; const FinancialCardWrap = styled.div``;

View File

@@ -15,7 +15,7 @@ import { Icon, FormattedMessage as T } from 'components';
* @param {*} param1 * @param {*} param1
* @returns * @returns
*/ */
const transactionItemRenderer = ( const projectTransactionItemRenderer = (
transaction, transaction,
{ handleClick, modifiers, query }, { handleClick, modifiers, query },
) => { ) => {
@@ -29,8 +29,8 @@ const transactionItemRenderer = (
); );
}; };
const transactionSelectProps = { const projectTransactionSelectProps = {
itemRenderer: transactionItemRenderer, itemRenderer: projectTransactionItemRenderer,
filterable: false, filterable: false,
popoverProps: { popoverProps: {
minimal: true, minimal: true,
@@ -43,13 +43,13 @@ const transactionSelectProps = {
}; };
/** /**
* * Project transactions select
* @param * @param
* @returns * @returns
*/ */
export function TransactionSelect({ transactions, ...rest }) { export function ProjectTransactionsSelect({ transactions, ...rest }) {
return ( return (
<Select {...transactionSelectProps} items={transactions} {...rest}> <Select {...projectTransactionSelectProps} items={transactions} {...rest}>
<Button <Button
minimal={true} minimal={true}
icon={<Icon icon={'plus'} />} icon={<Icon icon={'plus'} />}

View File

@@ -1,3 +1,2 @@
export * from './ProjectSelect'; export * from './ProjectTransactionsSelect';
export * from './TransactionSelect';
export * from './FinancialSection'; export * from './FinancialSection';

View File

@@ -1,5 +1,5 @@
//@ts-nocheck //@ts-nocheck
import React from 'react'; import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import ProjectDetailActionsBar from './ProjectDetailActionsBar'; import ProjectDetailActionsBar from './ProjectDetailActionsBar';
import ProjectDetailTabs from './ProjectDetailTabs'; import ProjectDetailTabs from './ProjectDetailTabs';
@@ -20,7 +20,7 @@ function ProjectTabs({
state: { projectName, projectId }, state: { projectName, projectId },
} = useLocation(); } = useLocation();
React.useEffect(() => { useEffect(() => {
changePageTitle(projectName); changePageTitle(projectName);
}, [changePageTitle, projectName]); }, [changePageTitle, projectName]);

View File

@@ -0,0 +1,23 @@
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('project_expense.schema.label.expense_name'),
),
estimatedExpense: Yup.number().label(
intl.get('project_expense.schema.label.estimated_expense'),
),
expemseDate: Yup.date(),
expenseQuantity: Yup.number().label(
intl.get('project_expense.schema.label.quantity'),
),
expenseUnitPrice: Yup.number().label(
intl.get('project_expense.schema.label.unitPrice'),
),
expenseTotal: Yup.number(),
expenseCharge: Yup.string(),
});
export const CreateProjectExpenseFormSchema = 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 { CreateProjectExpenseFormSchema } from './ProjectExpenseForm.schema';
import ProjectExpenseFormContent from './ProjectExpenseFormContent';
import { useProjectExpenseFormContext } from './ProjectExpenseFormProvider';
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: '',
};
/**
* Project expense form.
* @returns
*/
function ProjectExpenseForm({
//#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={CreateProjectExpenseFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
component={ProjectExpenseFormContent}
/>
);
}
export default compose(withDialogActions)(ProjectExpenseForm);

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 ProjectExpenseFormFields from './ProjectExpenseFormFields';
import ProjectExpneseFormFloatingActions from './ProjectExpneseFormFloatingActions';
/**
* Expense form content.
* @returns
*/
export default function ProjectExpenseFormContent() {
return (
<Form>
<ProjectExpenseFormFields />
<ProjectExpneseFormFloatingActions />
</Form>
);
}

View File

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

View File

@@ -0,0 +1,145 @@
//@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, FInputGroupComponent } from '../../components';
import ExpenseFormChargeFields from './ProjectExpenseFormChargeFields';
import { momentFormatter } from 'utils';
import { useProjectExpenseFormContext } from './ProjectExpenseFormProvider';
import { ChangeTypesSelect } from '../../components';
import { expenseChargeOption } from 'containers/Projects/containers/common/modalChargeOptions';
/**
* Project expense form fields.
* @returns
*/
export default function ProjectExpenseFormFields() {
return (
<div className={Classes.DIALOG_BODY}>
{/*------------ Expense Name -----------*/}
<FFormGroup
label={intl.get('project_expense.dialog.expense_name')}
name={'expenseName'}
>
<FInputGroup name="expenseName" />
</FFormGroup>
{/*------------ Track to Expense -----------*/}
<FFormGroup
name={'estimatedExpense'}
label={intl.get('project_expense.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('project_expense.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('project_expense.dialog.quantity')}
name={'expenseQuantity'}
>
<FInputGroupComponent name="expenseQuantity" />
</FFormGroup>
<MetaLineLabel>
<T id={'project_expense.dialog.cost_to_you'} />
</MetaLineLabel>
{/*------------ Unit Price -----------*/}
<ControlGroup className={Classes.FILL}>
<FFormGroup
name={'unitPrice'}
label={intl.get('project_expense.dialog.unit_price')}
>
<FInputGroupComponent name="expenseUnitPrice" />
</FFormGroup>
<FFormGroup
name={'expenseTotal'}
label={intl.get('project_expense.dialog.expense_total')}
>
<FInputGroup name="expenseTotal" />
</FFormGroup>
</ControlGroup>
<MetaLineLabel>
<T id={'project_expense.dialog.what_you_ll_charge'} />
</MetaLineLabel>
{/*------------ Charge -----------*/}
<FFormGroup
name={'expenseCharge'}
label={<T id={'project_expense.dialog.charge'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<ChangeTypesSelect
name="expenseCharge"
items={expenseChargeOption}
popoverProps={{ minimal: true }}
filterable={false}
/>
</FFormGroup>
{/*------------ Charge Fields -----------*/}
<ExpenseFormChargeFields />
{/*------------ Total -----------*/}
<ExpenseTotalBase>
<ExpenseTotalLabel>
<T id={'project_expense.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 ProjectExpenseFormContext = React.createContext();
/**
* Project expense form provider.
* @returns
*/
function ProjectExpenseFormProvider({
//#OwnProps
dialogName,
expenseId,
...props
}) {
// state provider.
const provider = {
dialogName,
};
return (
<DialogContent>
<ProjectExpenseFormContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useProjectExpenseFormContext = () => React.useContext(ProjectExpenseFormContext);
export { ProjectExpenseFormProvider, useProjectExpenseFormContext };

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 { useProjectExpenseFormContext } from './ProjectExpenseFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
function ProjectExpneseFormFloatingActions({
// #withDialogActions
closeDialog,
}) {
// Formik context.
const { isSubmitting } = useFormikContext();
// expense form dialog context.
const { dialogName } = useProjectExpenseFormContext();
// 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)(ProjectExpneseFormFloatingActions);

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

View File

@@ -1,19 +1,18 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({ const Schema = Yup.object().shape({
contact: Yup.string().label(intl.get('project.schema.label.contact')), contact_id: Yup.string().label(intl.get('project.schema.label.contact')),
projectName: Yup.string() name: Yup.string()
.label(intl.get('project.schema.label.project_name')) .label(intl.get('project.schema.label.project_name'))
.required(), .required(),
projectDeadline: Yup.date() deadline: Yup.date()
.label(intl.get('project.schema.label.deadline')) .label(intl.get('project.schema.label.deadline'))
.required(), .required(),
projectState: Yup.boolean().label( published: Yup.boolean().label(
intl.get('project.schema.label.project_state'), intl.get('project.schema.label.project_state'),
), ),
projectCost: Yup.number().label( cost_estimate: Yup.number().label(
intl.get('project.schema.label.project_cost'), intl.get('project.schema.label.project_cost'),
), ),
}); });

View File

@@ -3,20 +3,21 @@ import React from 'react';
import moment from 'moment'; import moment from 'moment';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components'; import { AppToaster } from 'components';
import ProjectFormContent from './ProjectFormContent'; import ProjectFormContent from './ProjectFormContent';
import { CreateProjectFormSchema } from './ProjectForm.schema'; import { CreateProjectFormSchema } from './ProjectForm.schema';
import { useProjectFormContext } from './ProjectFormProvider'; import { useProjectFormContext } from './ProjectFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils'; import { compose, transformToForm } from 'utils';
const defaultInitialValues = { const defaultInitialValues = {
contact: '', contact_id: '',
projectName: '', name: '',
projectDeadline: moment(new Date()).format('YYYY-MM-DD'), deadline: moment(new Date()).format('YYYY-MM-DD'),
projectState: false, published: false,
projectCost: '', cost_estimate: '',
}; };
/** /**
@@ -28,20 +29,37 @@ function ProjectForm({
closeDialog, closeDialog,
}) { }) {
// project form dialog context. // project form dialog context.
const { dialogName } = useProjectFormContext(); const {
dialogName,
project,
isNewMode,
projectId,
createProjectMutate,
editProjectMutate,
} = useProjectFormContext();
// Initial form values // Initial form values
const initialValues = { const initialValues = {
...defaultInitialValues, ...defaultInitialValues,
...transformToForm(project, defaultInitialValues),
}; };
// Handles the form submit. // Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => { const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {}; setSubmitting(true);
const form = { ...values };
// Handle request response success. // Handle request response success.
const onSuccess = (response) => { const onSuccess = (response) => {
AppToaster.show({}); AppToaster.show({
message: intl.get(
isNewMode
? 'projects.dialog.success_message'
: 'projects.dialog.edit_success_message',
),
intent: Intent.SUCCESS,
});
closeDialog(dialogName); closeDialog(dialogName);
}; };
@@ -53,6 +71,12 @@ function ProjectForm({
}) => { }) => {
setSubmitting(false); setSubmitting(false);
}; };
if (isNewMode) {
createProjectMutate(form).then(onSuccess).catch(onError);
} else {
editProjectMutate([projectId, form]).then(onSuccess).catch(onError);
}
}; };
return ( return (

View File

@@ -40,7 +40,7 @@ function ProjectFormFields() {
return ( return (
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
{/*------------ Contact -----------*/} {/*------------ Contact -----------*/}
<FastField name={'contact'}> <FastField name={'contact_id'}>
{({ form, field: { value }, meta: { error, touched } }) => ( {({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup <FormGroup
label={intl.get('projects.dialog.contact')} label={intl.get('projects.dialog.contact')}
@@ -53,7 +53,7 @@ function ProjectFormFields() {
selectedContactId={value} selectedContactId={value}
defaultSelectText={'Select Contact Account'} defaultSelectText={'Select Contact Account'}
onContactSelected={(customer) => { onContactSelected={(customer) => {
form.setFieldValue('contact', customer.id); form.setFieldValue('contact_id', customer.id);
}} }}
allowCreate={true} allowCreate={true}
popoverFill={true} popoverFill={true}
@@ -64,19 +64,20 @@ function ProjectFormFields() {
{/*------------ Project Name -----------*/} {/*------------ Project Name -----------*/}
<FFormGroup <FFormGroup
label={intl.get('projects.dialog.project_name')} label={intl.get('projects.dialog.project_name')}
name={'projectName'} name={'name'}
labelInfo={<FieldRequiredHint />}
> >
<FInputGroup name="projectName" /> <FInputGroup name="name" />
</FFormGroup> </FFormGroup>
{/*------------ DeadLine -----------*/} {/*------------ DeadLine -----------*/}
<FFormGroup <FFormGroup
label={intl.get('projects.dialog.deadline')} label={intl.get('projects.dialog.deadline')}
name={'projectDeadline'} name={'deadline'}
className={classNames(CLASSES.FILL, 'form-group--date')} className={classNames(CLASSES.FILL, 'form-group--date')}
> >
<FDateInput <FDateInput
{...momentFormatter('YYYY/MM/DD')} {...momentFormatter('YYYY/MM/DD')}
name="projectDeadline" name="deadline"
formatDate={(date) => date.toLocaleString()} formatDate={(date) => date.toLocaleString()}
popoverProps={{ popoverProps={{
position: Position.BOTTOM, position: Position.BOTTOM,
@@ -86,22 +87,23 @@ function ProjectFormFields() {
</FFormGroup> </FFormGroup>
{/*------------ CheckBox -----------*/} {/*------------ CheckBox -----------*/}
<FFormGroup name={'projectState'}> <FFormGroup name={'published'}>
<FCheckbox <FCheckbox
name="projectState" name="published"
label={intl.get('projects.dialog.calculator_expenses')} label={intl.get('projects.dialog.calculator_expenses')}
/> />
</FFormGroup> </FFormGroup>
{/*------------ Cost Estimate -----------*/} {/*------------ Cost Estimate -----------*/}
<FFormGroup <FFormGroup
name={'projectCost'} name={'cost_estimate'}
label={intl.get('projects.dialog.cost_estimate')} label={intl.get('projects.dialog.cost_estimate')}
labelInfo={<FieldRequiredHint />}
> >
<ControlGroup> <ControlGroup>
<InputPrependText text={'USD'} /> <InputPrependText text={'USD'} />
<FMoneyInputGroup <FMoneyInputGroup
disabled={values.projectState} disabled={values.published}
name={'project_cost'} name={'cost_estimate'}
allowDecimals={true} allowDecimals={true}
allowNegativeValue={true} allowNegativeValue={true}
/> />

View File

@@ -15,12 +15,12 @@ function ProjectFormFloatingActions({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
// project form dialog context.
const { dialogName } = useProjectFormContext();
// Formik context. // Formik context.
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
// project form dialog context.
const { dialogName } = useProjectFormContext();
// Handle close button click. // Handle close button click.
const handleCancelBtnClick = () => { const handleCancelBtnClick = () => {
closeDialog(dialogName); closeDialog(dialogName);
@@ -29,7 +29,7 @@ function ProjectFormFloatingActions({
return ( return (
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}> <Button onClick={handleCancelBtnClick} style={{ minWidth: '85px' }}>
<T id={'cancel'} /> <T id={'cancel'} />
</Button> </Button>
<Button <Button

View File

@@ -1,6 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { useCustomers } from 'hooks/query'; import { useCustomers } from 'hooks/query';
import { useCreateProject, useEditProject, useProject } from '../../hooks';
import { DialogContent } from 'components'; import { DialogContent } from 'components';
const ProjectFormContext = React.createContext(); const ProjectFormContext = React.createContext();
@@ -15,20 +16,36 @@ function ProjectFormProvider({
projectId, projectId,
...props ...props
}) { }) {
// Create and edit project mutations.
const { mutateAsync: createProjectMutate } = useCreateProject();
const { mutateAsync: editProjectMutate } = useEditProject();
// Handle fetch project detail.
const { data: project, isLoading: isProjectLoading } = useProject(projectId, {
enabled: !!projectId,
});
// Handle fetch customers data table or list // Handle fetch customers data table or list
const { const {
data: { customers }, data: { customers },
isLoading: isCustomersLoading, isLoading: isCustomersLoading,
} = useCustomers({ page_size: 10000 }); } = useCustomers({ page_size: 10000 });
const isNewMode = !projectId;
// State provider. // State provider.
const provider = { const provider = {
customers, customers,
dialogName, dialogName,
project,
projectId,
isNewMode,
createProjectMutate,
editProjectMutate,
}; };
return ( return (
<DialogContent isLoading={isCustomersLoading}> <DialogContent isLoading={isCustomersLoading || isProjectLoading}>
<ProjectFormContext.Provider value={provider} {...props} /> <ProjectFormContext.Provider value={provider} {...props} />
</DialogContent> </DialogContent>
); );

View File

@@ -14,13 +14,19 @@ const ProjectDialogContent = React.lazy(
*/ */
function ProjectFormDialog({ function ProjectFormDialog({
dialogName, dialogName,
payload: { projectId = null }, payload: { projectId = null, action },
isOpen, isOpen,
}) { }) {
return ( return (
<ProjectFormDialogRoot <ProjectFormDialogRoot
name={dialogName} name={dialogName}
title={<T id={'projects.label.new_project'} />} title={
action === 'edit' ? (
<T id="projects.dialog.edit_project" />
) : (
<T id={'projects.dialog.new_project'} />
)
}
isOpen={isOpen} isOpen={isOpen}
autoFocus={true} autoFocus={true}
canEscapeKeyClose={true} canEscapeKeyClose={true}
@@ -39,6 +45,7 @@ const ProjectFormDialogRoot = styled(Dialog)`
.bp3-dialog-body { .bp3-dialog-body {
.bp3-form-group { .bp3-form-group {
margin-bottom: 15px; margin-bottom: 15px;
margin-top: 15px;
label.bp3-label { label.bp3-label {
margin-bottom: 3px; margin-bottom: 3px;

View File

@@ -7,8 +7,10 @@ const Schema = Yup.object().shape({
.label(intl.get('task.schema.label.task_name')) .label(intl.get('task.schema.label.task_name'))
.required(), .required(),
taskHouse: Yup.string().label(intl.get('task.schema.label.task_house')), taskHouse: Yup.string().label(intl.get('task.schema.label.task_house')),
taskCharge: Yup.string().label(intl.get('task.schema.label.charge')).required(), taskCharge: Yup.string()
.label(intl.get('task.schema.label.charge'))
.required(),
taskamount: Yup.number().label(intl.get('task.schema.label.amount')), taskamount: Yup.number().label(intl.get('task.schema.label.amount')),
}); });
export const CreateTaskFormSchema = Schema; export const CreateProjectTaskFormSchema = Schema;

View File

@@ -1,10 +1,10 @@
//@ts-nocheck //@ts-nocheck
import React from 'react'; import React from 'react';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { CreateTaskFormSchema } from './TaskForm.schema'; import { CreateProjectTaskFormSchema } from './ProjectTaskForm.schema';
import { useTaskFormContext } from './TaskFormProvider'; import { useProjectTaskFormContext } from './ProjectTaskFormProvider';
import { AppToaster } from 'components'; import { AppToaster } from 'components';
import TaskFormContent from './TaskFormContent'; import ProjectTaskFormContent from './ProjectTaskFormContent';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -12,20 +12,20 @@ import { compose } from 'utils';
const defaultInitialValues = { const defaultInitialValues = {
taskName: '', taskName: '',
taskHouse: '00:00', taskHouse: '00:00',
taskCharge: 'Hourly rate', taskCharge: 'hourly_rate',
taskamount: '100000000', taskamount: '',
}; };
/** /**
* Task form. * Project task form.
* @returns * @returns
*/ */
function TaskForm({ function ProjectTaskForm({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
// task form dialog context. // task form dialog context.
const { dialogName } = useTaskFormContext(); const { dialogName } = useProjectTaskFormContext();
// Initial form values // Initial form values
const initialValues = { const initialValues = {
@@ -51,12 +51,12 @@ function TaskForm({
return ( return (
<Formik <Formik
validationSchema={CreateTaskFormSchema} validationSchema={CreateProjectTaskFormSchema}
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
component={TaskFormContent} component={ProjectTaskFormContent}
/> />
); );
} }
export default compose(withDialogActions)(TaskForm); export default compose(withDialogActions)(ProjectTaskForm);

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Form } from 'formik';
import ProjectTaskFormFields from './ProjectTaskFormFields';
import ProjectTaskFormFloatingActions from './ProjectTaskFormFloatingActions';
/**
* Task form content.
* @returns
*/
export default function TaskFormContent() {
return (
<Form>
<ProjectTaskFormFields />
<ProjectTaskFormFloatingActions />
</Form>
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { ProjectTaskFormProvider } from './ProjectTaskFormProvider';
import ProjectTaskForm from './ProjectTaskForm';
/**
* Project task form dialog content.
*/
export default function ProjectTaskFormDialogContent({
// #ownProps
dialogName,
task,
}) {
return (
<ProjectTaskFormProvider taskId={task} dialogName={dialogName}>
<ProjectTaskForm />
</ProjectTaskFormProvider>
);
}

View File

@@ -1,3 +1,4 @@
//@ts-nocheck
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
@@ -9,29 +10,28 @@ import {
Row, Row,
FormattedMessage as T, FormattedMessage as T,
} from 'components'; } from 'components';
import { modalChargeOptions } from '../../../../common/modalChargeOptions'; import { taskChargeOptions } from 'containers/Projects/containers/common/modalChargeOptions';
import { ChangeTypesSelect } from '../../components';
import { TaskModalChargeSelect } from './components';
/** /**
* Task form fields. * Project task form fields.
* @returns * @returns
*/ */
function TaskFormFields() { function ProjectTaskFormFields() {
// Formik context. // Formik context.
const { values } = useFormikContext(); const { values } = useFormikContext();
return ( return (
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
{/*------------ Task Name -----------*/} {/*------------ Task Name -----------*/}
<FFormGroup label={<T id={'task.dialog.task_name'} />} name={'taskName'}> <FFormGroup label={<T id={'project_task.dialog.task_name'} />} name={'taskName'}>
<FInputGroup name="taskName" /> <FInputGroup name="taskName" />
</FFormGroup> </FFormGroup>
{/*------------ Estimated Hours -----------*/} {/*------------ Estimated Hours -----------*/}
<Row> <Row>
<Col xs={4}> <Col xs={4}>
<FFormGroup <FFormGroup
label={<T id={'task.dialog.estimated_hours'} />} label={<T id={'project_task.dialog.estimated_hours'} />}
name={'taskHouse'} name={'taskHouse'}
> >
<FInputGroup name="taskHouse" /> <FInputGroup name="taskHouse" />
@@ -42,16 +42,19 @@ function TaskFormFields() {
<FFormGroup <FFormGroup
name={'taskCharge'} name={'taskCharge'}
className={'form-group--select-list'} className={'form-group--select-list'}
label={<T id={'task.dialog.charge'} />} label={<T id={'project_task.dialog.charge'} />}
> >
<ControlGroup> <ControlGroup>
<TaskModalChargeSelect <ChangeTypesSelect
name="taskCharge" name="taskCharge"
items={modalChargeOptions} items={taskChargeOptions}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
filterable={false} filterable={false}
/> />
<FInputGroup name="taskamount" /> <FInputGroup
name="taskamount"
disabled={values?.taskCharge === 'Non-chargeable'}
/>
</ControlGroup> </ControlGroup>
</FFormGroup> </FFormGroup>
</Col> </Col>
@@ -59,21 +62,22 @@ function TaskFormFields() {
{/*------------ Estimated Amount -----------*/} {/*------------ Estimated Amount -----------*/}
<EstimatedAmountBase> <EstimatedAmountBase>
<EstimatedAmountContent> <EstimatedAmountContent>
<T id={'task.dialog.estimated_amount'} /> <T id={'project_task.dialog.estimated_amount'} />
<EstimateAmount>$100000</EstimateAmount> <EstimateAmount>0.00</EstimateAmount>
</EstimatedAmountContent> </EstimatedAmountContent>
</EstimatedAmountBase> </EstimatedAmountBase>
</div> </div>
); );
} }
export default TaskFormFields; export default ProjectTaskFormFields;
const EstimatedAmountBase = styled.div` const EstimatedAmountBase = styled.div`
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
font-size: 12px; font-size: 14px;
/* opacity: 0.7; */ line-height: 1.5rem;
opacity: 0.75;
`; `;
const EstimatedAmountContent = styled.span` const EstimatedAmountContent = styled.span`
@@ -82,7 +86,7 @@ const EstimatedAmountContent = styled.span`
`; `;
const EstimateAmount = styled.span` const EstimateAmount = styled.span`
font-size: 13px; font-size: 15px;
font-weight: 700; font-weight: 700;
margin-left: 10px; margin-left: 10px;
`; `;

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { Intent, Button, Classes } from '@blueprintjs/core'; import { Intent, Button, Classes } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components'; import { FormattedMessage as T } from 'components';
import { useTaskFormContext } from './TaskFormProvider'; import { useProjectTaskFormContext } from './ProjectTaskFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -11,7 +11,7 @@ import { compose } from 'utils';
* Task form floating actions. * Task form floating actions.
* @returns * @returns
*/ */
function TaskFormFloatingActions({ function ProjectTaskFormFloatingActions({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
@@ -19,7 +19,7 @@ function TaskFormFloatingActions({
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
// Task form dialog context. // Task form dialog context.
const { dialogName } = useTaskFormContext(); const { dialogName } = useProjectTaskFormContext();
// Handle close button click. // Handle close button click.
const handleCancelBtnClick = () => { const handleCancelBtnClick = () => {
@@ -45,4 +45,4 @@ function TaskFormFloatingActions({
); );
} }
export default compose(withDialogActions)(TaskFormFloatingActions); export default compose(withDialogActions)(ProjectTaskFormFloatingActions);

View File

@@ -0,0 +1,32 @@
//@ts-nocheck
import React from 'react';
import { DialogContent } from 'components';
const ProjectTaskFormContext = React.createContext();
/**
* Project task form provider.
* @returns
*/
function ProjectTaskFormProvider({
// #ownProps
dialogName,
taskId,
...props
}) {
// State provider.
const provider = {
dialogName,
};
return (
<DialogContent>
<ProjectTaskFormContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useProjectTaskFormContext = () =>
React.useContext(ProjectTaskFormContext);
export { ProjectTaskFormProvider, useProjectTaskFormContext };

View File

@@ -5,30 +5,32 @@ import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
import withDialogRedux from 'components/DialogReduxConnect'; import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils'; import { compose } from 'utils';
const TaskFormDialogContent = React.lazy( const ProjectTaskFormDialogContent = React.lazy(
() => import('./TaskFormDialogContent'), () => import('./ProjectTaskFormDialogContent'),
); );
/** /**
* Task form dialog. * Project task form dialog.
* @returns * @returns
*/ */
function TaskFormDialog({ dialogName, payload: { taskId = null }, isOpen }) { function ProjectTaskFormDialog({
dialogName,
payload: { taskId = null },
isOpen,
}) {
return ( return (
<Dialog <Dialog
name={dialogName} name={dialogName}
title={intl.get('task.dialog.new_task')} title={intl.get('project_task.dialog.new_task')}
isOpen={isOpen} isOpen={isOpen}
autoFocus={true} autoFocus={true}
canEscapeKeyClose={true} canEscapeKeyClose={true}
style={{ width: '500px' }} style={{ width: '500px' }}
> >
<DialogSuspense> <DialogSuspense>
<TaskFormDialogContent dialogName={dialogName} task={taskId} /> <ProjectTaskFormDialogContent dialogName={dialogName} task={taskId} />
</DialogSuspense> </DialogSuspense>
</Dialog> </Dialog>
); );
} }
export default compose(withDialogRedux())(TaskFormDialog); export default compose(withDialogRedux())(ProjectTaskFormDialog);
const TaskFormDialogRoot = styled(Dialog)``;

View File

@@ -3,17 +3,19 @@ import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes'; import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({ const Schema = Yup.object().shape({
date: Yup.date().label(intl.get('time_entry.schema.label.date')).required(), date: Yup.date()
.label(intl.get('project_time_entry.schema.label.date'))
.required(),
projectId: Yup.string() projectId: Yup.string()
.label(intl.get('time_entry.schema.label.project_name')) .label(intl.get('project_time_entry.schema.label.project_name'))
.required(), .required(),
taskId: Yup.string() taskId: Yup.string()
.label(intl.get('time_entry.schema.label.task_name')) .label(intl.get('project_time_entry.schema.label.task_name'))
.required(), .required(),
description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT), description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
duration: Yup.string() duration: Yup.string()
.label(intl.get('time_entry.schema.label.duration')) .label(intl.get('project_time_entry.schema.label.duration'))
.required(), .required(),
}); });
export const CreateTimeEntryFormSchema = Schema; export const CreateProjectTimeEntryFormSchema = Schema;

View File

@@ -5,9 +5,9 @@ import intl from 'react-intl-universal';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { AppToaster } from 'components'; import { AppToaster } from 'components';
import TimeEntryFormContent from './TimeEntryFormContent'; import ProjectTimeEntryFormContent from './ProjectTimeEntryFormContent';
import { CreateTimeEntryFormSchema } from './TimeEntryForm.schema'; import { CreateProjectTimeEntryFormSchema } from './ProjectTimeEntryForm.schema';
import { useTimeEntryFormContext } from './TimeEntryFormProvider'; import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -21,15 +21,15 @@ const defaultInitialValues = {
}; };
/** /**
* Time entry form. * Project Time entry form.
* @returns * @returns
*/ */
function TimeEntryForm({ function ProjectTimeEntryForm({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
// time entry form dialog context. // time entry form dialog context.
const { dialogName } = useTimeEntryFormContext(); const { dialogName } = useProjectTimeEntryFormContext();
// Initial form values // Initial form values
const initialValues = { const initialValues = {
@@ -58,12 +58,12 @@ function TimeEntryForm({
return ( return (
<Formik <Formik
validationSchema={CreateTimeEntryFormSchema} validationSchema={CreateProjectTimeEntryFormSchema}
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
component={TimeEntryFormContent} component={ProjectTimeEntryFormContent}
/> />
); );
} }
export default compose(withDialogActions)(TimeEntryForm); export default compose(withDialogActions)(ProjectTimeEntryForm);

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Form } from 'formik';
import ProjectTimeEntryFormFields from './ProjectTimeEntryFormFields';
import ProjectTimeEntryFormFloatingActions from './ProjectTimeEntryFormFloatingActions';
/**
* Time entry form content.
* @returns
*/
export default function TimeEntryFormContent() {
return (
<Form>
<ProjectTimeEntryFormFields />
<ProjectTimeEntryFormFloatingActions />
</Form>
);
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { ProjectTimeEntryFormProvider } from './ProjectTimeEntryFormProvider';
import ProjectTimeEntryForm from './ProjectTimeEntryForm';
/**
* Project time entry form dialog content.
* @returns {ReactNode}
*/
export default function ProjectTimeEntryFormDialogContent({
// #ownProps
dialogName,
project,
timeEntry,
}) {
return (
<ProjectTimeEntryFormProvider
projectId={project}
timeEntryId={timeEntry}
dialogName={dialogName}
>
<ProjectTimeEntryForm />
</ProjectTimeEntryFormProvider>
);
}

View File

@@ -14,25 +14,25 @@ import {
FieldRequiredHint, FieldRequiredHint,
FormattedMessage as T, FormattedMessage as T,
} from 'components'; } from 'components';
import { ProjectSelect, TaskSelect } from './components'; import { TaskSelect, ProjectsSelect } from '../../components';
import { momentFormatter } from 'utils'; import { momentFormatter } from 'utils';
/** /**
* Time entry form fields. * Project time entry form fields.
* @returns * @returns
*/ */
function TimeEntryFormFields() { function ProjectTimeEntryFormFields() {
return ( return (
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
{/*------------ Project -----------*/} {/*------------ Project -----------*/}
<FFormGroup <FFormGroup
name={'projectId'} name={'projectId'}
label={<T id={'time_entry.dialog.project'} />} label={<T id={'project_time_entry.dialog.project'} />}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', Classes.FILL)} className={classNames('form-group--select-list', Classes.FILL)}
> >
<ProjectSelect <ProjectsSelect
name={'tesc'} name={'projectId'}
projects={[]} projects={[]}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
/> />
@@ -40,7 +40,7 @@ function TimeEntryFormFields() {
{/*------------ Task -----------*/} {/*------------ Task -----------*/}
<FFormGroup <FFormGroup
name={'taskId'} name={'taskId'}
label={<T id={'time_entry.dialog.task'} />} label={<T id={'project_time_entry.dialog.task'} />}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', Classes.FILL)} className={classNames('form-group--select-list', Classes.FILL)}
> >
@@ -53,7 +53,7 @@ function TimeEntryFormFields() {
{/*------------ Duration -----------*/} {/*------------ Duration -----------*/}
<FFormGroup <FFormGroup
label={intl.get('time_entry.dialog.duration')} label={intl.get('project_time_entry.dialog.duration')}
name={'duration'} name={'duration'}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
> >
@@ -61,7 +61,7 @@ function TimeEntryFormFields() {
</FFormGroup> </FFormGroup>
{/*------------ Date -----------*/} {/*------------ Date -----------*/}
<FFormGroup <FFormGroup
label={intl.get('time_entry.dialog.date')} label={intl.get('project_time_entry.dialog.date')}
name={'date'} name={'date'}
className={classNames(CLASSES.FILL, 'form-group--date')} className={classNames(CLASSES.FILL, 'form-group--date')}
> >
@@ -78,20 +78,13 @@ function TimeEntryFormFields() {
{/*------------ Description -----------*/} {/*------------ Description -----------*/}
<FFormGroup <FFormGroup
name={'description'} name={'description'}
label={intl.get('time_entry.dialog.description')} label={intl.get('project_time_entry.dialog.description')}
className={'form-group--description'} className={'form-group--description'}
> >
<FTextArea name={'description'} /> <FTextArea name={'description'} />
{/* <FEditableText
multiline={true}
// minLines={1.78}
// maxLines={1.78}
name={'description'}
placeholder=""
/> */}
</FFormGroup> </FFormGroup>
</div> </div>
); );
} }
export default TimeEntryFormFields; export default ProjectTimeEntryFormFields;

View File

@@ -3,20 +3,20 @@ import React from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { Intent, Button, Classes } from '@blueprintjs/core'; import { Intent, Button, Classes } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components'; import { FormattedMessage as T } from 'components';
import { useTimeEntryFormContext } from './TimeEntryFormProvider'; import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils'; import { compose } from 'utils';
/** /**
* Time entry form floating actions. * Projcet time entry form floating actions.
* @returns * @returns
*/ */
function TimeEntryFormFloatingActions({ function ProjectTimeEntryFormFloatingActions({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
// time entry form dialog context. // time entry form dialog context.
const { dialogName } = useTimeEntryFormContext(); const { dialogName } = useProjectTimeEntryFormContext();
// Formik context. // Formik context.
const { isSubmitting } = useFormikContext(); const { isSubmitting } = useFormikContext();
@@ -45,4 +45,4 @@ function TimeEntryFormFloatingActions({
); );
} }
export default compose(withDialogActions)(TimeEntryFormFloatingActions); export default compose(withDialogActions)(ProjectTimeEntryFormFloatingActions);

View File

@@ -0,0 +1,32 @@
//@ts-nocheck
import React from 'react';
import { DialogContent } from 'components';
const ProjecctTimeEntryFormContext = React.createContext();
/**
* Project time entry form provider.
* @returns
*/
function ProjectTimeEntryFormProvider({
// #ownProps
dialogName,
projectId,
timeEntryId,
...props
}) {
const provider = {
dialogName,
};
return (
<DialogContent>
<ProjecctTimeEntryFormContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useProjectTimeEntryFormContext = () =>
React.useContext(ProjecctTimeEntryFormContext);
export { ProjectTimeEntryFormProvider, useProjectTimeEntryFormContext };

View File

@@ -4,42 +4,42 @@ import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
import withDialogRedux from 'components/DialogReduxConnect'; import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils'; import { compose } from 'utils';
const TimeEntryFormDialogContent = React.lazy( const ProjectTimeEntryFormDialogContent = React.lazy(
() => import('./TimeEntryFormDialogContent'), () => import('./ProjectTimeEntryFormDialogContent'),
); );
/** /**
* Time entry form dialog. * Project time entry form dialog.
* @returns * @returns
*/ */
function TimeEntryFormDialog({ function ProjectTimeEntryFormDialog({
dialogName, dialogName,
isOpen, isOpen,
payload: { projectId = null, timeEntryId = null }, payload: { projectId = null, timeEntryId = null },
}) { }) {
return ( return (
<TimeEntryFormDialogRoot <ProjectTimeEntryFormDialogRoot
name={dialogName} name={dialogName}
title={<T id={'time_entry.dialog.label'} />} title={<T id={'project_time_entry.dialog.label'} />}
isOpen={isOpen} isOpen={isOpen}
autoFocus={true} autoFocus={true}
canEscapeKeyClose={true} canEscapeKeyClose={true}
style={{ width: '400px' }} style={{ width: '400px' }}
> >
<DialogSuspense> <DialogSuspense>
<TimeEntryFormDialogContent <ProjectTimeEntryFormDialogContent
dialogName={dialogName} dialogName={dialogName}
project={projectId} project={projectId}
timeEntry={timeEntryId} timeEntry={timeEntryId}
/> />
</DialogSuspense> </DialogSuspense>
</TimeEntryFormDialogRoot> </ProjectTimeEntryFormDialogRoot>
); );
} }
export default compose(withDialogRedux())(TimeEntryFormDialog); export default compose(withDialogRedux())(ProjectTimeEntryFormDialog);
const TimeEntryFormDialogRoot = styled(Dialog)` const ProjectTimeEntryFormDialogRoot = styled(Dialog)`
.bp3-dialog-body { .bp3-dialog-body {
.bp3-form-group { .bp3-form-group {
margin-bottom: 15px; margin-bottom: 15px;

View File

@@ -1,3 +1,4 @@
//@ts-nocheck
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@@ -5,51 +6,17 @@ import { DataTable } from 'components';
import { TABLES } from 'common/tables'; import { TABLES } from 'common/tables';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows'; import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton'; import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import ProjectsEmptyStatus from './ProjectsEmptyStatus';
import { useProjectsListContext } from './ProjectsListProvider'; import { useProjectsListContext } from './ProjectsListProvider';
import { useMemorizedColumnsWidths } from 'hooks'; import { useMemorizedColumnsWidths } from 'hooks';
import { useProjectsListColumns, ActionsMenu } from './components'; import { useProjectsListColumns, ActionsMenu } from './components';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withProjectsActions from './withProjectsActions'; import withProjectsActions from './withProjectsActions';
import withSettings from '../../../Settings/withSettings'; import withSettings from '../../../Settings/withSettings';
import { compose } from 'utils'; import { compose } from 'utils';
const projects = [
{
id: 1,
name: 'Maroon Bronze',
deadline: '2022-06-08T22:00:00.000Z',
display_name: 'Kyrie Rearden',
cost_estimate: '40000',
task_amount: '0',
is_process: true,
is_closed: false,
is_draft: false,
},
{
id: 2,
name: 'Project Sherwood',
deadline: '2022-06-08T22:00:00.000Z',
display_name: 'Ella-Grace Miller',
cost_estimate: '700',
task_amount: '300',
is_process: false,
is_closed: false,
is_draft: true,
},
{
id: 3,
name: 'Tax Compliance',
deadline: '2022-06-23T22:00:00.000Z',
display_name: 'Abby & Wells',
cost_estimate: '3000',
task_amount: '0',
is_process: true,
is_closed: false,
is_draft: false,
},
];
/** /**
* Projects list datatable. * Projects list datatable.
* @returns * @returns
@@ -58,11 +25,23 @@ function ProjectsDataTable({
// #withDial // #withDial
openDialog, openDialog,
// #withAlertsActions
openAlert,
// #withSettings // #withSettings
projectsTableSize, projectsTableSize,
}) { }) {
const history = useHistory(); const history = useHistory();
// Projects list context.
const { projects, isEmptyStatus, isProjectsLoading, isProjectsFetching } =
useProjectsListContext();
// Handle delete project.
const handleDeleteProject = ({ id }) => {
openAlert('project-delete', { projectId: id });
};
// Retrieve projects table columns. // Retrieve projects table columns.
const columns = useProjectsListColumns(); const columns = useProjectsListColumns();
@@ -78,12 +57,13 @@ function ProjectsDataTable({
const handleEditProject = (project) => { const handleEditProject = (project) => {
openDialog('project-form', { openDialog('project-form', {
projectId: project.id, projectId: project.id,
action: 'edit',
}); });
}; };
// Handle new task button click. // Handle new task button click.
const handleNewTaskButtonClick = () => { const handleNewTaskButtonClick = () => {
openDialog('task-form'); openDialog('project-task-form');
}; };
// Local storage memorizing columns widths. // Local storage memorizing columns widths.
@@ -98,13 +78,18 @@ function ProjectsDataTable({
}); });
}; };
// Display project empty status instead of the table.
if (isEmptyStatus) {
return <ProjectsEmptyStatus />;
}
return ( return (
<ProjectsTable <ProjectsTable
columns={columns} columns={columns}
data={projects} data={projects}
// loading={} loading={isProjectsLoading}
// headerLoading={} headerLoading={isProjectsLoading}
// progressBarLoading={} progressBarLoading={isProjectsFetching}
manualSortBy={true} manualSortBy={true}
noInitialFetch={true} noInitialFetch={true}
sticky={true} sticky={true}
@@ -119,6 +104,7 @@ function ProjectsDataTable({
payload={{ payload={{
onViewDetails: handleViewDetailProject, onViewDetails: handleViewDetailProject,
onEdit: handleEditProject, onEdit: handleEditProject,
onDelete: handleDeleteProject,
onNewTask: handleNewTaskButtonClick, onNewTask: handleNewTaskButtonClick,
}} }}
/> />
@@ -127,6 +113,7 @@ function ProjectsDataTable({
export default compose( export default compose(
withDialogActions, withDialogActions,
withAlertsActions,
withProjectsActions, withProjectsActions,
withSettings(({ projectSettings }) => ({ withSettings(({ projectSettings }) => ({
projectsTableSize: projectSettings?.tableSize, projectsTableSize: projectSettings?.tableSize,
@@ -138,27 +125,28 @@ const ProjectsTable = styled(DataTable)`
.tr .td { .tr .td {
padding: 0.5rem 0.8rem; padding: 0.5rem 0.8rem;
} }
.avatar.td { .avatar.td {
.avatar { .cell-inner {
display: inline-block; .avatar {
background: #adbcc9; display: inline-block;
border-radius: 8%; background: #adbcc9;
text-align: center; border-radius: 8%;
font-weight: 400; text-align: center;
color: #fff; font-weight: 400;
color: #fff;
&[data-size='medium'] { &[data-size='medium'] {
height: 30px; height: 30px;
width: 30px; width: 30px;
line-height: 30px; line-height: 30px;
font-size: 14px; font-size: 14px;
} }
&[data-size='small'] { &[data-size='small'] {
height: 25px; height: 25px;
width: 25px; width: 25px;
line-height: 25px; line-height: 25px;
font-size: 12px; font-size: 12px;
}
} }
} }
} }

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { EmptyStatus, FormattedMessage as T } from 'components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
function ProjectsEmptyStatus({
// #withDialogActions
openDialog,
}) {
// Handle new project button click.
const handleNewProjectClick = () => {
openDialog('project-form', {});
};
return (
<EmptyStatus
title={<T id="projects.empty_status.title" />}
description={
<p>
<T id="projects.empty_status.description" />
</p>
}
action={
<React.Fragment>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={handleNewProjectClick}
>
<T id="projects.empty_status.action" />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</React.Fragment>
}
/>
);
}
export default compose(withDialogActions)(ProjectsEmptyStatus);

View File

@@ -1,7 +1,9 @@
//@ts-nocheck //@ts-nocheck
import React from 'react'; import React from 'react';
import { isEmpty } from 'lodash';
import { useResourceViews, useResourceMeta } from 'hooks/query'; import { useResourceViews, useResourceMeta } from 'hooks/query';
import DashboardInsider from '../../../../components/Dashboard/DashboardInsider'; import DashboardInsider from '../../../../components/Dashboard/DashboardInsider';
import { useProjects } from '../../hooks';
const ProjectsListContext = React.createContext(); const ProjectsListContext = React.createContext();
@@ -14,16 +16,32 @@ function ProjectsListProvider({ query, tableStateChanged, ...props }) {
const { data: projectsViews, isLoading: isViewsLoading } = const { data: projectsViews, isLoading: isViewsLoading } =
useResourceViews('projects'); useResourceViews('projects');
// Fetch accounts list according to the given custom view id.
const {
data: { projects },
isFetching: isProjectsFetching,
isLoading: isProjectsLoading,
} = useProjects(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus =
isEmpty(projects) && !tableStateChanged && !isProjectsLoading;
// provider payload. // provider payload.
const provider = { const provider = {
projects,
projectsViews, projectsViews,
isProjectsLoading,
isProjectsFetching,
isViewsLoading,
isEmptyStatus,
}; };
return ( return (
<DashboardInsider <DashboardInsider loading={isViewsLoading} name={'projects'}>
// loading={isViewsLoading}
name={'projects'}
>
<ProjectsListContext.Provider value={provider} {...props} /> <ProjectsListContext.Provider value={provider} {...props} />
</DashboardInsider> </DashboardInsider>
); );

View File

@@ -102,13 +102,15 @@ export const ActionsMenu = ({
export const ProjectsAccessor = (row) => ( export const ProjectsAccessor = (row) => (
<ProjectItemsWrap> <ProjectItemsWrap>
<ProjectItemsHeader> <ProjectItemsHeader>
<ProjectItemContactName>{row.display_name}</ProjectItemContactName> <ProjectItemContactName>
{row.contact_display_name}
</ProjectItemContactName>
<ProjectItemProjectName>{row.name}</ProjectItemProjectName> <ProjectItemProjectName>{row.name}</ProjectItemProjectName>
</ProjectItemsHeader> </ProjectItemsHeader>
<ProjectItemDescription> <ProjectItemDescription>
<FormatDate value={row.deadline} /> <FormatDate value={row.deadline_formatted} />
{intl.get('projects.label.cost_estimate', { {intl.get('projects.label.cost_estimate', {
value: row.cost_estimate, value: row.cost_estimate_formatted,
})} })}
</ProjectItemDescription> </ProjectItemDescription>
</ProjectItemsWrap> </ProjectItemsWrap>
@@ -175,10 +177,10 @@ const ProjectItemDescription = styled.div`
const ProjectStatusRoot = styled.div` const ProjectStatusRoot = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
/* justify-content: flex-end; */
margin-right: 0.5rem; margin-right: 0.5rem;
flex-direction: row-reverse; flex-direction: row-reverse;
`; `;
const ProjectStatusTaskAmount = styled.div` const ProjectStatusTaskAmount = styled.div`
text-align: right; text-align: right;
font-weight: 400; font-weight: 400;
@@ -198,6 +200,7 @@ const ProjectProgressBar = styled(ProgressBar)`
} }
} }
`; `;
const StatusTag = styled(Tag)` const StatusTag = styled(Tag)`
min-width: 65px; min-width: 65px;
text-align: center; text-align: center;

View File

@@ -1,17 +0,0 @@
import React from 'react';
import { Form } from 'formik';
import TaskFormFields from './TaskFormFields';
import TaskFormFloatingActions from './TaskFormFloatingActions';
/**
* Task form content.
* @returns
*/
export default function TaskFormContent() {
return (
<Form>
<TaskFormFields />
<TaskFormFloatingActions />
</Form>
);
}

View File

@@ -1,18 +0,0 @@
import React from 'react';
import { TaskFormProvider } from './TaskFormProvider';
import TaskForm from './TaskForm';
/**
* Task form dialog content.
*/
export default function TaskFormDialogContent({
// #ownProps
dialogName,
task,
}) {
return (
<TaskFormProvider taskId={task} dialogName={dialogName}>
<TaskForm />
</TaskFormProvider>
);
}

View File

@@ -1,31 +0,0 @@
//@ts-nocheck
import React from 'react';
import { DialogContent } from 'components';
const TaskFormContext = React.createContext();
/**
* Task form provider.
* @returns
*/
function TaskFormProvider({
// #ownProps
dialogName,
taskId,
...props
}) {
// State provider.
const provider = {
dialogName,
};
return (
<DialogContent>
<TaskFormContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useTaskFormContext = () => React.useContext(TaskFormContext);
export { TaskFormProvider, useTaskFormContext };

View File

@@ -1,17 +0,0 @@
import React from 'react';
import { Form } from 'formik';
import TimeEntryFormFields from './TimeEntryFormFields';
import TimeEntryFormFloatingActions from './TimeEntryFormFloatingActions';
/**
* Time entry form content.
* @returns
*/
export default function TimeEntryFormContent() {
return (
<Form>
<TimeEntryFormFields />
<TimeEntryFormFloatingActions />
</Form>
);
}

View File

@@ -1,24 +0,0 @@
import React from 'react';
import { TimeEntryFormProvider } from './TimeEntryFormProvider';
import TimeEntryForm from './TimeEntryForm';
/**
* Time entry form dialog content.
* @returns {ReactNode}
*/
export default function TimeEntryFormDialogContent({
// #ownProps
dialogName,
project,
timeEntry,
}) {
return (
<TimeEntryFormProvider
projectId={project}
timeEntryId={timeEntry}
dialogName={dialogName}
>
<TimeEntryForm />
</TimeEntryFormProvider>
);
}

View File

@@ -1,31 +0,0 @@
//@ts-nocheck
import React from 'react';
import { DialogContent } from 'components';
const TimeEntryFormContext = React.createContext();
/**
* Time entry form provider.
* @returns
*/
function TimeEntryFormProvider({
// #ownProps
dialogName,
projectId,
timeEntryId,
...props
}) {
const provider = {
dialogName,
};
return (
<DialogContent>
<TimeEntryFormContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useTimeEntryFormContext = () => React.useContext(TimeEntryFormContext);
export { TimeEntryFormProvider, useTimeEntryFormContext };

View File

@@ -1,63 +0,0 @@
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from '../../../../../components/Forms';
/**
*
* @param {*} query
* @param {*} project
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const projectItemPredicate = (query, project, _index, exactMatch) => {
const normalizedTitle = project.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${project.name}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param {*} project
* @param {*} param1
* @returns
*/
const projectItemRenderer = (project, { handleClick, modifiers, query }) => {
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
key={project.id}
onClick={handleClick}
text={project.name}
/>
);
};
const projectSelectProps = {
// itemPredicate: projectItemPredicate,
itemRenderer: projectItemRenderer,
valueAccessor: 'id',
labelAccessor: 'name',
};
export function ProjectSelect({ projects, ...rest }) {
return (
<FSelect
items={projects}
{...projectSelectProps}
{...rest}
input={ProjectSelectButton}
/>
);
}
function ProjectSelectButton({ label }) {
return <Button text={label ? label : intl.get('find_or_choose_a_project')} />;
}

View File

@@ -1,2 +0,0 @@
export * from './ProjectSelect';
export * from './TaskSelect';

View File

@@ -0,0 +1,17 @@
import intl from 'react-intl-universal';
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('expenses.dialog.custom_pirce'), value: 'custom_pirce' },
{ name: intl.get('expenses.dialog.non_chargeable'), value: 'non_chargeable' },
];

View File

@@ -0,0 +1,125 @@
//@ts-nocheck
import { useQueryClient, useMutation } from 'react-query';
import { useRequestQuery } from 'hooks/useQueryRequest';
import { transformPagination } from 'utils';
import useApiRequest from 'hooks/useRequest';
import t from './type';
// Common invalidate queries.
const commonInvalidateQueries = (queryClient) => {
// Invalidate projects.
queryClient.invalidateQueries(t.PROJECT);
queryClient.invalidateQueries(t.PROJECTS);
};
/**
* Create a new project
* @param props
*/
export function useCreateProject(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((values) => apiRequest.post('projects', values), {
onSuccess: (res, values) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
});
}
/**
* Edit the given project
* @param props
* @returns
*/
export function useEditProject(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`/projects/${id}`, values),
{
onSuccess: (res, [id, values]) => {
// Invalidate specific project.
queryClient.invalidateQueries([t.PROJECT, id]);
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
/**
* Delete the given project
* @param props
*/
export function useDeleteProject(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.delete(`projects/${id}`), {
onSuccess: (res, id) => {
// Invalidate specific project.
queryClient.invalidateQueries([t.PROJECT, id]);
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
});
}
/**
* Retrieve the projects details.
* @param projectId The project id
* @param props
* @param requestProps
* @returns
*/
export function useProject(projectId, props, requestProps) {
return useRequestQuery(
[t.PROJECT, projectId],
{ method: 'get', url: `projects/${projectId}`, ...requestProps },
{
select: (res) => res.data.project,
defaultData: {},
...props,
},
);
}
const transformProjects = (res) => ({
projects: res.data.projects,
});
/**
* Retrieve projects list with pagination meta.
* @param query
* @param props
*/
export function useProjects(query, props) {
return useRequestQuery(
[t.PROJECTS, query],
{ method: 'get', url: 'projects', params: query },
{
select: transformProjects,
defaultData: {
projects: [],
},
...props,
},
);
}
export function useRefreshInvoices() {
const queryClient = useQueryClient();
return {
refresh: () => {
queryClient.invalidateQueries(t.PROJECTS);
},
};
}

View File

@@ -0,0 +1,11 @@
const CUSTOMERS = {
CUSTOMERS: 'CUSTOMERS',
CUSTOMER: 'CUSTOMER',
};
const PROJECTS = {
PROJECT: 'PROJECT',
PROJECTS: 'PROJECTS',
};
export default { ...PROJECTS, ...CUSTOMERS };

View File

@@ -23,7 +23,9 @@ export default (mapState) => {
vendorsCreditNoteSetting: state.settings.data.vendorCredit, vendorsCreditNoteSetting: state.settings.data.vendorCredit,
warehouseTransferSettings: state.settings.data.warehouseTransfers, warehouseTransferSettings: state.settings.data.warehouseTransfers,
projectSettings:state.settings.data.projects, projectSettings:state.settings.data.projects,
timesheetsSettings:state.settings.data.timesheets timesheetsSettings:state.settings.data.timesheets,
purchasesSettings:state.settings.data.purchases,
salesSettings:state.settings.data.sales,
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };

View File

@@ -2057,20 +2057,32 @@
"projects.dialog.cost_estimate": "Cost Estimate", "projects.dialog.cost_estimate": "Cost Estimate",
"projects.label.create": "Create", "projects.label.create": "Create",
"projects.label.cost_estimate": " • Estimate {value}", "projects.label.cost_estimate": " • Estimate {value}",
"task.dialog.new_task": "New Task", "projects.dialog.success_message": "The project has been created successfully.",
"task.dialog.task_name": "Task Name", "projects.dialog.edit_success_message": "The project has been edited successfully.",
"task.dialog.estimated_hours": "Estimate Hours", "projects.dialog.new_project": "New Project",
"task.dialog.charge": "Charge", "projects.dialog.edit_project": "Edit Project",
"task.dialog.estimated_amount": "Estimated Amount", "projects.alert.delete_message": "The deleted project has been deleted successfully.",
"projects.alert.once_delete_this_project": "Once you delete this project, you won't be able to restore it later. Are you sure you want to delete this project?",
"projects.empty_status.title":"",
"projects.empty_status.description":"",
"projects.empty_status.action":"New Project",
"project_task.dialog.new_task": "New Task",
"project_task.dialog.task_name": "Task Name",
"project_task.dialog.estimated_hours": "Estimate Hours",
"project_task.dialog.charge": "Charge",
"project_task.dialog.estimated_amount": "Estimated Amount",
"project_task.dialog.hourly_rate": "Hourly rate",
"project_task.dialog.fixed_price": "Fixed price",
"project_task.dialog.non_chargeable": "Non-chargeable",
"project.schema.label.contact": "Contact", "project.schema.label.contact": "Contact",
"project.schema.label.project_name": "Project name", "project.schema.label.project_name": "Project name",
"project.schema.label.deadline": "Deadline", "project.schema.label.deadline": "Deadline",
"project.schema.label.project_state": "Project state", "project.schema.label.project_state": "Project state",
"project.schema.label.project_cost": "Project cost", "project.schema.label.project_cost": "Project cost",
"task.schema.label.task_name": "Task name", "project_task.schema.label.task_name": "Task name",
"task.schema.label.task_house": "Task house", "project_task.schema.label.task_house": "Task house",
"task.schema.label.charge": "Charge", "project_task.schema.label.charge": "Charge",
"task.schema.label.amount": "Amount", "project_task.schema.label.amount": "Amount",
"projcet_details.action.new_transaction": "New Transaction", "projcet_details.action.new_transaction": "New Transaction",
"projcet_details.action.time_entry": "Time", "projcet_details.action.time_entry": "Time",
"projcet_details.action.edit_project": "Edit Project", "projcet_details.action.edit_project": "Edit Project",
@@ -2079,23 +2091,75 @@
"project_details.label.purchases": "Purchases", "project_details.label.purchases": "Purchases",
"project_details.label.sales": "Sales", "project_details.label.sales": "Sales",
"project_details.label.journals": "Journals", "project_details.label.journals": "Journals",
"project_details.new_expenses": "New Expenses",
"project_details.new_estimated_expenses": "New Estimated Expenses",
"timesheets.actions.delete_timesheet": "Delete", "timesheets.actions.delete_timesheet": "Delete",
"timesheets.column.date": "Date", "timesheets.column.date": "Date",
"timesheets.column.task": "Task", "timesheets.column.task": "Task",
"timesheets.column.user": "User", "timesheets.column.user": "User",
"timesheets.column.time": "Time", "timesheets.column.time": "Time",
"timesheets.column.billing_status": "Billing Status", "timesheets.column.billing_status": "Billing Status",
"time_entry.dialog.label": "New Time Entry", "project_time_entry.dialog.label": "New Time Entry",
"time_entry.dialog.project": "Project", "project_time_entry.dialog.project": "Project",
"time_entry.dialog.task": "Task", "project_time_entry.dialog.task": "Task",
"time_entry.dialog.description": "Description", "project_time_entry.dialog.description": "Description",
"time_entry.dialog.duration": "Duration", "project_time_entry.dialog.duration": "Duration",
"time_entry.dialog.date": "Date", "project_time_entry.dialog.date": "Date",
"time_entry.dialog.create": "Create", "project_time_entry.dialog.create": "Create",
"time_entry.schema.label.project_name": "Project name", "project_time_entry.schema.label.project_name": "Project name",
"time_entry.schema.label.task_name": "Task name", "project_time_entry.schema.label.task_name": "Task name",
"time_entry.schema.label.duration": "Duration", "project_time_entry.schema.label.duration": "Duration",
"time_entry.schema.label.date": "Date", "project_time_entry.schema.label.date": "Date",
"find_or_choose_a_project": "Find or choose a project", "find_or_choose_a_project": "Find or choose a project",
"choose_a_task": "Choose a task" "choose_a_task": "Choose a task",
"project_expense.dialog.label": "New Expense",
"project_expense.dialog.expense_name": "Expense Name",
"project_expense.dialog.expense_date": "Date",
"project_expense.dialog.quantity": "Quantity",
"project_expense.dialog.charge": "Charge",
"project_expense.dialog.track_expense": "Track to Expense",
"project_expense.dialog.unit_price": "Unit Price",
"project_expense.dialog.expense_total": "Total",
"project_expense.dialog.percentage": "Percentage",
"project_expense.dialog.total": "Total:",
"project_expense.dialog.markup": "% Markup",
"project_expense.dialog.pass_cost_on": "Pass cost on",
"project_expense.dialog.custom_pirce": "Custom Pirce",
"project_expense.dialog.non_chargeable": "Non-chargeable",
"project_expense.dialog.cost_to_you": "Cost to you",
"project_expense.dialog.what_you_ll_charge": "What you'll charge",
"project_expense.schema.label.expense_name": "Expense name",
"project_expense.schema.label.estimated_expense": "Estimated expense",
"project_expense.schema.label.quantity": "Quantity",
"project_expense.schema.label.unit_price": "Unit price",
"choose_an_estimated_expense": "Choose an estimated expense",
"estimated_expenses.dialog.label": "New Estimated Expense",
"estimated_expenses.dialog.estimated_expense": "Estimated Expense Name",
"estimated_expenses.dialog.quantity": "Quantity",
"estimated_expenses.dialog.unit_price": "Unit Price",
"estimated_expenses.dialog.total": "Total",
"estimated_expenses.dialog.charge": "Charge",
"estimated_expenses.dialog.percentage": "Percentage",
"estimated_expenses.dialog.estimated_amount": "Estimated Amount:",
"estimated_expenses.dialog.cost_to_you": "Cost to you",
"estimated_expenses.dialog.what_you_ll_charge": "What you'll charge",
"estimated_expense.schema.label.estimated_expense": "Estimated expense name",
"estimated_expense.schema.label.quantity": "Quantity",
"estimated_expense.schema.label.unit_price": "Unit price",
"purchases.column.date": "Date",
"purchases.column.type": "Type",
"purchases.column.transaction_no": "Transaction No",
"purchases.column.due_date": "Due Date",
"purchases.column.balance": "Balance",
"purchases.column.total": "Total",
"purchases.column.status": "Status",
"purchases.action.delete": "Delete",
"sales.column.date": "Date",
"sales.column.type": "Type",
"sales.column.transaction_no": "Transaction No",
"sales.column.due_date": "Due Date",
"sales.column.balance": "Balance",
"sales.column.total": "Total",
"sales.column.status": "Status",
"sales.action.delete": "Delete"
} }

View File

@@ -67,6 +67,12 @@ const initialState = {
timesheets: { timesheets: {
tableSize: 'medium', tableSize: 'medium',
}, },
purchases: {
tableSize: 'medium',
},
sales: {
tableSize: 'medium',
}
}, },
}; };