Compare commits
12 Commits
multi-line
...
BIG-386-li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
754618aa7a | ||
|
|
50c905eabb | ||
|
|
709e06a646 | ||
|
|
8826d2bc5b | ||
|
|
38a961b899 | ||
|
|
7ef7e126e5 | ||
|
|
bcf0ec25b8 | ||
|
|
965a8966f6 | ||
|
|
b030d6ea37 | ||
|
|
31fef21362 | ||
|
|
6f2a456a56 | ||
|
|
6134ad5598 |
@@ -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' },
|
||||
];
|
||||
@@ -19,6 +19,8 @@ export const TABLES = {
|
||||
WAREHOUSE_TRANSFERS: 'warehouse_transfers',
|
||||
PROJECTS: 'projects',
|
||||
TIMESHEETS: 'timesheets',
|
||||
PURCHASES: 'purchases',
|
||||
SALES: 'sales',
|
||||
};
|
||||
|
||||
export const TABLE_SIZE = {
|
||||
|
||||
@@ -88,23 +88,27 @@ export default function TableHeader() {
|
||||
},
|
||||
} = useContext(TableContext);
|
||||
|
||||
// Can't contiunue if the thead is disabled.
|
||||
if (hideTableHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (headerLoading && TableHeaderSkeletonRenderer) {
|
||||
return <TableHeaderSkeletonRenderer />;
|
||||
}
|
||||
|
||||
return (
|
||||
!hideTableHeader && (
|
||||
<ScrollSyncPane>
|
||||
<div className="thead">
|
||||
<div className={'thead-inner'}>
|
||||
{headerGroups.map((headerGroup, index) => (
|
||||
<TableHeaderGroup key={index} headerGroup={headerGroup} />
|
||||
))}
|
||||
<If condition={progressBarLoading}>
|
||||
<MaterialProgressBar />
|
||||
</If>
|
||||
</div>
|
||||
<ScrollSyncPane>
|
||||
<div className="thead">
|
||||
<div className={'thead-inner'}>
|
||||
{headerGroups.map((headerGroup, index) => (
|
||||
<TableHeaderGroup key={index} headerGroup={headerGroup} />
|
||||
))}
|
||||
<If condition={progressBarLoading}>
|
||||
<MaterialProgressBar />
|
||||
</If>
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
)
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,8 +41,10 @@ import WarehouseActivateDialog from '../containers/Dialogs/WarehouseActivateDial
|
||||
import CustomerOpeningBalanceDialog from '../containers/Dialogs/CustomerOpeningBalanceDialog';
|
||||
import VendorOpeningBalanceDialog from '../containers/Dialogs/VendorOpeningBalanceDialog';
|
||||
import ProjectFormDialog from '../containers/Projects/containers/ProjectFormDialog';
|
||||
import TaskFormDialog from '../containers/Projects/containers/TaskFormDialog';
|
||||
import TimeEntryFormDialog from '../containers/Projects/containers/TimeEntryFormDialog';
|
||||
import ProjectTaskFormDialog from '../containers/Projects/containers/ProjectTaskFormDialog';
|
||||
import ProjectTimeEntryFormDialog from '../containers/Projects/containers/ProjectTimeEntryFormDialog';
|
||||
import ProjectExpenseForm from '../containers/Projects/containers/ProjectExpenseForm';
|
||||
import EstimatedExpenseFormDialog from '../containers/Projects/containers/EstimatedExpenseFormDialog';
|
||||
|
||||
/**
|
||||
* Dialogs container.
|
||||
@@ -94,8 +96,10 @@ export default function DialogsContainer() {
|
||||
<CustomerOpeningBalanceDialog dialogName={'customer-opening-balance'} />
|
||||
<VendorOpeningBalanceDialog dialogName={'vendor-opening-balance'} />
|
||||
<ProjectFormDialog dialogName={'project-form'} />
|
||||
<TaskFormDialog dialogName={'task-form'} />
|
||||
<TimeEntryFormDialog dialogName={'time-entry-form'} />
|
||||
<ProjectTaskFormDialog dialogName={'project-task-form'} />
|
||||
<ProjectTimeEntryFormDialog dialogName={'project-time-entry-form'} />
|
||||
<ProjectExpenseForm dialogName={'project-expense-form'} />
|
||||
<EstimatedExpenseFormDialog dialogName={'estimated-expense-form'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import TransactionsLockingAlerts from '../TransactionsLocking/TransactionsLockin
|
||||
import WarehousesAlerts from '../Preferences/Warehouses/WarehousesAlerts';
|
||||
import WarehousesTransfersAlerts from '../WarehouseTransfers/WarehousesTransfersAlerts';
|
||||
import BranchesAlerts from '../Preferences/Branches/BranchesAlerts';
|
||||
import ProjectAlerts from '../../containers/Projects/containers/ProjectAlerts';
|
||||
|
||||
export default [
|
||||
...AccountsAlerts,
|
||||
@@ -50,4 +51,5 @@ export default [
|
||||
...WarehousesAlerts,
|
||||
...WarehousesTransfersAlerts,
|
||||
...BranchesAlerts,
|
||||
...ProjectAlerts,
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { FSelect } from 'components';
|
||||
import { FSelect } from '../../../components';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -8,7 +8,7 @@ import { FSelect } from 'components';
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
const taskModalChargeRenderer = (item, { handleClick, modifiers, query }) => {
|
||||
const chargeTypeItemRenderer = (item, { handleClick, modifiers, query }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
label={item.label}
|
||||
@@ -19,8 +19,8 @@ const taskModalChargeRenderer = (item, { handleClick, modifiers, query }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const taskModalChargeSelectProps = {
|
||||
itemRenderer: taskModalChargeRenderer,
|
||||
const chargeTypeSelectProps = {
|
||||
itemRenderer: chargeTypeItemRenderer,
|
||||
valueAccessor: 'value',
|
||||
labelAccessor: 'name',
|
||||
};
|
||||
@@ -30,22 +30,21 @@ const taskModalChargeSelectProps = {
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
export function TaskModalChargeSelect({ items, ...rest }) {
|
||||
export function ChangeTypesSelect({ items, ...rest }) {
|
||||
return (
|
||||
<FSelect
|
||||
{...taskModalChargeSelectProps}
|
||||
{...chargeTypeSelectProps}
|
||||
{...rest}
|
||||
items={items}
|
||||
input={TaskModalChargeSelectButton}
|
||||
input={ChargeTypeSelectButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
function TaskModalChargeSelectButton({ label }) {
|
||||
function ChargeTypeSelectButton({ label }) {
|
||||
return <Button text={label} />;
|
||||
}
|
||||
67
src/containers/Projects/components/ExpenseSelect.tsx
Normal file
67
src/containers/Projects/components/ExpenseSelect.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
20
src/containers/Projects/components/FInputGroupComponent.tsx
Normal file
20
src/containers/Projects/components/FInputGroupComponent.tsx
Normal 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} />;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { FSelect } from 'components';
|
||||
* @param {*} exactMatch
|
||||
* @returns
|
||||
*/
|
||||
const projectItemPredicate = (query, project, _index, exactMatch) => {
|
||||
const projectsItemPredicate = (query, project, _index, exactMatch) => {
|
||||
const normalizedTitle = project.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
@@ -28,7 +28,7 @@ const projectItemPredicate = (query, project, _index, exactMatch) => {
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
const projectItemRenderer = (project, { handleClick, modifiers, query }) => {
|
||||
const projectsItemRenderer = (project, { handleClick, modifiers, query }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
@@ -41,13 +41,13 @@ const projectItemRenderer = (project, { handleClick, modifiers, query }) => {
|
||||
};
|
||||
|
||||
const projectSelectProps = {
|
||||
itemPredicate: projectItemPredicate,
|
||||
itemRenderer: projectItemRenderer,
|
||||
itemPredicate: projectsItemPredicate,
|
||||
itemRenderer: projectsItemRenderer,
|
||||
valueAccessor: 'id',
|
||||
labelAccessor: 'name',
|
||||
};
|
||||
|
||||
export function ProjectSelect({ projects, ...rest }) {
|
||||
export function ProjectsSelect({ projects, ...rest }) {
|
||||
return (
|
||||
<FSelect
|
||||
items={projects}
|
||||
@@ -1,7 +1,8 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { FSelect } from '../../../../../components/Forms';
|
||||
import { FSelect } from 'components';
|
||||
|
||||
/**
|
||||
*
|
||||
5
src/containers/Projects/components/index.ts
Normal file
5
src/containers/Projects/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './ExpenseSelect';
|
||||
export * from './ChangeTypesSelect';
|
||||
export * from './TaskSelect';
|
||||
export * from './ProjectsSelect';
|
||||
export * from './FInputGroupComponent';
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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);
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
const ProjectDeleteAlert = React.lazy(() => import('./ProjectDeleteAlert'));
|
||||
|
||||
/**
|
||||
* Project alerts.
|
||||
*/
|
||||
export default [{ name: 'project-delete', component: ProjectDeleteAlert }];
|
||||
@@ -14,10 +14,11 @@ import {
|
||||
FormattedMessage as T,
|
||||
DashboardRowsHeightButton,
|
||||
} from 'components';
|
||||
import { TransactionSelect } from './components';
|
||||
import { ProjectTransactionsSelect } from './components';
|
||||
import withSettings from '../../../Settings/withSettings';
|
||||
import withSettingsActions from '../../../Settings/withSettingsActions';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import { projectTranslations } from './common';
|
||||
import { useProjectDetailContext } from './ProjectDetailProvider';
|
||||
import { compose } from 'utils';
|
||||
|
||||
@@ -40,7 +41,13 @@ function ProjectDetailActionsBar({
|
||||
|
||||
// Handle new transaction button click.
|
||||
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 = () => {
|
||||
@@ -50,11 +57,13 @@ function ProjectDetailActionsBar({
|
||||
};
|
||||
// Handle table row size change.
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('timesheets', 'tableSize', size);
|
||||
addSetting('timesheets', 'tableSize', size) &&
|
||||
addSetting('sales', 'tableSize', size) &&
|
||||
addSetting('purchases', 'tableSize', size);
|
||||
};
|
||||
|
||||
const handleTimeEntryBtnClick = () => {
|
||||
openDialog('time-entry-form', {
|
||||
openDialog('project-time-entry-form', {
|
||||
projectId,
|
||||
});
|
||||
};
|
||||
@@ -65,11 +74,8 @@ function ProjectDetailActionsBar({
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<TransactionSelect
|
||||
transactions={[
|
||||
{ name: 'Invoice', path: 'invoices/new' },
|
||||
{ name: 'Expenses', path: 'expenses/new' },
|
||||
]}
|
||||
<ProjectTransactionsSelect
|
||||
transactions={projectTranslations}
|
||||
onItemSelect={handleNewTransactionBtnClick}
|
||||
/>
|
||||
<Button
|
||||
@@ -105,8 +111,6 @@ function ProjectDetailActionsBar({
|
||||
initialValue={timesheetsTableSize}
|
||||
onChange={handleTableRowSizeChange}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
|
||||
</NavbarGroup>
|
||||
<NavbarGroup align={Alignment.RIGHT}>
|
||||
<Button
|
||||
|
||||
@@ -2,8 +2,9 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Tabs, Tab } from '@blueprintjs/core';
|
||||
|
||||
import ProjectTimesheet from './ProjectTimesheet';
|
||||
import ProjectTimeSheets from './ProjectTimeSheets';
|
||||
import ProjectPurchasesTable from './ProjectPurchasesTable';
|
||||
import ProjectSalesTable from './ProjectSalesTable';
|
||||
|
||||
/**
|
||||
* Project detail tabs.
|
||||
@@ -16,19 +17,24 @@ export default function ProjectDetailTabs() {
|
||||
animate={true}
|
||||
large={true}
|
||||
renderActiveTabPanelOnly={true}
|
||||
defaultSelectedTabId={'timesheet'}
|
||||
defaultSelectedTabId={'purchases'}
|
||||
>
|
||||
<Tab id="overview" title={intl.get('project_details.label.overview')} />
|
||||
<Tab
|
||||
id="timesheet"
|
||||
title={intl.get('project_details.label.timesheet')}
|
||||
panel={<ProjectTimesheet />}
|
||||
panel={<ProjectTimeSheets />}
|
||||
/>
|
||||
<Tab
|
||||
id="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')} />
|
||||
</Tabs>
|
||||
</ProjectTabsContent>
|
||||
@@ -42,6 +48,10 @@ const ProjectTabsContent = styled.div`
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #d2dce2;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.bp3-large > .bp3-tab {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
@@ -59,9 +69,11 @@ const ProjectTabsContent = styled.div`
|
||||
}
|
||||
}
|
||||
.bp3-tab-panel {
|
||||
margin-top: 20px;
|
||||
/* margin: 20px 32px; */
|
||||
/* margin: 20px; */
|
||||
/* margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 25px;
|
||||
padding: 0 25px; */
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -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)``;
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
import { calculateStatus } from 'utils';
|
||||
|
||||
/**
|
||||
* Timesheets header
|
||||
* Project Timesheets header
|
||||
* @returns
|
||||
*/
|
||||
export default function TimesheetsHeader() {
|
||||
export function ProjectTimesheetsHeader() {
|
||||
return (
|
||||
<DetailFinancialSection>
|
||||
<DetailFinancialCard label={'Project estimate'} value={'3.14'} />
|
||||
@@ -4,42 +4,25 @@ import styled from 'styled-components';
|
||||
import { DataTable } from 'components';
|
||||
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
|
||||
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 { useMemorizedColumnsWidths } from 'hooks';
|
||||
import withSettings from '../../../../Settings/withSettings';
|
||||
|
||||
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.
|
||||
* @returns
|
||||
*/
|
||||
function TimesheetsTable({
|
||||
function ProjectTimesheetsTableRoot({
|
||||
// #withSettings
|
||||
timesheetsTableSize,
|
||||
}) {
|
||||
// Retrieve timesheet table columns.
|
||||
const columns = useTimesheetColumns();
|
||||
// Retrieve project timesheet table columns.
|
||||
const columns = useProjectTimesheetColumns();
|
||||
|
||||
// Handle delete timesheet.
|
||||
const handleDeleteTimesheet = () => {};
|
||||
@@ -49,12 +32,9 @@ function TimesheetsTable({
|
||||
useMemorizedColumnsWidths(TABLES.TIMESHEETS);
|
||||
|
||||
return (
|
||||
<TimesheetDataTable
|
||||
<ProjectTimesheetDataTable
|
||||
columns={columns}
|
||||
data={Timesheet}
|
||||
// loading={}
|
||||
// headerLoading={}
|
||||
// progressBarLoading={}
|
||||
data={[]}
|
||||
manualSortBy={true}
|
||||
noInitialFetch={true}
|
||||
sticky={true}
|
||||
@@ -71,13 +51,13 @@ function TimesheetsTable({
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default compose(
|
||||
export const ProjectTimesheetsTable = compose(
|
||||
withSettings(({ timesheetsSettings }) => ({
|
||||
timesheetsTableSize: timesheetsSettings?.tableSize,
|
||||
})),
|
||||
)(TimesheetsTable);
|
||||
)(ProjectTimesheetsTableRoot);
|
||||
|
||||
const TimesheetDataTable = styled(DataTable)`
|
||||
const ProjectTimesheetDataTable = styled(DataTable)`
|
||||
.table {
|
||||
.thead .tr .th {
|
||||
.resizer {
|
||||
@@ -87,38 +67,38 @@ const TimesheetDataTable = styled(DataTable)`
|
||||
|
||||
.tbody {
|
||||
.tr .td {
|
||||
padding: 0.5rem 0.8rem;
|
||||
}
|
||||
|
||||
.avatar.td {
|
||||
.avatar {
|
||||
display: inline-block;
|
||||
background: #adbcc9;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
.cell-inner {
|
||||
.avatar {
|
||||
display: inline-block;
|
||||
background: #adbcc9;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
|
||||
&[data-size='medium'] {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&[data-size='small'] {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
line-height: 25px;
|
||||
font-size: 12px;
|
||||
&[data-size='medium'] {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&[data-size='small'] {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
line-height: 25px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-size--small {
|
||||
.tbody .tr {
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
.table-size--small {
|
||||
.tbody .tr {
|
||||
height: 45px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,15 +1,13 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
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 { safeCallback, firstLettersArgs } from 'utils';
|
||||
import { chain } from 'lodash';
|
||||
|
||||
/**
|
||||
* Table actions cell.
|
||||
*/
|
||||
|
||||
export function ActionsMenu({
|
||||
payload: { onDelete, onViewDetails },
|
||||
row: { original },
|
||||
@@ -78,43 +76,3 @@ const TimesheetDescription = styled.span`
|
||||
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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -29,17 +29,16 @@ export const FinancialProgressBar = ({ ...rest }) => {
|
||||
|
||||
const FinancialSectionWrap = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 20px 20px 20px;
|
||||
margin: 22px 32px;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
const FinancialSectionCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
flex-shrink: 1;
|
||||
border-radius: 3px;
|
||||
width: 220px;
|
||||
width: 230px;
|
||||
height: 116px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
|
||||
@@ -47,7 +46,6 @@ const FinancialSectionCard = styled.div`
|
||||
|
||||
const FinancialSectionCardContent = styled.div`
|
||||
margin: 16px;
|
||||
/* flex-direction: column; */
|
||||
`;
|
||||
|
||||
const FinancialCardWrap = styled.div``;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Icon, FormattedMessage as T } from 'components';
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
const transactionItemRenderer = (
|
||||
const projectTransactionItemRenderer = (
|
||||
transaction,
|
||||
{ handleClick, modifiers, query },
|
||||
) => {
|
||||
@@ -29,8 +29,8 @@ const transactionItemRenderer = (
|
||||
);
|
||||
};
|
||||
|
||||
const transactionSelectProps = {
|
||||
itemRenderer: transactionItemRenderer,
|
||||
const projectTransactionSelectProps = {
|
||||
itemRenderer: projectTransactionItemRenderer,
|
||||
filterable: false,
|
||||
popoverProps: {
|
||||
minimal: true,
|
||||
@@ -43,13 +43,13 @@ const transactionSelectProps = {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Project transactions select
|
||||
* @param
|
||||
* @returns
|
||||
*/
|
||||
export function TransactionSelect({ transactions, ...rest }) {
|
||||
export function ProjectTransactionsSelect({ transactions, ...rest }) {
|
||||
return (
|
||||
<Select {...transactionSelectProps} items={transactions} {...rest}>
|
||||
<Select {...projectTransactionSelectProps} items={transactions} {...rest}>
|
||||
<Button
|
||||
minimal={true}
|
||||
icon={<Icon icon={'plus'} />}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './ProjectSelect';
|
||||
export * from './TransactionSelect';
|
||||
export * from './ProjectTransactionsSelect';
|
||||
export * from './FinancialSection';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import ProjectDetailActionsBar from './ProjectDetailActionsBar';
|
||||
import ProjectDetailTabs from './ProjectDetailTabs';
|
||||
@@ -20,7 +20,7 @@ function ProjectTabs({
|
||||
state: { projectName, projectId },
|
||||
} = useLocation();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
changePageTitle(projectName);
|
||||
}, [changePageTitle, projectName]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -1,19 +1,18 @@
|
||||
import * as Yup from 'yup';
|
||||
import intl from 'react-intl-universal';
|
||||
import { DATATYPES_LENGTH } from 'common/dataTypes';
|
||||
|
||||
const Schema = Yup.object().shape({
|
||||
contact: Yup.string().label(intl.get('project.schema.label.contact')),
|
||||
projectName: Yup.string()
|
||||
contact_id: Yup.string().label(intl.get('project.schema.label.contact')),
|
||||
name: Yup.string()
|
||||
.label(intl.get('project.schema.label.project_name'))
|
||||
.required(),
|
||||
projectDeadline: Yup.date()
|
||||
deadline: Yup.date()
|
||||
.label(intl.get('project.schema.label.deadline'))
|
||||
.required(),
|
||||
projectState: Yup.boolean().label(
|
||||
published: Yup.boolean().label(
|
||||
intl.get('project.schema.label.project_state'),
|
||||
),
|
||||
projectCost: Yup.number().label(
|
||||
cost_estimate: Yup.number().label(
|
||||
intl.get('project.schema.label.project_cost'),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -3,20 +3,21 @@ import React from 'react';
|
||||
import moment from 'moment';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { AppToaster } from 'components';
|
||||
import ProjectFormContent from './ProjectFormContent';
|
||||
import { CreateProjectFormSchema } from './ProjectForm.schema';
|
||||
import { useProjectFormContext } from './ProjectFormProvider';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
import { compose, transformToForm } from 'utils';
|
||||
|
||||
const defaultInitialValues = {
|
||||
contact: '',
|
||||
projectName: '',
|
||||
projectDeadline: moment(new Date()).format('YYYY-MM-DD'),
|
||||
projectState: false,
|
||||
projectCost: '',
|
||||
contact_id: '',
|
||||
name: '',
|
||||
deadline: moment(new Date()).format('YYYY-MM-DD'),
|
||||
published: false,
|
||||
cost_estimate: '',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -28,20 +29,37 @@ function ProjectForm({
|
||||
closeDialog,
|
||||
}) {
|
||||
// project form dialog context.
|
||||
const { dialogName } = useProjectFormContext();
|
||||
const {
|
||||
dialogName,
|
||||
project,
|
||||
isNewMode,
|
||||
projectId,
|
||||
createProjectMutate,
|
||||
editProjectMutate,
|
||||
} = useProjectFormContext();
|
||||
|
||||
// Initial form values
|
||||
const initialValues = {
|
||||
...defaultInitialValues,
|
||||
...transformToForm(project, defaultInitialValues),
|
||||
};
|
||||
|
||||
// Handles the form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
const form = {};
|
||||
setSubmitting(true);
|
||||
const form = { ...values };
|
||||
|
||||
// Handle request response success.
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -53,6 +71,12 @@ function ProjectForm({
|
||||
}) => {
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
if (isNewMode) {
|
||||
createProjectMutate(form).then(onSuccess).catch(onError);
|
||||
} else {
|
||||
editProjectMutate([projectId, form]).then(onSuccess).catch(onError);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -40,7 +40,7 @@ function ProjectFormFields() {
|
||||
return (
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
{/*------------ Contact -----------*/}
|
||||
<FastField name={'contact'}>
|
||||
<FastField name={'contact_id'}>
|
||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={intl.get('projects.dialog.contact')}
|
||||
@@ -53,7 +53,7 @@ function ProjectFormFields() {
|
||||
selectedContactId={value}
|
||||
defaultSelectText={'Select Contact Account'}
|
||||
onContactSelected={(customer) => {
|
||||
form.setFieldValue('contact', customer.id);
|
||||
form.setFieldValue('contact_id', customer.id);
|
||||
}}
|
||||
allowCreate={true}
|
||||
popoverFill={true}
|
||||
@@ -64,19 +64,20 @@ function ProjectFormFields() {
|
||||
{/*------------ Project Name -----------*/}
|
||||
<FFormGroup
|
||||
label={intl.get('projects.dialog.project_name')}
|
||||
name={'projectName'}
|
||||
name={'name'}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
>
|
||||
<FInputGroup name="projectName" />
|
||||
<FInputGroup name="name" />
|
||||
</FFormGroup>
|
||||
{/*------------ DeadLine -----------*/}
|
||||
<FFormGroup
|
||||
label={intl.get('projects.dialog.deadline')}
|
||||
name={'projectDeadline'}
|
||||
name={'deadline'}
|
||||
className={classNames(CLASSES.FILL, 'form-group--date')}
|
||||
>
|
||||
<FDateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
name="projectDeadline"
|
||||
name="deadline"
|
||||
formatDate={(date) => date.toLocaleString()}
|
||||
popoverProps={{
|
||||
position: Position.BOTTOM,
|
||||
@@ -86,22 +87,23 @@ function ProjectFormFields() {
|
||||
</FFormGroup>
|
||||
|
||||
{/*------------ CheckBox -----------*/}
|
||||
<FFormGroup name={'projectState'}>
|
||||
<FFormGroup name={'published'}>
|
||||
<FCheckbox
|
||||
name="projectState"
|
||||
name="published"
|
||||
label={intl.get('projects.dialog.calculator_expenses')}
|
||||
/>
|
||||
</FFormGroup>
|
||||
{/*------------ Cost Estimate -----------*/}
|
||||
<FFormGroup
|
||||
name={'projectCost'}
|
||||
name={'cost_estimate'}
|
||||
label={intl.get('projects.dialog.cost_estimate')}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
>
|
||||
<ControlGroup>
|
||||
<InputPrependText text={'USD'} />
|
||||
<FMoneyInputGroup
|
||||
disabled={values.projectState}
|
||||
name={'project_cost'}
|
||||
disabled={values.published}
|
||||
name={'cost_estimate'}
|
||||
allowDecimals={true}
|
||||
allowNegativeValue={true}
|
||||
/>
|
||||
|
||||
@@ -15,12 +15,12 @@ function ProjectFormFloatingActions({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
// project form dialog context.
|
||||
const { dialogName } = useProjectFormContext();
|
||||
|
||||
// Formik context.
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
// project form dialog context.
|
||||
const { dialogName } = useProjectFormContext();
|
||||
|
||||
// Handle close button click.
|
||||
const handleCancelBtnClick = () => {
|
||||
closeDialog(dialogName);
|
||||
@@ -29,7 +29,7 @@ function ProjectFormFloatingActions({
|
||||
return (
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
|
||||
<Button onClick={handleCancelBtnClick} style={{ minWidth: '85px' }}>
|
||||
<T id={'cancel'} />
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useCustomers } from 'hooks/query';
|
||||
import { useCreateProject, useEditProject, useProject } from '../../hooks';
|
||||
import { DialogContent } from 'components';
|
||||
|
||||
const ProjectFormContext = React.createContext();
|
||||
@@ -15,20 +16,36 @@ function ProjectFormProvider({
|
||||
projectId,
|
||||
...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
|
||||
const {
|
||||
data: { customers },
|
||||
isLoading: isCustomersLoading,
|
||||
} = useCustomers({ page_size: 10000 });
|
||||
|
||||
const isNewMode = !projectId;
|
||||
|
||||
// State provider.
|
||||
const provider = {
|
||||
customers,
|
||||
dialogName,
|
||||
project,
|
||||
projectId,
|
||||
isNewMode,
|
||||
createProjectMutate,
|
||||
editProjectMutate,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent isLoading={isCustomersLoading}>
|
||||
<DialogContent isLoading={isCustomersLoading || isProjectLoading}>
|
||||
<ProjectFormContext.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
|
||||
@@ -14,13 +14,19 @@ const ProjectDialogContent = React.lazy(
|
||||
*/
|
||||
function ProjectFormDialog({
|
||||
dialogName,
|
||||
payload: { projectId = null },
|
||||
payload: { projectId = null, action },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<ProjectFormDialogRoot
|
||||
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}
|
||||
autoFocus={true}
|
||||
canEscapeKeyClose={true}
|
||||
@@ -39,6 +45,7 @@ const ProjectFormDialogRoot = styled(Dialog)`
|
||||
.bp3-dialog-body {
|
||||
.bp3-form-group {
|
||||
margin-bottom: 15px;
|
||||
margin-top: 15px;
|
||||
|
||||
label.bp3-label {
|
||||
margin-bottom: 3px;
|
||||
|
||||
@@ -7,8 +7,10 @@ const Schema = Yup.object().shape({
|
||||
.label(intl.get('task.schema.label.task_name'))
|
||||
.required(),
|
||||
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')),
|
||||
});
|
||||
|
||||
export const CreateTaskFormSchema = Schema;
|
||||
export const CreateProjectTaskFormSchema = Schema;
|
||||
@@ -1,10 +1,10 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import { Formik } from 'formik';
|
||||
import { CreateTaskFormSchema } from './TaskForm.schema';
|
||||
import { useTaskFormContext } from './TaskFormProvider';
|
||||
import { CreateProjectTaskFormSchema } from './ProjectTaskForm.schema';
|
||||
import { useProjectTaskFormContext } from './ProjectTaskFormProvider';
|
||||
import { AppToaster } from 'components';
|
||||
import TaskFormContent from './TaskFormContent';
|
||||
import ProjectTaskFormContent from './ProjectTaskFormContent';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
@@ -12,20 +12,20 @@ import { compose } from 'utils';
|
||||
const defaultInitialValues = {
|
||||
taskName: '',
|
||||
taskHouse: '00:00',
|
||||
taskCharge: 'Hourly rate',
|
||||
taskamount: '100000000',
|
||||
taskCharge: 'hourly_rate',
|
||||
taskamount: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Task form.
|
||||
* Project task form.
|
||||
* @returns
|
||||
*/
|
||||
function TaskForm({
|
||||
function ProjectTaskForm({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
// task form dialog context.
|
||||
const { dialogName } = useTaskFormContext();
|
||||
const { dialogName } = useProjectTaskFormContext();
|
||||
|
||||
// Initial form values
|
||||
const initialValues = {
|
||||
@@ -51,12 +51,12 @@ function TaskForm({
|
||||
|
||||
return (
|
||||
<Formik
|
||||
validationSchema={CreateTaskFormSchema}
|
||||
validationSchema={CreateProjectTaskFormSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
component={TaskFormContent}
|
||||
component={ProjectTaskFormContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogActions)(TaskForm);
|
||||
export default compose(withDialogActions)(ProjectTaskForm);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useFormikContext } from 'formik';
|
||||
@@ -9,29 +10,28 @@ import {
|
||||
Row,
|
||||
FormattedMessage as T,
|
||||
} from 'components';
|
||||
import { modalChargeOptions } from '../../../../common/modalChargeOptions';
|
||||
|
||||
import { TaskModalChargeSelect } from './components';
|
||||
import { taskChargeOptions } from 'containers/Projects/containers/common/modalChargeOptions';
|
||||
import { ChangeTypesSelect } from '../../components';
|
||||
|
||||
/**
|
||||
* Task form fields.
|
||||
* Project task form fields.
|
||||
* @returns
|
||||
*/
|
||||
function TaskFormFields() {
|
||||
function ProjectTaskFormFields() {
|
||||
// Formik context.
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return (
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
{/*------------ 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" />
|
||||
</FFormGroup>
|
||||
{/*------------ Estimated Hours -----------*/}
|
||||
<Row>
|
||||
<Col xs={4}>
|
||||
<FFormGroup
|
||||
label={<T id={'task.dialog.estimated_hours'} />}
|
||||
label={<T id={'project_task.dialog.estimated_hours'} />}
|
||||
name={'taskHouse'}
|
||||
>
|
||||
<FInputGroup name="taskHouse" />
|
||||
@@ -42,16 +42,19 @@ function TaskFormFields() {
|
||||
<FFormGroup
|
||||
name={'taskCharge'}
|
||||
className={'form-group--select-list'}
|
||||
label={<T id={'task.dialog.charge'} />}
|
||||
label={<T id={'project_task.dialog.charge'} />}
|
||||
>
|
||||
<ControlGroup>
|
||||
<TaskModalChargeSelect
|
||||
<ChangeTypesSelect
|
||||
name="taskCharge"
|
||||
items={modalChargeOptions}
|
||||
items={taskChargeOptions}
|
||||
popoverProps={{ minimal: true }}
|
||||
filterable={false}
|
||||
/>
|
||||
<FInputGroup name="taskamount" />
|
||||
<FInputGroup
|
||||
name="taskamount"
|
||||
disabled={values?.taskCharge === 'Non-chargeable'}
|
||||
/>
|
||||
</ControlGroup>
|
||||
</FFormGroup>
|
||||
</Col>
|
||||
@@ -59,21 +62,22 @@ function TaskFormFields() {
|
||||
{/*------------ Estimated Amount -----------*/}
|
||||
<EstimatedAmountBase>
|
||||
<EstimatedAmountContent>
|
||||
<T id={'task.dialog.estimated_amount'} />
|
||||
<EstimateAmount>$100000</EstimateAmount>
|
||||
<T id={'project_task.dialog.estimated_amount'} />
|
||||
<EstimateAmount>0.00</EstimateAmount>
|
||||
</EstimatedAmountContent>
|
||||
</EstimatedAmountBase>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TaskFormFields;
|
||||
export default ProjectTaskFormFields;
|
||||
|
||||
const EstimatedAmountBase = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
font-size: 12px;
|
||||
/* opacity: 0.7; */
|
||||
font-size: 14px;
|
||||
line-height: 1.5rem;
|
||||
opacity: 0.75;
|
||||
`;
|
||||
|
||||
const EstimatedAmountContent = styled.span`
|
||||
@@ -82,7 +86,7 @@ const EstimatedAmountContent = styled.span`
|
||||
`;
|
||||
|
||||
const EstimateAmount = styled.span`
|
||||
font-size: 13px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import { useTaskFormContext } from './TaskFormProvider';
|
||||
import { useProjectTaskFormContext } from './ProjectTaskFormProvider';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
@@ -11,7 +11,7 @@ import { compose } from 'utils';
|
||||
* Task form floating actions.
|
||||
* @returns
|
||||
*/
|
||||
function TaskFormFloatingActions({
|
||||
function ProjectTaskFormFloatingActions({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
@@ -19,7 +19,7 @@ function TaskFormFloatingActions({
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
// Task form dialog context.
|
||||
const { dialogName } = useTaskFormContext();
|
||||
const { dialogName } = useProjectTaskFormContext();
|
||||
|
||||
// Handle close button click.
|
||||
const handleCancelBtnClick = () => {
|
||||
@@ -45,4 +45,4 @@ function TaskFormFloatingActions({
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogActions)(TaskFormFloatingActions);
|
||||
export default compose(withDialogActions)(ProjectTaskFormFloatingActions);
|
||||
@@ -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 };
|
||||
@@ -5,30 +5,32 @@ import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
|
||||
import withDialogRedux from 'components/DialogReduxConnect';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const TaskFormDialogContent = React.lazy(
|
||||
() => import('./TaskFormDialogContent'),
|
||||
const ProjectTaskFormDialogContent = React.lazy(
|
||||
() => import('./ProjectTaskFormDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Task form dialog.
|
||||
* Project task form dialog.
|
||||
* @returns
|
||||
*/
|
||||
function TaskFormDialog({ dialogName, payload: { taskId = null }, isOpen }) {
|
||||
function ProjectTaskFormDialog({
|
||||
dialogName,
|
||||
payload: { taskId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={intl.get('task.dialog.new_task')}
|
||||
title={intl.get('project_task.dialog.new_task')}
|
||||
isOpen={isOpen}
|
||||
autoFocus={true}
|
||||
canEscapeKeyClose={true}
|
||||
style={{ width: '500px' }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<TaskFormDialogContent dialogName={dialogName} task={taskId} />
|
||||
<ProjectTaskFormDialogContent dialogName={dialogName} task={taskId} />
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default compose(withDialogRedux())(TaskFormDialog);
|
||||
|
||||
const TaskFormDialogRoot = styled(Dialog)``;
|
||||
export default compose(withDialogRedux())(ProjectTaskFormDialog);
|
||||
@@ -3,17 +3,19 @@ import intl from 'react-intl-universal';
|
||||
import { DATATYPES_LENGTH } from 'common/dataTypes';
|
||||
|
||||
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()
|
||||
.label(intl.get('time_entry.schema.label.project_name'))
|
||||
.label(intl.get('project_time_entry.schema.label.project_name'))
|
||||
.required(),
|
||||
taskId: Yup.string()
|
||||
.label(intl.get('time_entry.schema.label.task_name'))
|
||||
.label(intl.get('project_time_entry.schema.label.task_name'))
|
||||
.required(),
|
||||
description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
|
||||
duration: Yup.string()
|
||||
.label(intl.get('time_entry.schema.label.duration'))
|
||||
.label(intl.get('project_time_entry.schema.label.duration'))
|
||||
.required(),
|
||||
});
|
||||
|
||||
export const CreateTimeEntryFormSchema = Schema;
|
||||
export const CreateProjectTimeEntryFormSchema = Schema;
|
||||
@@ -5,9 +5,9 @@ import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { AppToaster } from 'components';
|
||||
|
||||
import TimeEntryFormContent from './TimeEntryFormContent';
|
||||
import { CreateTimeEntryFormSchema } from './TimeEntryForm.schema';
|
||||
import { useTimeEntryFormContext } from './TimeEntryFormProvider';
|
||||
import ProjectTimeEntryFormContent from './ProjectTimeEntryFormContent';
|
||||
import { CreateProjectTimeEntryFormSchema } from './ProjectTimeEntryForm.schema';
|
||||
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
@@ -21,15 +21,15 @@ const defaultInitialValues = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Time entry form.
|
||||
* Project Time entry form.
|
||||
* @returns
|
||||
*/
|
||||
function TimeEntryForm({
|
||||
function ProjectTimeEntryForm({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
// time entry form dialog context.
|
||||
const { dialogName } = useTimeEntryFormContext();
|
||||
const { dialogName } = useProjectTimeEntryFormContext();
|
||||
|
||||
// Initial form values
|
||||
const initialValues = {
|
||||
@@ -58,12 +58,12 @@ function TimeEntryForm({
|
||||
|
||||
return (
|
||||
<Formik
|
||||
validationSchema={CreateTimeEntryFormSchema}
|
||||
validationSchema={CreateProjectTimeEntryFormSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
component={TimeEntryFormContent}
|
||||
component={ProjectTimeEntryFormContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogActions)(TimeEntryForm);
|
||||
export default compose(withDialogActions)(ProjectTimeEntryForm);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -14,25 +14,25 @@ import {
|
||||
FieldRequiredHint,
|
||||
FormattedMessage as T,
|
||||
} from 'components';
|
||||
import { ProjectSelect, TaskSelect } from './components';
|
||||
import { TaskSelect, ProjectsSelect } from '../../components';
|
||||
import { momentFormatter } from 'utils';
|
||||
|
||||
/**
|
||||
* Time entry form fields.
|
||||
* Project time entry form fields.
|
||||
* @returns
|
||||
*/
|
||||
function TimeEntryFormFields() {
|
||||
function ProjectTimeEntryFormFields() {
|
||||
return (
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
{/*------------ Project -----------*/}
|
||||
<FFormGroup
|
||||
name={'projectId'}
|
||||
label={<T id={'time_entry.dialog.project'} />}
|
||||
label={<T id={'project_time_entry.dialog.project'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
<ProjectSelect
|
||||
name={'tesc'}
|
||||
<ProjectsSelect
|
||||
name={'projectId'}
|
||||
projects={[]}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
@@ -40,7 +40,7 @@ function TimeEntryFormFields() {
|
||||
{/*------------ Task -----------*/}
|
||||
<FFormGroup
|
||||
name={'taskId'}
|
||||
label={<T id={'time_entry.dialog.task'} />}
|
||||
label={<T id={'project_time_entry.dialog.task'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
@@ -53,7 +53,7 @@ function TimeEntryFormFields() {
|
||||
|
||||
{/*------------ Duration -----------*/}
|
||||
<FFormGroup
|
||||
label={intl.get('time_entry.dialog.duration')}
|
||||
label={intl.get('project_time_entry.dialog.duration')}
|
||||
name={'duration'}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
>
|
||||
@@ -61,7 +61,7 @@ function TimeEntryFormFields() {
|
||||
</FFormGroup>
|
||||
{/*------------ Date -----------*/}
|
||||
<FFormGroup
|
||||
label={intl.get('time_entry.dialog.date')}
|
||||
label={intl.get('project_time_entry.dialog.date')}
|
||||
name={'date'}
|
||||
className={classNames(CLASSES.FILL, 'form-group--date')}
|
||||
>
|
||||
@@ -78,20 +78,13 @@ function TimeEntryFormFields() {
|
||||
{/*------------ Description -----------*/}
|
||||
<FFormGroup
|
||||
name={'description'}
|
||||
label={intl.get('time_entry.dialog.description')}
|
||||
label={intl.get('project_time_entry.dialog.description')}
|
||||
className={'form-group--description'}
|
||||
>
|
||||
<FTextArea name={'description'} />
|
||||
{/* <FEditableText
|
||||
multiline={true}
|
||||
// minLines={1.78}
|
||||
// maxLines={1.78}
|
||||
name={'description'}
|
||||
placeholder=""
|
||||
/> */}
|
||||
</FFormGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimeEntryFormFields;
|
||||
export default ProjectTimeEntryFormFields;
|
||||
@@ -3,20 +3,20 @@ import React from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import { useTimeEntryFormContext } from './TimeEntryFormProvider';
|
||||
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Time entry form floating actions.
|
||||
* Projcet time entry form floating actions.
|
||||
* @returns
|
||||
*/
|
||||
function TimeEntryFormFloatingActions({
|
||||
function ProjectTimeEntryFormFloatingActions({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
// time entry form dialog context.
|
||||
const { dialogName } = useTimeEntryFormContext();
|
||||
const { dialogName } = useProjectTimeEntryFormContext();
|
||||
|
||||
// Formik context.
|
||||
const { isSubmitting } = useFormikContext();
|
||||
@@ -45,4 +45,4 @@ function TimeEntryFormFloatingActions({
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogActions)(TimeEntryFormFloatingActions);
|
||||
export default compose(withDialogActions)(ProjectTimeEntryFormFloatingActions);
|
||||
@@ -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 };
|
||||
@@ -4,42 +4,42 @@ import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
|
||||
import withDialogRedux from 'components/DialogReduxConnect';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const TimeEntryFormDialogContent = React.lazy(
|
||||
() => import('./TimeEntryFormDialogContent'),
|
||||
const ProjectTimeEntryFormDialogContent = React.lazy(
|
||||
() => import('./ProjectTimeEntryFormDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Time entry form dialog.
|
||||
* Project time entry form dialog.
|
||||
* @returns
|
||||
*/
|
||||
function TimeEntryFormDialog({
|
||||
function ProjectTimeEntryFormDialog({
|
||||
dialogName,
|
||||
isOpen,
|
||||
payload: { projectId = null, timeEntryId = null },
|
||||
}) {
|
||||
return (
|
||||
<TimeEntryFormDialogRoot
|
||||
<ProjectTimeEntryFormDialogRoot
|
||||
name={dialogName}
|
||||
title={<T id={'time_entry.dialog.label'} />}
|
||||
title={<T id={'project_time_entry.dialog.label'} />}
|
||||
isOpen={isOpen}
|
||||
autoFocus={true}
|
||||
canEscapeKeyClose={true}
|
||||
style={{ width: '400px' }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<TimeEntryFormDialogContent
|
||||
<ProjectTimeEntryFormDialogContent
|
||||
dialogName={dialogName}
|
||||
project={projectId}
|
||||
timeEntry={timeEntryId}
|
||||
/>
|
||||
</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-form-group {
|
||||
margin-bottom: 15px;
|
||||
@@ -1,3 +1,4 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
@@ -5,51 +6,17 @@ import { DataTable } from 'components';
|
||||
import { TABLES } from 'common/tables';
|
||||
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
|
||||
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
|
||||
import ProjectsEmptyStatus from './ProjectsEmptyStatus';
|
||||
import { useProjectsListContext } from './ProjectsListProvider';
|
||||
import { useMemorizedColumnsWidths } from 'hooks';
|
||||
import { useProjectsListColumns, ActionsMenu } from './components';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withAlertsActions from 'containers/Alert/withAlertActions';
|
||||
import withProjectsActions from './withProjectsActions';
|
||||
import withSettings from '../../../Settings/withSettings';
|
||||
|
||||
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.
|
||||
* @returns
|
||||
@@ -58,11 +25,23 @@ function ProjectsDataTable({
|
||||
// #withDial
|
||||
openDialog,
|
||||
|
||||
// #withAlertsActions
|
||||
openAlert,
|
||||
|
||||
// #withSettings
|
||||
projectsTableSize,
|
||||
}) {
|
||||
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.
|
||||
const columns = useProjectsListColumns();
|
||||
|
||||
@@ -78,12 +57,13 @@ function ProjectsDataTable({
|
||||
const handleEditProject = (project) => {
|
||||
openDialog('project-form', {
|
||||
projectId: project.id,
|
||||
action: 'edit',
|
||||
});
|
||||
};
|
||||
|
||||
// Handle new task button click.
|
||||
const handleNewTaskButtonClick = () => {
|
||||
openDialog('task-form');
|
||||
openDialog('project-task-form');
|
||||
};
|
||||
|
||||
// Local storage memorizing columns widths.
|
||||
@@ -98,13 +78,18 @@ function ProjectsDataTable({
|
||||
});
|
||||
};
|
||||
|
||||
// Display project empty status instead of the table.
|
||||
if (isEmptyStatus) {
|
||||
return <ProjectsEmptyStatus />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectsTable
|
||||
columns={columns}
|
||||
data={projects}
|
||||
// loading={}
|
||||
// headerLoading={}
|
||||
// progressBarLoading={}
|
||||
loading={isProjectsLoading}
|
||||
headerLoading={isProjectsLoading}
|
||||
progressBarLoading={isProjectsFetching}
|
||||
manualSortBy={true}
|
||||
noInitialFetch={true}
|
||||
sticky={true}
|
||||
@@ -119,6 +104,7 @@ function ProjectsDataTable({
|
||||
payload={{
|
||||
onViewDetails: handleViewDetailProject,
|
||||
onEdit: handleEditProject,
|
||||
onDelete: handleDeleteProject,
|
||||
onNewTask: handleNewTaskButtonClick,
|
||||
}}
|
||||
/>
|
||||
@@ -127,6 +113,7 @@ function ProjectsDataTable({
|
||||
|
||||
export default compose(
|
||||
withDialogActions,
|
||||
withAlertsActions,
|
||||
withProjectsActions,
|
||||
withSettings(({ projectSettings }) => ({
|
||||
projectsTableSize: projectSettings?.tableSize,
|
||||
@@ -138,27 +125,28 @@ const ProjectsTable = styled(DataTable)`
|
||||
.tr .td {
|
||||
padding: 0.5rem 0.8rem;
|
||||
}
|
||||
|
||||
.avatar.td {
|
||||
.avatar {
|
||||
display: inline-block;
|
||||
background: #adbcc9;
|
||||
border-radius: 8%;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
.cell-inner {
|
||||
.avatar {
|
||||
display: inline-block;
|
||||
background: #adbcc9;
|
||||
border-radius: 8%;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
|
||||
&[data-size='medium'] {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&[data-size='small'] {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
line-height: 25px;
|
||||
font-size: 12px;
|
||||
&[data-size='medium'] {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
line-height: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&[data-size='small'] {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
line-height: 25px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -1,7 +1,9 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useResourceViews, useResourceMeta } from 'hooks/query';
|
||||
import DashboardInsider from '../../../../components/Dashboard/DashboardInsider';
|
||||
import { useProjects } from '../../hooks';
|
||||
|
||||
const ProjectsListContext = React.createContext();
|
||||
|
||||
@@ -14,16 +16,32 @@ function ProjectsListProvider({ query, tableStateChanged, ...props }) {
|
||||
const { data: projectsViews, isLoading: isViewsLoading } =
|
||||
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.
|
||||
const provider = {
|
||||
projects,
|
||||
|
||||
projectsViews,
|
||||
|
||||
isProjectsLoading,
|
||||
isProjectsFetching,
|
||||
isViewsLoading,
|
||||
|
||||
isEmptyStatus,
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
// loading={isViewsLoading}
|
||||
name={'projects'}
|
||||
>
|
||||
<DashboardInsider loading={isViewsLoading} name={'projects'}>
|
||||
<ProjectsListContext.Provider value={provider} {...props} />
|
||||
</DashboardInsider>
|
||||
);
|
||||
|
||||
@@ -102,13 +102,15 @@ export const ActionsMenu = ({
|
||||
export const ProjectsAccessor = (row) => (
|
||||
<ProjectItemsWrap>
|
||||
<ProjectItemsHeader>
|
||||
<ProjectItemContactName>{row.display_name}</ProjectItemContactName>
|
||||
<ProjectItemContactName>
|
||||
{row.contact_display_name}
|
||||
</ProjectItemContactName>
|
||||
<ProjectItemProjectName>{row.name}</ProjectItemProjectName>
|
||||
</ProjectItemsHeader>
|
||||
<ProjectItemDescription>
|
||||
<FormatDate value={row.deadline} />
|
||||
<FormatDate value={row.deadline_formatted} />
|
||||
{intl.get('projects.label.cost_estimate', {
|
||||
value: row.cost_estimate,
|
||||
value: row.cost_estimate_formatted,
|
||||
})}
|
||||
</ProjectItemDescription>
|
||||
</ProjectItemsWrap>
|
||||
@@ -175,10 +177,10 @@ const ProjectItemDescription = styled.div`
|
||||
const ProjectStatusRoot = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* justify-content: flex-end; */
|
||||
margin-right: 0.5rem;
|
||||
flex-direction: row-reverse;
|
||||
`;
|
||||
|
||||
const ProjectStatusTaskAmount = styled.div`
|
||||
text-align: right;
|
||||
font-weight: 400;
|
||||
@@ -198,6 +200,7 @@ const ProjectProgressBar = styled(ProgressBar)`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StatusTag = styled(Tag)`
|
||||
min-width: 65px;
|
||||
text-align: center;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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')} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './ProjectSelect';
|
||||
export * from './TaskSelect';
|
||||
@@ -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' },
|
||||
];
|
||||
125
src/containers/Projects/hooks/index.ts
Normal file
125
src/containers/Projects/hooks/index.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
11
src/containers/Projects/hooks/type.ts
Normal file
11
src/containers/Projects/hooks/type.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const CUSTOMERS = {
|
||||
CUSTOMERS: 'CUSTOMERS',
|
||||
CUSTOMER: 'CUSTOMER',
|
||||
};
|
||||
|
||||
const PROJECTS = {
|
||||
PROJECT: 'PROJECT',
|
||||
PROJECTS: 'PROJECTS',
|
||||
};
|
||||
|
||||
export default { ...PROJECTS, ...CUSTOMERS };
|
||||
@@ -23,7 +23,9 @@ export default (mapState) => {
|
||||
vendorsCreditNoteSetting: state.settings.data.vendorCredit,
|
||||
warehouseTransferSettings: state.settings.data.warehouseTransfers,
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -2057,20 +2057,32 @@
|
||||
"projects.dialog.cost_estimate": "Cost Estimate",
|
||||
"projects.label.create": "Create",
|
||||
"projects.label.cost_estimate": " • Estimate {value}",
|
||||
"task.dialog.new_task": "New Task",
|
||||
"task.dialog.task_name": "Task Name",
|
||||
"task.dialog.estimated_hours": "Estimate Hours",
|
||||
"task.dialog.charge": "Charge",
|
||||
"task.dialog.estimated_amount": "Estimated Amount",
|
||||
"projects.dialog.success_message": "The project has been created successfully.",
|
||||
"projects.dialog.edit_success_message": "The project has been edited successfully.",
|
||||
"projects.dialog.new_project": "New Project",
|
||||
"projects.dialog.edit_project": "Edit Project",
|
||||
"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.project_name": "Project name",
|
||||
"project.schema.label.deadline": "Deadline",
|
||||
"project.schema.label.project_state": "Project state",
|
||||
"project.schema.label.project_cost": "Project cost",
|
||||
"task.schema.label.task_name": "Task name",
|
||||
"task.schema.label.task_house": "Task house",
|
||||
"task.schema.label.charge": "Charge",
|
||||
"task.schema.label.amount": "Amount",
|
||||
"project_task.schema.label.task_name": "Task name",
|
||||
"project_task.schema.label.task_house": "Task house",
|
||||
"project_task.schema.label.charge": "Charge",
|
||||
"project_task.schema.label.amount": "Amount",
|
||||
"projcet_details.action.new_transaction": "New Transaction",
|
||||
"projcet_details.action.time_entry": "Time",
|
||||
"projcet_details.action.edit_project": "Edit Project",
|
||||
@@ -2079,23 +2091,75 @@
|
||||
"project_details.label.purchases": "Purchases",
|
||||
"project_details.label.sales": "Sales",
|
||||
"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.column.date": "Date",
|
||||
"timesheets.column.task": "Task",
|
||||
"timesheets.column.user": "User",
|
||||
"timesheets.column.time": "Time",
|
||||
"timesheets.column.billing_status": "Billing Status",
|
||||
"time_entry.dialog.label": "New Time Entry",
|
||||
"time_entry.dialog.project": "Project",
|
||||
"time_entry.dialog.task": "Task",
|
||||
"time_entry.dialog.description": "Description",
|
||||
"time_entry.dialog.duration": "Duration",
|
||||
"time_entry.dialog.date": "Date",
|
||||
"time_entry.dialog.create": "Create",
|
||||
"time_entry.schema.label.project_name": "Project name",
|
||||
"time_entry.schema.label.task_name": "Task name",
|
||||
"time_entry.schema.label.duration": "Duration",
|
||||
"time_entry.schema.label.date": "Date",
|
||||
"project_time_entry.dialog.label": "New Time Entry",
|
||||
"project_time_entry.dialog.project": "Project",
|
||||
"project_time_entry.dialog.task": "Task",
|
||||
"project_time_entry.dialog.description": "Description",
|
||||
"project_time_entry.dialog.duration": "Duration",
|
||||
"project_time_entry.dialog.date": "Date",
|
||||
"project_time_entry.dialog.create": "Create",
|
||||
"project_time_entry.schema.label.project_name": "Project name",
|
||||
"project_time_entry.schema.label.task_name": "Task name",
|
||||
"project_time_entry.schema.label.duration": "Duration",
|
||||
"project_time_entry.schema.label.date": "Date",
|
||||
"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"
|
||||
}
|
||||
@@ -67,6 +67,12 @@ const initialState = {
|
||||
timesheets: {
|
||||
tableSize: 'medium',
|
||||
},
|
||||
purchases: {
|
||||
tableSize: 'medium',
|
||||
},
|
||||
sales: {
|
||||
tableSize: 'medium',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user