Compare commits

...

12 Commits

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

View File

@@ -1,7 +0,0 @@
import intl from 'react-intl-universal';
export const modalChargeOptions = [
{ name: 'Hourly rate', value: 'Hourly rate' },
{ name: 'Fixed price', value: 'Fixed price' },
{ name: 'Non-chargeable', value: 'Non-chargeable' },
];

View File

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

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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,
];

View File

@@ -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} />;
}

View File

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

View File

@@ -0,0 +1,20 @@
//@ts-nocheck
import React from 'react';
import { FInputGroup } from 'components';
import { useFormikContext } from 'formik';
export function FInputGroupComponent({ toField, ...props }) {
const { values, setFieldValue } = useFormikContext();
const { expenseQuantity, expenseUnitPrice } = values;
const total = expenseQuantity * expenseUnitPrice;
const handleBlur = () => {
setFieldValue(toField, total);
};
const inputGroupProps = {
onBlur: handleBlur,
...props,
};
return <FInputGroup {...inputGroupProps} />;
}

View File

@@ -11,7 +11,7 @@ import { FSelect } from 'components';
* @param {*} exactMatch
* @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}

View File

@@ -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';
/**
*

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
//@ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Classes, ControlGroup } from '@blueprintjs/core';
import { FFormGroup, FInputGroup, Choose } from 'components';
import { useFormikContext } from 'formik';
function PercentageFormField() {
return (
<FFormGroup
label={intl.get('estimated_expenses.dialog.percentage')}
name={'percentage'}
>
<FInputGroup name="percentage" />
</FFormGroup>
);
}
function CustomPirceField() {
return (
<ControlGroup className={Classes.FILL}>
<FFormGroup
name={'unitPrice'}
label={intl.get('estimated_expenses.dialog.unit_price')}
>
<FInputGroup name="unitPrice" />
</FFormGroup>
<FFormGroup
name={'unitPrice'}
label={intl.get('estimated_expenses.dialog.total')}
>
<FInputGroup name="total" />
</FFormGroup>
</ControlGroup>
);
}
/**
* estimate expense form charge fields.
* @returns
*/
export default function EstimatedExpenseFormChargeFields() {
const { values } = useFormikContext();
return (
<Choose>
<Choose.When condition={values.charge === 'markup'}>
<PercentageFormField />
</Choose.When>
<Choose.When condition={values.charge === 'custom_pirce'}>
<CustomPirceField />
</Choose.When>
</Choose>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,10 +14,11 @@ import {
FormattedMessage as T,
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

View File

@@ -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; */
}
}
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}
}
`;

View File

@@ -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,
},
],
[],
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,17 +29,16 @@ export const FinancialProgressBar = ({ ...rest }) => {
const FinancialSectionWrap = styled.div`
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``;

View File

@@ -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'} />}

View File

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

View File

@@ -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]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,18 @@
import * as Yup from 'yup';
import 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'),
),
});

View File

@@ -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 (

View File

@@ -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}
/>

View File

@@ -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

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
//@ts-nocheck
import React from 'react';
import 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;
`;

View File

@@ -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);

View File

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

View File

@@ -5,30 +5,32 @@ import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { 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);

View File

@@ -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;

View File

@@ -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);

View File

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

View File

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

View File

@@ -14,25 +14,25 @@ import {
FieldRequiredHint,
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;

View File

@@ -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);

View File

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

View File

@@ -4,42 +4,42 @@ import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { 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;

View File

@@ -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;
}
}
}
}

View File

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

View File

@@ -1,7 +1,9 @@
//@ts-nocheck
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>
);

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
import intl from 'react-intl-universal';
export const taskChargeOptions = [
{ name: intl.get('task.dialog.hourly_rate'), value: 'hourly_rate' },
{ name: intl.get('task.dialog.fixed_price'), value: 'fixed_price' },
{ name: intl.get('task.dialog.non_chargeable'), value: 'non_chargeable' },
];
export const expenseChargeOption = [
{
name: intl.get('expenses.dialog.markup'),
value: 'markup',
},
{ name: intl.get('expenses.dialog.pass_cost_on'), value: 'pass_cost_on' },
{ name: intl.get('expenses.dialog.custom_pirce'), value: 'custom_pirce' },
{ name: intl.get('expenses.dialog.non_chargeable'), value: 'non_chargeable' },
];

View File

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

View File

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

View File

@@ -23,7 +23,9 @@ export default (mapState) => {
vendorsCreditNoteSetting: state.settings.data.vendorCredit,
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;
};

View File

@@ -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"
}

View File

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