mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-24 08:39:49 +00:00
feat: add project.
This commit is contained in:
@@ -197,6 +197,7 @@ export function DataTable(props) {
|
|||||||
DataTable.defaultProps = {
|
DataTable.defaultProps = {
|
||||||
pagination: false,
|
pagination: false,
|
||||||
hidePaginationNoPages: true,
|
hidePaginationNoPages: true,
|
||||||
|
hideTableHeader: false,
|
||||||
|
|
||||||
size: null,
|
size: null,
|
||||||
spinnerProps: { size: 30 },
|
spinnerProps: { size: 30 },
|
||||||
|
|||||||
@@ -79,9 +79,19 @@ function TableHeaderGroup({ headerGroup }) {
|
|||||||
export default function TableHeader() {
|
export default function TableHeader() {
|
||||||
const {
|
const {
|
||||||
table: { headerGroups, page },
|
table: { headerGroups, page },
|
||||||
props: { TableHeaderSkeletonRenderer, headerLoading, progressBarLoading },
|
props: {
|
||||||
|
TableHeaderSkeletonRenderer,
|
||||||
|
headerLoading,
|
||||||
|
progressBarLoading,
|
||||||
|
hideTableHeader,
|
||||||
|
},
|
||||||
} = useContext(TableContext);
|
} = useContext(TableContext);
|
||||||
|
|
||||||
|
// Can't contiunue if the thead is disabled.
|
||||||
|
if (hideTableHeader) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (headerLoading && TableHeaderSkeletonRenderer) {
|
if (headerLoading && TableHeaderSkeletonRenderer) {
|
||||||
return <TableHeaderSkeletonRenderer />;
|
return <TableHeaderSkeletonRenderer />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ import BranchActivateDialog from '@/containers/Dialogs/BranchActivateDialog';
|
|||||||
import WarehouseActivateDialog from '@/containers/Dialogs/WarehouseActivateDialog';
|
import WarehouseActivateDialog from '@/containers/Dialogs/WarehouseActivateDialog';
|
||||||
import CustomerOpeningBalanceDialog from '@/containers/Dialogs/CustomerOpeningBalanceDialog';
|
import CustomerOpeningBalanceDialog from '@/containers/Dialogs/CustomerOpeningBalanceDialog';
|
||||||
import VendorOpeningBalanceDialog from '@/containers/Dialogs/VendorOpeningBalanceDialog';
|
import VendorOpeningBalanceDialog from '@/containers/Dialogs/VendorOpeningBalanceDialog';
|
||||||
|
import ProjectFormDialog from '@/containers/Projects/containers/ProjectFormDialog';
|
||||||
|
import 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';
|
||||||
|
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
@@ -116,6 +121,15 @@ export default function DialogsContainer() {
|
|||||||
<VendorOpeningBalanceDialog
|
<VendorOpeningBalanceDialog
|
||||||
dialogName={DialogsName.VendorOpeningBalanceForm}
|
dialogName={DialogsName.VendorOpeningBalanceForm}
|
||||||
/>
|
/>
|
||||||
|
<ProjectFormDialog dialogName={DialogsName.ProjectForm} />
|
||||||
|
<ProjectTaskFormDialog dialogName={DialogsName.ProjectTaskForm} />
|
||||||
|
<ProjectTimeEntryFormDialog
|
||||||
|
dialogName={DialogsName.ProjectTimeEntryForm}
|
||||||
|
/>
|
||||||
|
<ProjectExpenseForm dialogName={DialogsName.ProjectExpenseForm} />
|
||||||
|
<EstimatedExpenseFormDialog
|
||||||
|
dialogName={DialogsName.EstimateExpenseForm}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,4 +38,9 @@ export enum DialogsName {
|
|||||||
WarehouseActivateForm = 'warehouse-activate',
|
WarehouseActivateForm = 'warehouse-activate',
|
||||||
CustomerOpeningBalanceForm = 'customer-opening-balance',
|
CustomerOpeningBalanceForm = 'customer-opening-balance',
|
||||||
VendorOpeningBalanceForm = 'vendor-opening-balance',
|
VendorOpeningBalanceForm = 'vendor-opening-balance',
|
||||||
|
ProjectForm = 'project-form',
|
||||||
|
ProjectTaskForm = 'project-task-form',
|
||||||
|
ProjectTimeEntryForm = 'project-time-entry-form',
|
||||||
|
ProjectExpenseForm = 'project-expense-form',
|
||||||
|
EstimateExpenseForm = 'estimate-expense-form',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -538,6 +538,38 @@ export const SidebarMenu = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// ---------------------
|
||||||
|
// # Projects Management
|
||||||
|
// ---------------------
|
||||||
|
{
|
||||||
|
text: 'Projects',
|
||||||
|
type: ISidebarMenuItemType.Overlay,
|
||||||
|
overlayId: ISidebarMenuOverlayIds.Projects,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
text: 'Projects Management',
|
||||||
|
type: ISidebarMenuItemType.Group,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
text: 'Projects',
|
||||||
|
href: '/projects',
|
||||||
|
type: ISidebarMenuItemType.Link,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: <T id={'New tasks'} />,
|
||||||
|
type: ISidebarMenuItemType.Group,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
text: <T id={'projects.label.new_project'} />,
|
||||||
|
type: ISidebarMenuItemType.Dialog,
|
||||||
|
dialogName: 'project-form',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// ---------------
|
// ---------------
|
||||||
// # Reports
|
// # Reports
|
||||||
// ---------------
|
// ---------------
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import TransactionsLockingAlerts from '@/containers/TransactionsLocking/Transact
|
|||||||
import WarehousesAlerts from '@/containers/Preferences/Warehouses/WarehousesAlerts';
|
import WarehousesAlerts from '@/containers/Preferences/Warehouses/WarehousesAlerts';
|
||||||
import WarehousesTransfersAlerts from '@/containers/WarehouseTransfers/WarehousesTransfersAlerts';
|
import WarehousesTransfersAlerts from '@/containers/WarehouseTransfers/WarehousesTransfersAlerts';
|
||||||
import BranchesAlerts from '@/containers/Preferences/Branches/BranchesAlerts';
|
import BranchesAlerts from '@/containers/Preferences/Branches/BranchesAlerts';
|
||||||
|
import ProjectAlerts from '@/containers/Projects/containers/ProjectAlerts';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
...AccountsAlerts,
|
...AccountsAlerts,
|
||||||
@@ -50,4 +51,5 @@ export default [
|
|||||||
...WarehousesAlerts,
|
...WarehousesAlerts,
|
||||||
...WarehousesTransfersAlerts,
|
...WarehousesTransfersAlerts,
|
||||||
...BranchesAlerts,
|
...BranchesAlerts,
|
||||||
|
...ProjectAlerts,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MenuItem, Button } from '@blueprintjs/core';
|
import { MenuItem, Button } from '@blueprintjs/core';
|
||||||
import { FSelect } from 'components';
|
import { FSelect } from '@/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -8,7 +8,7 @@ import { FSelect } from 'components';
|
|||||||
* @param {*} param1
|
* @param {*} param1
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const taskModalChargeRenderer = (item, { handleClick, modifiers, query }) => {
|
const chargeTypeItemRenderer = (item, { handleClick, modifiers, query }) => {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label={item.label}
|
label={item.label}
|
||||||
@@ -19,8 +19,8 @@ const taskModalChargeRenderer = (item, { handleClick, modifiers, query }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const taskModalChargeSelectProps = {
|
const chargeTypeSelectProps = {
|
||||||
itemRenderer: taskModalChargeRenderer,
|
itemRenderer: chargeTypeItemRenderer,
|
||||||
valueAccessor: 'value',
|
valueAccessor: 'value',
|
||||||
labelAccessor: 'name',
|
labelAccessor: 'name',
|
||||||
};
|
};
|
||||||
@@ -30,22 +30,21 @@ const taskModalChargeSelectProps = {
|
|||||||
* @param param0
|
* @param param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function TaskModalChargeSelect({ items, ...rest }) {
|
export function ChangeTypesSelect({ items, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<FSelect
|
<FSelect
|
||||||
{...taskModalChargeSelectProps}
|
{...chargeTypeSelectProps}
|
||||||
{...rest}
|
{...rest}
|
||||||
items={items}
|
items={items}
|
||||||
input={TaskModalChargeSelectButton}
|
input={ChargeTypeSelectButton}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param param0
|
* @param param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function TaskModalChargeSelectButton({ label }) {
|
function ChargeTypeSelectButton({ label }) {
|
||||||
return <Button text={label} />;
|
return <Button text={label} />;
|
||||||
}
|
}
|
||||||
67
src/containers/Projects/components/ExpenseSelect.tsx
Normal file
67
src/containers/Projects/components/ExpenseSelect.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { MenuItem, Button } from '@blueprintjs/core';
|
||||||
|
import { FSelect } from '@/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param query
|
||||||
|
* @param expense
|
||||||
|
* @param _index
|
||||||
|
* @param exactMatch
|
||||||
|
*/
|
||||||
|
const expenseItemPredicate = (query, expense, _index, exactMatch) => {
|
||||||
|
const normalizedTitle = expense.name.toLowerCase();
|
||||||
|
const normalizedQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
if (exactMatch) {
|
||||||
|
return normalizedTitle === normalizedQuery;
|
||||||
|
} else {
|
||||||
|
return `${expense.name}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param expense
|
||||||
|
* @param param1
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const expenseItemRenderer = (expense, { handleClick, modifiers, query }) => {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
active={modifiers.active}
|
||||||
|
disabled={modifiers.disabled}
|
||||||
|
key={expense.id}
|
||||||
|
onClick={handleClick}
|
||||||
|
text={expense.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expenseSelectProps = {
|
||||||
|
itemPredicate: expenseItemPredicate,
|
||||||
|
itemRenderer: expenseItemRenderer,
|
||||||
|
valueAccessor: 'id',
|
||||||
|
labelAccessor: 'name',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ExpenseSelect({ expenses, defaultText, ...rest }) {
|
||||||
|
return (
|
||||||
|
<FSelect
|
||||||
|
items={expenses}
|
||||||
|
{...expenseSelectProps}
|
||||||
|
{...rest}
|
||||||
|
input={ExpenseSelectButton}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExpenseSelectButton({ label, ...rest }) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
text={label ? label : intl.get('choose_an_estimated_expense')}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/containers/Projects/components/FInputGroupComponent.tsx
Normal file
19
src/containers/Projects/components/FInputGroupComponent.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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} />;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { MenuItem, Button } from '@blueprintjs/core';
|
import { MenuItem, Button } from '@blueprintjs/core';
|
||||||
import { FSelect } from 'components';
|
import { FSelect } from '@/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -11,7 +11,7 @@ import { FSelect } from 'components';
|
|||||||
* @param {*} exactMatch
|
* @param {*} exactMatch
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const projectItemPredicate = (query, project, _index, exactMatch) => {
|
const projectsItemPredicate = (query, project, _index, exactMatch) => {
|
||||||
const normalizedTitle = project.name.toLowerCase();
|
const normalizedTitle = project.name.toLowerCase();
|
||||||
const normalizedQuery = query.toLowerCase();
|
const normalizedQuery = query.toLowerCase();
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ const projectItemPredicate = (query, project, _index, exactMatch) => {
|
|||||||
* @param {*} param1
|
* @param {*} param1
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const projectItemRenderer = (project, { handleClick, modifiers, query }) => {
|
const projectsItemRenderer = (project, { handleClick, modifiers, query }) => {
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
active={modifiers.active}
|
active={modifiers.active}
|
||||||
@@ -41,13 +41,13 @@ const projectItemRenderer = (project, { handleClick, modifiers, query }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const projectSelectProps = {
|
const projectSelectProps = {
|
||||||
itemPredicate: projectItemPredicate,
|
itemPredicate: projectsItemPredicate,
|
||||||
itemRenderer: projectItemRenderer,
|
itemRenderer: projectsItemRenderer,
|
||||||
valueAccessor: 'id',
|
valueAccessor: 'id',
|
||||||
labelAccessor: 'name',
|
labelAccessor: 'name',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ProjectSelect({ projects, ...rest }) {
|
export function ProjectsSelect({ projects, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<FSelect
|
<FSelect
|
||||||
items={projects}
|
items={projects}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { MenuItem, Button } from '@blueprintjs/core';
|
import { MenuItem, Button } from '@blueprintjs/core';
|
||||||
import { FSelect } from '../../../../../components/Forms';
|
import { FSelect } from '@/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
5
src/containers/Projects/components/index.ts
Normal file
5
src/containers/Projects/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './ExpenseSelect';
|
||||||
|
export * from './ChangeTypesSelect';
|
||||||
|
export * from './TaskSelect';
|
||||||
|
export * from './ProjectsSelect';
|
||||||
|
export * from './FInputGroupComponent';
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
|
||||||
|
const Schema = Yup.object().shape({
|
||||||
|
estimatedExpense: Yup.number().label(
|
||||||
|
intl.get('estimated_expense.schema.label.estimated_expense'),
|
||||||
|
),
|
||||||
|
quantity: Yup.number().label(
|
||||||
|
intl.get('estimated_expense.schema.label.quantity'),
|
||||||
|
),
|
||||||
|
unitPrice: Yup.number().label(
|
||||||
|
intl.get('estimated_expense.schema.label.unit_price'),
|
||||||
|
),
|
||||||
|
expenseTotal: Yup.number(),
|
||||||
|
charge: Yup.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateEstimatedExpenseFormSchema = Schema;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { CreateEstimatedExpenseFormSchema } from './EstimatedExpense.schema';
|
||||||
|
import EstimatedExpenseFormConent from './EstimatedExpenseFormConent';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
const defaultInitialValues = {
|
||||||
|
estimatedExpense: '',
|
||||||
|
unitPrice: '',
|
||||||
|
quantity: 1,
|
||||||
|
charge: '% markup',
|
||||||
|
percentage: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimated expense form dialog.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function EstimatedExpenseForm({
|
||||||
|
//#withDialogActions
|
||||||
|
closeDialog,
|
||||||
|
}) {
|
||||||
|
const initialValues = {
|
||||||
|
...defaultInitialValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles the form submit.
|
||||||
|
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
|
// Handle request response success.
|
||||||
|
const onSuccess = (response) => {
|
||||||
|
AppToaster.show({});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle request response errors.
|
||||||
|
const onError = ({
|
||||||
|
response: {
|
||||||
|
data: { errors },
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
validationSchema={CreateEstimatedExpenseFormSchema}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
component={EstimatedExpenseFormConent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogActions)(EstimatedExpenseForm);
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { Classes, ControlGroup } from '@blueprintjs/core';
|
||||||
|
import { FFormGroup, FInputGroup, Choose } from '@/components';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
function PercentageFormField() {
|
||||||
|
return (
|
||||||
|
<FFormGroup
|
||||||
|
label={intl.get('estimated_expenses.dialog.percentage')}
|
||||||
|
name={'percentage'}
|
||||||
|
>
|
||||||
|
<FInputGroup name="percentage" />
|
||||||
|
</FFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomPirceField() {
|
||||||
|
return (
|
||||||
|
<ControlGroup className={Classes.FILL}>
|
||||||
|
<FFormGroup
|
||||||
|
name={'unitPrice'}
|
||||||
|
label={intl.get('estimated_expenses.dialog.unit_price')}
|
||||||
|
>
|
||||||
|
<FInputGroup name="unitPrice" />
|
||||||
|
</FFormGroup>
|
||||||
|
<FFormGroup
|
||||||
|
name={'unitPrice'}
|
||||||
|
label={intl.get('estimated_expenses.dialog.total')}
|
||||||
|
>
|
||||||
|
<FInputGroup name="total" />
|
||||||
|
</FFormGroup>
|
||||||
|
</ControlGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* estimate expense form charge fields.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function EstimatedExpenseFormChargeFields() {
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
return (
|
||||||
|
<Choose>
|
||||||
|
<Choose.When condition={values.charge === 'markup'}>
|
||||||
|
<PercentageFormField />
|
||||||
|
</Choose.When>
|
||||||
|
<Choose.When condition={values.charge === 'custom_pirce'}>
|
||||||
|
<CustomPirceField />
|
||||||
|
</Choose.When>
|
||||||
|
</Choose>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Form } from 'formik';
|
||||||
|
import EstimatedExpenseFormFields from './EstimatedExpenseFormFields';
|
||||||
|
import EstimatedExpenseFormFloatingActions from './EstimatedExpenseFormFloatingActions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimated expense form content.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function EstimatedExpenseFormConent() {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<EstimatedExpenseFormFields />
|
||||||
|
<EstimatedExpenseFormFloatingActions />
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { EstimatedExpenseFormProvider } from './EstimatedExpenseFormProvider';
|
||||||
|
import EstimatedExpenseForm from './EstimatedExpenseForm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate expense form dialog.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
export default function EstimatedExpenseFormDialogContent({
|
||||||
|
//#ownProps
|
||||||
|
dialogName,
|
||||||
|
estimatedExpense,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<EstimatedExpenseFormProvider
|
||||||
|
dialogName={dialogName}
|
||||||
|
estimatedExpenseId={estimatedExpense}
|
||||||
|
>
|
||||||
|
<EstimatedExpenseForm />
|
||||||
|
</EstimatedExpenseFormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
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,
|
||||||
|
ChangeTypesSelect,
|
||||||
|
} from '../../components';
|
||||||
|
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
|
||||||
|
import EstimatedExpenseFormChargeFields from './EstimatedExpenseFormChargeFields';
|
||||||
|
import { expenseChargeOption } from '../common/modalChargeOptions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimated expense form fields.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function EstimatedExpenseFormFields() {
|
||||||
|
return (
|
||||||
|
<div className={Classes.DIALOG_BODY}>
|
||||||
|
{/*------------ Estimated Expense -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
name={'estimatedExpense'}
|
||||||
|
label={intl.get('estimated_expenses.dialog.estimated_expense')}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
>
|
||||||
|
<ExpenseSelect
|
||||||
|
name={'estimatedExpense'}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
expenses={[]}
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/*------------ Quantity -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
label={intl.get('estimated_expenses.dialog.quantity')}
|
||||||
|
name={'quantity'}
|
||||||
|
>
|
||||||
|
<FInputGroupComponent name="quantity" />
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<MetaLineLabel>
|
||||||
|
<T id={'estimated_expenses.dialog.cost_to_you'} />
|
||||||
|
</MetaLineLabel>
|
||||||
|
{/*------------ Unit Price -----------*/}
|
||||||
|
<ControlGroup className={Classes.FILL}>
|
||||||
|
<FFormGroup
|
||||||
|
name={'unitPrice'}
|
||||||
|
label={intl.get('estimated_expenses.dialog.unit_price')}
|
||||||
|
>
|
||||||
|
<FInputGroupComponent name="unitPrice" />
|
||||||
|
</FFormGroup>
|
||||||
|
<FFormGroup
|
||||||
|
name={'unitPrice'}
|
||||||
|
label={intl.get('estimated_expenses.dialog.total')}
|
||||||
|
>
|
||||||
|
<FInputGroup name="expenseTotal" />
|
||||||
|
</FFormGroup>
|
||||||
|
</ControlGroup>
|
||||||
|
|
||||||
|
<MetaLineLabel>
|
||||||
|
<T id={'estimated_expenses.dialog.what_you_ll_charge'} />
|
||||||
|
</MetaLineLabel>
|
||||||
|
{/*------------ Charge -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
name={'charge'}
|
||||||
|
label={<T id={'estimated_expenses.dialog.charge'} />}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
>
|
||||||
|
<ChangeTypesSelect
|
||||||
|
name="charge"
|
||||||
|
items={expenseChargeOption}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
filterable={false}
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
<EstimatedExpenseFormChargeFields />
|
||||||
|
{/*------------ Estimated Amount -----------*/}
|
||||||
|
<EstimatedAmountWrap>
|
||||||
|
<EstimatedAmountLabel>
|
||||||
|
<T id={'estimated_expenses.dialog.estimated_amount'} />
|
||||||
|
</EstimatedAmountLabel>
|
||||||
|
<EstimatedAmount>0.00</EstimatedAmount>
|
||||||
|
</EstimatedAmountWrap>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetaLineLabel = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EstimatedAmountWrap = styled.div`
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
`;
|
||||||
|
const EstimatedAmountLabel = styled.span`
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
opacity: 0.75;
|
||||||
|
`;
|
||||||
|
const EstimatedAmount = styled.span`
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding-left: 14px;
|
||||||
|
line-height: 2rem;
|
||||||
|
`;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||||
|
import { FormattedMessage as T } from '@/components';
|
||||||
|
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimated expense form floating actions.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function EstimatedExpenseFormFloatingActions({
|
||||||
|
// #withDialogActions
|
||||||
|
closeDialog,
|
||||||
|
}) {
|
||||||
|
// Formik context.
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
// expense form dialog context.
|
||||||
|
const { dialogName } = useEstimatedExpenseFormContext();
|
||||||
|
|
||||||
|
// Handle close button click.
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
closeDialog(dialogName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
|
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
|
||||||
|
<T id={'cancel'} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
loading={isSubmitting}
|
||||||
|
style={{ minWidth: '75px' }}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{<T id={'save'} />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogActions)(EstimatedExpenseFormFloatingActions);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DialogContent } from '@/components';
|
||||||
|
|
||||||
|
const EstimatedExpenseFormContext = React.createContext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimated expense form provider.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function EstimatedExpenseFormProvider({
|
||||||
|
//#OwnProps
|
||||||
|
dialogName,
|
||||||
|
estimatedExpenseId,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
// state provider.
|
||||||
|
const provider = {
|
||||||
|
dialogName,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<EstimatedExpenseFormContext.Provider value={provider} {...props} />
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useEstimatedExpenseFormContext = () =>
|
||||||
|
React.useContext(EstimatedExpenseFormContext);
|
||||||
|
|
||||||
|
export { EstimatedExpenseFormProvider, useEstimatedExpenseFormContext };
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
|
||||||
|
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
const EstimatedExpenseFormDialogContent = React.lazy(
|
||||||
|
() => import('./EstimatedExpenseFormDialogContent'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate expense form dialog.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function EstimatedExpenseFormDialog({
|
||||||
|
dialogName,
|
||||||
|
payload: { projectId = null },
|
||||||
|
isOpen,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<EstimateExpenseFormDialogRoot
|
||||||
|
name={dialogName}
|
||||||
|
title={<T id={'estimated_expenses.dialog.label'} />}
|
||||||
|
isOpen={isOpen}
|
||||||
|
autoFocus={true}
|
||||||
|
canEscapeKeyClose={true}
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
>
|
||||||
|
<DialogSuspense>
|
||||||
|
<EstimatedExpenseFormDialogContent
|
||||||
|
dialogName={dialogName}
|
||||||
|
estimatedExpense={projectId}
|
||||||
|
/>
|
||||||
|
</DialogSuspense>
|
||||||
|
</EstimateExpenseFormDialogRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogRedux())(EstimatedExpenseFormDialog);
|
||||||
|
|
||||||
|
const EstimateExpenseFormDialogRoot = styled(Dialog)`
|
||||||
|
.bp3-dialog-body {
|
||||||
|
.bp3-form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
label.bp3-label {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bp3-dialog-footer {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { FormattedMessage as T, FormattedHTMLMessage } from '@/components';
|
||||||
|
import { Intent, Alert } from '@blueprintjs/core';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { useDeleteProject } from '../../hooks';
|
||||||
|
|
||||||
|
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
||||||
|
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project delete alert.
|
||||||
|
*/
|
||||||
|
function ProjectDeleteAlert({
|
||||||
|
name,
|
||||||
|
|
||||||
|
// #withAlertStoreConnect
|
||||||
|
isOpen,
|
||||||
|
payload: { projectId },
|
||||||
|
|
||||||
|
// #withAlertActions
|
||||||
|
closeAlert,
|
||||||
|
|
||||||
|
// #withDrawerActions
|
||||||
|
closeDrawer,
|
||||||
|
}) {
|
||||||
|
const { mutateAsync: deleteProjectMutate, isLoading } = useDeleteProject();
|
||||||
|
|
||||||
|
// handle cancel delete project alert.
|
||||||
|
const handleCancelDeleteAlert = () => {
|
||||||
|
closeAlert(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
// handleConfirm delete project
|
||||||
|
const handleConfirmProjectDelete = () => {
|
||||||
|
deleteProjectMutate(projectId)
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: intl.get('projects.alert.delete_message'),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(
|
||||||
|
({
|
||||||
|
response: {
|
||||||
|
data: { errors },
|
||||||
|
},
|
||||||
|
}) => {},
|
||||||
|
)
|
||||||
|
.finally(() => {
|
||||||
|
closeAlert(name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
cancelButtonText={<T id={'cancel'} />}
|
||||||
|
confirmButtonText={<T id={'delete'} />}
|
||||||
|
icon="trash"
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onCancel={handleCancelDeleteAlert}
|
||||||
|
onConfirm={handleConfirmProjectDelete}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<FormattedHTMLMessage id={'projects.alert.once_delete_this_project'} />
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withAlertStoreConnect(),
|
||||||
|
withAlertActions,
|
||||||
|
)(ProjectDeleteAlert);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const ProjectDeleteAlert = React.lazy(() => import('./ProjectDeleteAlert'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project alerts.
|
||||||
|
*/
|
||||||
|
export default [{ name: 'project-delete', component: ProjectDeleteAlert }];
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
@@ -8,18 +7,19 @@ import {
|
|||||||
NavbarGroup,
|
NavbarGroup,
|
||||||
Alignment,
|
Alignment,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
DashboardRowsHeightButton,
|
DashboardRowsHeightButton,
|
||||||
} from 'components';
|
DashboardActionsBar,
|
||||||
import { TransactionSelect } from './components';
|
} from '@/components';
|
||||||
import withSettings from '../../../Settings/withSettings';
|
import { ProjectTransactionsSelect } from './components';
|
||||||
import withSettingsActions from '../../../Settings/withSettingsActions';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { projectTranslations } from './common';
|
||||||
import { useProjectDetailContext } from './ProjectDetailProvider';
|
import { useProjectDetailContext } from './ProjectDetailProvider';
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project detail actions bar.
|
* Project detail actions bar.
|
||||||
@@ -40,7 +40,13 @@ function ProjectDetailActionsBar({
|
|||||||
|
|
||||||
// Handle new transaction button click.
|
// Handle new transaction button click.
|
||||||
const handleNewTransactionBtnClick = ({ path }) => {
|
const handleNewTransactionBtnClick = ({ path }) => {
|
||||||
history.push(`/${path}`);
|
switch (path) {
|
||||||
|
case 'expense':
|
||||||
|
openDialog('project-expense-form', { projectId });
|
||||||
|
break;
|
||||||
|
case 'estimated_expense':
|
||||||
|
openDialog('estimated-expense-form', { projectId });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditProjectBtnClick = () => {
|
const handleEditProjectBtnClick = () => {
|
||||||
@@ -50,11 +56,13 @@ function ProjectDetailActionsBar({
|
|||||||
};
|
};
|
||||||
// Handle table row size change.
|
// Handle table row size change.
|
||||||
const handleTableRowSizeChange = (size) => {
|
const handleTableRowSizeChange = (size) => {
|
||||||
addSetting('timesheets', 'tableSize', size);
|
addSetting('timesheets', 'tableSize', size) &&
|
||||||
|
addSetting('sales', 'tableSize', size) &&
|
||||||
|
addSetting('purchases', 'tableSize', size);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeEntryBtnClick = () => {
|
const handleTimeEntryBtnClick = () => {
|
||||||
openDialog('time-entry-form', {
|
openDialog('project-time-entry-form', {
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -65,11 +73,8 @@ function ProjectDetailActionsBar({
|
|||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
<TransactionSelect
|
<ProjectTransactionsSelect
|
||||||
transactions={[
|
transactions={projectTranslations}
|
||||||
{ name: 'Invoice', path: 'invoices/new' },
|
|
||||||
{ name: 'Expenses', path: 'expenses/new' },
|
|
||||||
]}
|
|
||||||
onItemSelect={handleNewTransactionBtnClick}
|
onItemSelect={handleNewTransactionBtnClick}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -105,8 +110,6 @@ function ProjectDetailActionsBar({
|
|||||||
initialValue={timesheetsTableSize}
|
initialValue={timesheetsTableSize}
|
||||||
onChange={handleTableRowSizeChange}
|
onChange={handleTableRowSizeChange}
|
||||||
/>
|
/>
|
||||||
<NavbarDivider />
|
|
||||||
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
|
|
||||||
</NavbarGroup>
|
</NavbarGroup>
|
||||||
<NavbarGroup align={Alignment.RIGHT}>
|
<NavbarGroup align={Alignment.RIGHT}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DashboardInsider from '../../../../components/Dashboard/DashboardInsider';
|
import { DashboardInsider } from '@/components';
|
||||||
|
|
||||||
const ProjectDetailContext = React.createContext();
|
const ProjectDetailContext = React.createContext();
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ function ProjectDetailProvider({
|
|||||||
projectId,
|
projectId,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<DashboardInsider class="timesheets">
|
<DashboardInsider className="timesheets">
|
||||||
<ProjectDetailContext.Provider value={provider} {...props} />
|
<ProjectDetailContext.Provider value={provider} {...props} />
|
||||||
</DashboardInsider>
|
</DashboardInsider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import React from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { Tabs, Tab } from '@blueprintjs/core';
|
import { Tabs, Tab } from '@blueprintjs/core';
|
||||||
|
import ProjectTimeSheets from './ProjectTimeSheets';
|
||||||
import ProjectTimesheet from './ProjectTimesheet';
|
import ProjectPurchasesTable from './ProjectPurchasesTable';
|
||||||
|
import ProjectSalesTable from './ProjectSalesTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project detail tabs.
|
* Project detail tabs.
|
||||||
@@ -16,19 +17,24 @@ export default function ProjectDetailTabs() {
|
|||||||
animate={true}
|
animate={true}
|
||||||
large={true}
|
large={true}
|
||||||
renderActiveTabPanelOnly={true}
|
renderActiveTabPanelOnly={true}
|
||||||
defaultSelectedTabId={'timesheet'}
|
defaultSelectedTabId={'purchases'}
|
||||||
>
|
>
|
||||||
<Tab id="overview" title={intl.get('project_details.label.overview')} />
|
<Tab id="overview" title={intl.get('project_details.label.overview')} />
|
||||||
<Tab
|
<Tab
|
||||||
id="timesheet"
|
id="timesheet"
|
||||||
title={intl.get('project_details.label.timesheet')}
|
title={intl.get('project_details.label.timesheet')}
|
||||||
panel={<ProjectTimesheet />}
|
panel={<ProjectTimeSheets />}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
id="purchases"
|
id="purchases"
|
||||||
title={intl.get('project_details.label.purchases')}
|
title={intl.get('project_details.label.purchases')}
|
||||||
|
panel={<ProjectPurchasesTable />}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
id="sales"
|
||||||
|
title={intl.get('project_details.label.sales')}
|
||||||
|
panel={<ProjectSalesTable />}
|
||||||
/>
|
/>
|
||||||
<Tab id="sales" title={intl.get('project_details.label.sales')} />
|
|
||||||
<Tab id="journals" title={intl.get('project_details.label.journals')} />
|
<Tab id="journals" title={intl.get('project_details.label.journals')} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</ProjectTabsContent>
|
</ProjectTabsContent>
|
||||||
@@ -42,6 +48,10 @@ const ProjectTabsContent = styled.div`
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-bottom: 1px solid #d2dce2;
|
border-bottom: 1px solid #d2dce2;
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.bp3-large > .bp3-tab {
|
&.bp3-large > .bp3-tab {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -59,9 +69,11 @@ const ProjectTabsContent = styled.div`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bp3-tab-panel {
|
.bp3-tab-panel {
|
||||||
margin-top: 20px;
|
/* margin: 20px 32px; */
|
||||||
|
/* margin: 20px; */
|
||||||
|
/* margin-top: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 0 25px;
|
padding: 0 25px; */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
DataTable,
|
||||||
|
TableSkeletonRows,
|
||||||
|
TableSkeletonHeader,
|
||||||
|
} from '@/components';
|
||||||
|
import { TABLES } from '@/constants/tables';
|
||||||
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
|
import { ActionMenu } from './components';
|
||||||
|
import { useProjectPurchasesColumns } from './hooks';
|
||||||
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project Purchases DataTable.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectPurchasesTableRoot({
|
||||||
|
// #withSettings
|
||||||
|
purchasesTableSize,
|
||||||
|
}) {
|
||||||
|
// Retrieve purchases table columns.
|
||||||
|
const columns = useProjectPurchasesColumns();
|
||||||
|
|
||||||
|
// Handle delete purchase.
|
||||||
|
const handleDeletePurchase = () => {};
|
||||||
|
|
||||||
|
// Local storage memorizing columns widths.
|
||||||
|
const [initialColumnsWidths, , handleColumnResizing] =
|
||||||
|
useMemorizedColumnsWidths(TABLES.PURCHASES);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={[]}
|
||||||
|
manualSortBy={true}
|
||||||
|
selectionColumn={true}
|
||||||
|
noInitialFetch={true}
|
||||||
|
sticky={true}
|
||||||
|
ContextMenu={ActionMenu}
|
||||||
|
TableLoadingRenderer={TableSkeletonRows}
|
||||||
|
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
||||||
|
initialColumnsWidths={initialColumnsWidths}
|
||||||
|
onColumnResizing={handleColumnResizing}
|
||||||
|
size={purchasesTableSize}
|
||||||
|
payload={{
|
||||||
|
onDelete: handleDeletePurchase,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const ProjectPurchasesTable = compose(
|
||||||
|
withSettings(({ purchasesSettings }) => ({
|
||||||
|
purchasesTableSize: purchasesSettings?.tableSize,
|
||||||
|
})),
|
||||||
|
)(ProjectPurchasesTableRoot);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { Icon } from '@/components';
|
||||||
|
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
|
||||||
|
import { safeCallback } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table actions cell.
|
||||||
|
*/
|
||||||
|
export function ActionMenu({ payload: { onDelete }, row: { original } }) {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuItem
|
||||||
|
text={intl.get('purchases.action.delete')}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClick={safeCallback(onDelete, original)}
|
||||||
|
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import clsx from 'classnames';
|
||||||
|
import { CLASSES } from '@/constants/classes';
|
||||||
|
import { FormatDateCell } from '@/components';
|
||||||
|
|
||||||
|
export function useProjectPurchasesColumns() {
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'date',
|
||||||
|
Header: intl.get('purchases.column.date'),
|
||||||
|
accessor: 'date',
|
||||||
|
Cell: FormatDateCell,
|
||||||
|
width: 120,
|
||||||
|
className: 'date',
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'type',
|
||||||
|
Header: intl.get('purchases.column.type'),
|
||||||
|
accessor: 'type',
|
||||||
|
width: 120,
|
||||||
|
className: 'type',
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'transaction_no',
|
||||||
|
Header: intl.get('purchases.column.transaction_no'),
|
||||||
|
accessor: 'transaction_no',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'due_date',
|
||||||
|
Header: intl.get('purchases.column.due_date'),
|
||||||
|
accessor: 'due_date',
|
||||||
|
Cell: FormatDateCell,
|
||||||
|
width: 120,
|
||||||
|
className: 'due_date',
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'balance',
|
||||||
|
Header: intl.get('purchases.column.balance'),
|
||||||
|
accessor: 'balance',
|
||||||
|
width: 120,
|
||||||
|
clickable: true,
|
||||||
|
align: 'right',
|
||||||
|
className: clsx(CLASSES.FONT_BOLD),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'total',
|
||||||
|
Header: intl.get('purchases.column.total'),
|
||||||
|
accessor: 'total',
|
||||||
|
align: 'right',
|
||||||
|
width: 120,
|
||||||
|
className: clsx(CLASSES.FONT_BOLD),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
Header: intl.get('purchases.column.status'),
|
||||||
|
accessor: 'status',
|
||||||
|
width: 120,
|
||||||
|
className: 'status',
|
||||||
|
clickable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { ProjectPurchasesTable } from './ProjectPurchasesTable';
|
||||||
|
import { DashboardContentTable } from '@/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ProjectPurchasesTableRoot() {
|
||||||
|
return (
|
||||||
|
<DashboardContentTable>
|
||||||
|
<ProjectPurchasesTable />
|
||||||
|
</DashboardContentTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
DataTable,
|
||||||
|
TableSkeletonRows,
|
||||||
|
TableSkeletonHeader,
|
||||||
|
} from '@/components';
|
||||||
|
import { TABLES } from '@/constants/tables';
|
||||||
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
|
import { ActionMenu } from './components';
|
||||||
|
import { useProjectSalesColumns } from './hooks';
|
||||||
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Porject sales datatable.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectSalesTableRoot({
|
||||||
|
// #withSettings
|
||||||
|
salesTableSize,
|
||||||
|
}) {
|
||||||
|
// Retrieve project sales table columns.
|
||||||
|
const columns = useProjectSalesColumns();
|
||||||
|
|
||||||
|
// Handle delete sale.
|
||||||
|
const handleDeleteSale = () => {};
|
||||||
|
|
||||||
|
// Local storage memorizing columns widths.
|
||||||
|
const [initialColumnsWidths, , handleColumnResizing] =
|
||||||
|
useMemorizedColumnsWidths(TABLES.SALES);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={[]}
|
||||||
|
manualSortBy={true}
|
||||||
|
selectionColumn={true}
|
||||||
|
noInitialFetch={true}
|
||||||
|
sticky={true}
|
||||||
|
ContextMenu={ActionMenu}
|
||||||
|
TableLoadingRenderer={TableSkeletonRows}
|
||||||
|
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
||||||
|
initialColumnsWidths={initialColumnsWidths}
|
||||||
|
onColumnResizing={handleColumnResizing}
|
||||||
|
size={salesTableSize}
|
||||||
|
payload={{
|
||||||
|
onDelete: handleDeleteSale,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const ProjectSalesTable = compose(
|
||||||
|
withSettings(({ salesSettings }) => ({
|
||||||
|
salesTableSize: salesSettings?.tableSize,
|
||||||
|
})),
|
||||||
|
)(ProjectSalesTableRoot);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
import { FormatDateCell, Icon, FormattedMessage as T } from '@/components';
|
||||||
|
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
|
||||||
|
import { safeCallback } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table actions cell.
|
||||||
|
*/
|
||||||
|
export function ActionMenu({ payload: { onDelete }, row: { original } }) {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuItem
|
||||||
|
text={intl.get('sales.action.delete')}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClick={safeCallback(onDelete, original)}
|
||||||
|
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import clsx from 'classnames';
|
||||||
|
import { CLASSES } from '@/constants/classes';
|
||||||
|
import { FormatDateCell } from '@/components';
|
||||||
|
|
||||||
|
export function useProjectSalesColumns() {
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'date',
|
||||||
|
Header: intl.get('sales.column.date'),
|
||||||
|
accessor: 'date',
|
||||||
|
Cell: FormatDateCell,
|
||||||
|
width: 120,
|
||||||
|
className: 'date',
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'type',
|
||||||
|
Header: intl.get('sales.column.type'),
|
||||||
|
accessor: 'type',
|
||||||
|
width: 120,
|
||||||
|
className: 'type',
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'transaction_no',
|
||||||
|
Header: intl.get('sales.column.transaction_no'),
|
||||||
|
accessor: 'transaction_no',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'due_date',
|
||||||
|
Header: intl.get('sales.column.due_date'),
|
||||||
|
accessor: 'due_date',
|
||||||
|
Cell: FormatDateCell,
|
||||||
|
width: 120,
|
||||||
|
className: 'due_date',
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'balance',
|
||||||
|
Header: intl.get('sales.column.balance'),
|
||||||
|
accessor: 'balance',
|
||||||
|
width: 120,
|
||||||
|
clickable: true,
|
||||||
|
align: 'right',
|
||||||
|
className: clsx(CLASSES.FONT_BOLD),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'total',
|
||||||
|
Header: intl.get('sales.column.total'),
|
||||||
|
accessor: 'total',
|
||||||
|
align: 'right',
|
||||||
|
width: 120,
|
||||||
|
className: clsx(CLASSES.FONT_BOLD),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
Header: intl.get('sales.column.status'),
|
||||||
|
accessor: 'status',
|
||||||
|
width: 120,
|
||||||
|
className: 'status',
|
||||||
|
clickable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { ProjectSalesTable } from './ProjectSalesTable';
|
||||||
|
import { DashboardContentTable } from '@/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project Sales Table.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ProjectSalesTableRoot() {
|
||||||
|
return (
|
||||||
|
<ProjectSalesContentTable>
|
||||||
|
<ProjectSalesTable />
|
||||||
|
</ProjectSalesContentTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectSalesContentTable = styled(DashboardContentTable)``;
|
||||||
@@ -3,20 +3,20 @@ import React from 'react';
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { FormatDate } from 'components';
|
import { FormatDate } from '@/components';
|
||||||
import {
|
import {
|
||||||
DetailFinancialCard,
|
DetailFinancialCard,
|
||||||
DetailFinancialSection,
|
DetailFinancialSection,
|
||||||
FinancialProgressBar,
|
FinancialProgressBar,
|
||||||
FinancialCardText,
|
FinancialCardText,
|
||||||
} from '../components';
|
} from '../components';
|
||||||
import { calculateStatus } from 'utils';
|
import { calculateStatus } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timesheets header
|
* Project Timesheets header
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function TimesheetsHeader() {
|
export function ProjectTimesheetsHeader() {
|
||||||
return (
|
return (
|
||||||
<DetailFinancialSection>
|
<DetailFinancialSection>
|
||||||
<DetailFinancialCard label={'Project estimate'} value={'3.14'} />
|
<DetailFinancialCard label={'Project estimate'} value={'3.14'} />
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
DataTable,
|
||||||
|
TableSkeletonRows,
|
||||||
|
TableSkeletonHeader,
|
||||||
|
} from '@/components';
|
||||||
|
import { ActionsMenu } from './components';
|
||||||
|
import { useProjectTimesheetColumns } from './hooks';
|
||||||
|
import { TABLES } from '@/constants/tables';
|
||||||
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timesheet DataTable.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectTimesheetsTableRoot({
|
||||||
|
// #withSettings
|
||||||
|
timesheetsTableSize,
|
||||||
|
}) {
|
||||||
|
// Retrieve project timesheet table columns.
|
||||||
|
const columns = useProjectTimesheetColumns();
|
||||||
|
|
||||||
|
// Handle delete timesheet.
|
||||||
|
const handleDeleteTimesheet = () => {};
|
||||||
|
|
||||||
|
// Local storage memorizing columns widths.
|
||||||
|
const [initialColumnsWidths, , handleColumnResizing] =
|
||||||
|
useMemorizedColumnsWidths(TABLES.TIMESHEETS);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProjectTimesheetDataTable
|
||||||
|
columns={columns}
|
||||||
|
data={[]}
|
||||||
|
manualSortBy={true}
|
||||||
|
noInitialFetch={true}
|
||||||
|
sticky={true}
|
||||||
|
hideTableHeader={true}
|
||||||
|
ContextMenu={ActionsMenu}
|
||||||
|
TableLoadingRenderer={TableSkeletonRows}
|
||||||
|
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
||||||
|
initialColumnsWidths={initialColumnsWidths}
|
||||||
|
onColumnResizing={handleColumnResizing}
|
||||||
|
size={timesheetsTableSize}
|
||||||
|
payload={{
|
||||||
|
onDelete: handleDeleteTimesheet,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const ProjectTimesheetsTable = compose(
|
||||||
|
withSettings(({ timesheetsSettings }) => ({
|
||||||
|
timesheetsTableSize: timesheetsSettings?.tableSize,
|
||||||
|
})),
|
||||||
|
)(ProjectTimesheetsTableRoot);
|
||||||
|
|
||||||
|
const ProjectTimesheetDataTable = styled(DataTable)`
|
||||||
|
.table {
|
||||||
|
.thead .tr .th {
|
||||||
|
.resizer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbody {
|
||||||
|
.tr .td {
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar.td {
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.table-size--small {
|
||||||
|
.tbody .tr {
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { FormatDate, Icon, FormattedMessage as T } from 'components';
|
import { FormatDate, Icon } from '@/components';
|
||||||
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
|
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
|
||||||
import { safeCallback, firstLettersArgs } from 'utils';
|
import { safeCallback, firstLettersArgs } from '@/utils';
|
||||||
import { chain } from 'lodash';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table actions cell.
|
* Table actions cell.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function ActionsMenu({
|
export function ActionsMenu({
|
||||||
payload: { onDelete, onViewDetails },
|
payload: { onDelete, onViewDetails },
|
||||||
row: { original },
|
row: { original },
|
||||||
@@ -78,43 +76,3 @@ const TimesheetDescription = styled.span`
|
|||||||
margin: 0.3rem;
|
margin: 0.3rem;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve timesheet list columns columns.
|
|
||||||
*/
|
|
||||||
export function useTimesheetColumns() {
|
|
||||||
return React.useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
id: 'avatar',
|
|
||||||
Header: '',
|
|
||||||
Cell: AvatarCell,
|
|
||||||
className: 'avatar',
|
|
||||||
width: 45,
|
|
||||||
disableResizing: true,
|
|
||||||
disableSortBy: true,
|
|
||||||
clickable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'name',
|
|
||||||
Header: 'Header',
|
|
||||||
accessor: TimesheetAccessor,
|
|
||||||
width: 100,
|
|
||||||
className: 'name',
|
|
||||||
clickable: true,
|
|
||||||
textOverview: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'duration',
|
|
||||||
Header: '',
|
|
||||||
accessor: 'duration',
|
|
||||||
width: 100,
|
|
||||||
className: 'duration',
|
|
||||||
align: 'right',
|
|
||||||
clickable: true,
|
|
||||||
textOverview: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { AvatarCell, TimesheetAccessor } from './components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve project timesheet list columns.
|
||||||
|
*/
|
||||||
|
export function useProjectTimesheetColumns() {
|
||||||
|
return React.useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'avatar',
|
||||||
|
Header: '',
|
||||||
|
Cell: AvatarCell,
|
||||||
|
className: 'avatar',
|
||||||
|
width: 45,
|
||||||
|
disableResizing: true,
|
||||||
|
disableSortBy: true,
|
||||||
|
clickable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
Header: 'Header',
|
||||||
|
accessor: TimesheetAccessor,
|
||||||
|
width: 100,
|
||||||
|
className: 'name',
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'duration',
|
||||||
|
Header: '',
|
||||||
|
accessor: 'duration',
|
||||||
|
width: 100,
|
||||||
|
className: 'duration',
|
||||||
|
align: 'right',
|
||||||
|
clickable: true,
|
||||||
|
textOverview: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { ProjectTimesheetsTable } from './ProjectTimesheetsTable';
|
||||||
|
import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project Timesheets.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ProjectTimeSheets() {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<ProjectTimesheetsHeader />
|
||||||
|
<ProjectTimesheetTableCard>
|
||||||
|
<ProjectTimesheetsTable />
|
||||||
|
</ProjectTimesheetTableCard>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectTimesheetTableCard = styled.div`
|
||||||
|
margin: 22px 32px;
|
||||||
|
border: 1px solid #c8cad0;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
`;
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
// @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 { useTimesheetColumns, ActionsMenu } from './components';
|
|
||||||
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({
|
|
||||||
// #withSettings
|
|
||||||
timesheetsTableSize,
|
|
||||||
}) {
|
|
||||||
// Retrieve timesheet table columns.
|
|
||||||
const columns = useTimesheetColumns();
|
|
||||||
|
|
||||||
// Handle delete timesheet.
|
|
||||||
const handleDeleteTimesheet = () => {};
|
|
||||||
|
|
||||||
// Local storage memorizing columns widths.
|
|
||||||
const [initialColumnsWidths, , handleColumnResizing] =
|
|
||||||
useMemorizedColumnsWidths(TABLES.TIMESHEETS);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TimesheetDataTable
|
|
||||||
columns={columns}
|
|
||||||
data={Timesheet}
|
|
||||||
// loading={}
|
|
||||||
// headerLoading={}
|
|
||||||
// progressBarLoading={}
|
|
||||||
manualSortBy={true}
|
|
||||||
noInitialFetch={true}
|
|
||||||
sticky={true}
|
|
||||||
hideTableHeader={true}
|
|
||||||
ContextMenu={ActionsMenu}
|
|
||||||
TableLoadingRenderer={TableSkeletonRows}
|
|
||||||
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
|
||||||
initialColumnsWidths={initialColumnsWidths}
|
|
||||||
onColumnResizing={handleColumnResizing}
|
|
||||||
size={timesheetsTableSize}
|
|
||||||
payload={{
|
|
||||||
onDelete: handleDeleteTimesheet,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default compose(
|
|
||||||
withSettings(({ timesheetsSettings }) => ({
|
|
||||||
timesheetsTableSize: timesheetsSettings?.tableSize,
|
|
||||||
})),
|
|
||||||
)(TimesheetsTable);
|
|
||||||
|
|
||||||
const TimesheetDataTable = styled(DataTable)`
|
|
||||||
.table {
|
|
||||||
.thead .tr .th {
|
|
||||||
.resizer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
|
|
||||||
&[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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import TimesheetsTable from './TimesheetsTable';
|
|
||||||
import TimesheetsHeader from './TimesheetsHeader';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project Timesheet.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function ProjectTimesheet() {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<TimesheetsHeader />
|
|
||||||
<ProjectTimesheetTableCard>
|
|
||||||
<TimesheetsTable />
|
|
||||||
</ProjectTimesheetTableCard>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectTimesheetTableCard = styled.div`
|
|
||||||
margin: 20px;
|
|
||||||
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
|
|
||||||
border-radius: 3px;
|
|
||||||
background: #fff;
|
|
||||||
`;
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
export const projectTranslations = [
|
||||||
|
{ name: intl.get('project_details.new_expenses'), path: 'expense' },
|
||||||
|
{
|
||||||
|
name: intl.get('project_details.new_estimated_expenses'),
|
||||||
|
path: 'estimated_expense',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -29,17 +29,16 @@ export const FinancialProgressBar = ({ ...rest }) => {
|
|||||||
|
|
||||||
const FinancialSectionWrap = styled.div`
|
const FinancialSectionWrap = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
margin: 22px 32px;
|
||||||
margin: 20px 20px 20px;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FinancialSectionCard = styled.div`
|
const FinancialSectionCard = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-shrink: 0;
|
flex-shrink: 1;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
width: 220px;
|
width: 230px;
|
||||||
height: 116px;
|
height: 116px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
|
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
|
||||||
@@ -47,7 +46,6 @@ const FinancialSectionCard = styled.div`
|
|||||||
|
|
||||||
const FinancialSectionCardContent = styled.div`
|
const FinancialSectionCardContent = styled.div`
|
||||||
margin: 16px;
|
margin: 16px;
|
||||||
/* flex-direction: column; */
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FinancialCardWrap = styled.div``;
|
const FinancialCardWrap = styled.div``;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
//@ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -7,7 +6,7 @@ import {
|
|||||||
PopoverInteractionKind,
|
PopoverInteractionKind,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { Select } from '@blueprintjs/select';
|
import { Select } from '@blueprintjs/select';
|
||||||
import { Icon, FormattedMessage as T } from 'components';
|
import { Icon, FormattedMessage as T } from '@/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -15,7 +14,7 @@ import { Icon, FormattedMessage as T } from 'components';
|
|||||||
* @param {*} param1
|
* @param {*} param1
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const transactionItemRenderer = (
|
const projectTransactionItemRenderer = (
|
||||||
transaction,
|
transaction,
|
||||||
{ handleClick, modifiers, query },
|
{ handleClick, modifiers, query },
|
||||||
) => {
|
) => {
|
||||||
@@ -29,8 +28,8 @@ const transactionItemRenderer = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const transactionSelectProps = {
|
const projectTransactionSelectProps = {
|
||||||
itemRenderer: transactionItemRenderer,
|
itemRenderer: projectTransactionItemRenderer,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
popoverProps: {
|
popoverProps: {
|
||||||
minimal: true,
|
minimal: true,
|
||||||
@@ -43,13 +42,13 @@ const transactionSelectProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Project transactions select
|
||||||
* @param
|
* @param
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function TransactionSelect({ transactions, ...rest }) {
|
export function ProjectTransactionsSelect({ transactions, ...rest }) {
|
||||||
return (
|
return (
|
||||||
<Select {...transactionSelectProps} items={transactions} {...rest}>
|
<Select {...projectTransactionSelectProps} items={transactions} {...rest}>
|
||||||
<Button
|
<Button
|
||||||
minimal={true}
|
minimal={true}
|
||||||
icon={<Icon icon={'plus'} />}
|
icon={<Icon icon={'plus'} />}
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
export * from './ProjectSelect';
|
export * from './ProjectTransactionsSelect';
|
||||||
export * from './TransactionSelect';
|
|
||||||
export * from './FinancialSection';
|
export * from './FinancialSection';
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
//@ts-nocheck
|
import React, { useEffect } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import ProjectDetailActionsBar from './ProjectDetailActionsBar';
|
import ProjectDetailActionsBar from './ProjectDetailActionsBar';
|
||||||
import ProjectDetailTabs from './ProjectDetailTabs';
|
import ProjectDetailTabs from './ProjectDetailTabs';
|
||||||
import { DashboardPageContent } from 'components';
|
import { DashboardPageContent } from '@/components';
|
||||||
import { ProjectDetailProvider } from './ProjectDetailProvider';
|
import { ProjectDetailProvider } from './ProjectDetailProvider';
|
||||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project tabs.
|
* Project tabs.
|
||||||
@@ -20,7 +19,7 @@ function ProjectTabs({
|
|||||||
state: { projectName, projectId },
|
state: { projectName, projectId },
|
||||||
} = useLocation();
|
} = useLocation();
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
changePageTitle(projectName);
|
changePageTitle(projectName);
|
||||||
}, [changePageTitle, projectName]);
|
}, [changePageTitle, projectName]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
const Schema = Yup.object().shape({
|
||||||
|
expenseName: Yup.string().label(
|
||||||
|
intl.get('project_expense.schema.label.expense_name'),
|
||||||
|
),
|
||||||
|
estimatedExpense: Yup.number().label(
|
||||||
|
intl.get('project_expense.schema.label.estimated_expense'),
|
||||||
|
),
|
||||||
|
expemseDate: Yup.date(),
|
||||||
|
expenseQuantity: Yup.number().label(
|
||||||
|
intl.get('project_expense.schema.label.quantity'),
|
||||||
|
),
|
||||||
|
expenseUnitPrice: Yup.number().label(
|
||||||
|
intl.get('project_expense.schema.label.unitPrice'),
|
||||||
|
),
|
||||||
|
expenseTotal: Yup.number(),
|
||||||
|
expenseCharge: Yup.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateProjectExpenseFormSchema = Schema;
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { CreateProjectExpenseFormSchema } from './ProjectExpenseForm.schema';
|
||||||
|
import ProjectExpenseFormContent from './ProjectExpenseFormContent';
|
||||||
|
import { useProjectExpenseFormContext } from './ProjectExpenseFormProvider';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
const defaultInitialValues = {
|
||||||
|
expenseName: '',
|
||||||
|
estimatedExpense: '',
|
||||||
|
expemseDate: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
|
expenseUnitPrice: '',
|
||||||
|
expenseQuantity: 1,
|
||||||
|
expenseCharge: '% markup',
|
||||||
|
percentage: '',
|
||||||
|
expenseTotal: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project expense form.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectExpenseForm({
|
||||||
|
//#withDialogActions
|
||||||
|
closeDialog,
|
||||||
|
}) {
|
||||||
|
const initialValues = {
|
||||||
|
...defaultInitialValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles the form submit.
|
||||||
|
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
|
const form = {};
|
||||||
|
|
||||||
|
// Handle request response success.
|
||||||
|
const onSuccess = (response) => {
|
||||||
|
AppToaster.show({});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle request response errors.
|
||||||
|
const onError = ({
|
||||||
|
response: {
|
||||||
|
data: { errors },
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
validationSchema={CreateProjectExpenseFormSchema}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
component={ProjectExpenseFormContent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogActions)(ProjectExpenseForm);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { Classes, ControlGroup } from '@blueprintjs/core';
|
||||||
|
import { FFormGroup, FInputGroup, Choose } from '@/components';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
function PercentageFormField() {
|
||||||
|
return (
|
||||||
|
<FFormGroup
|
||||||
|
label={intl.get('expenses.dialog.percentage')}
|
||||||
|
name={'percentage'}
|
||||||
|
>
|
||||||
|
<FInputGroup name="percentage" />
|
||||||
|
</FFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomPirceField() {
|
||||||
|
return (
|
||||||
|
<ControlGroup className={Classes.FILL}>
|
||||||
|
<FFormGroup
|
||||||
|
name={'expenseUnitPrice'}
|
||||||
|
label={intl.get('expenses.dialog.unit_price')}
|
||||||
|
>
|
||||||
|
<FInputGroup name="expenseUnitPrice" />
|
||||||
|
</FFormGroup>
|
||||||
|
<FFormGroup
|
||||||
|
name={'expenseTotal'}
|
||||||
|
label={intl.get('expenses.dialog.total')}
|
||||||
|
>
|
||||||
|
<FInputGroup name="expenseTotal" />
|
||||||
|
</FFormGroup>
|
||||||
|
</ControlGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expense form charge fields.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ExpenseFormChargeFields() {
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Choose>
|
||||||
|
<Choose.When condition={values.expenseCharge === 'markup'}>
|
||||||
|
<PercentageFormField />
|
||||||
|
</Choose.When>
|
||||||
|
<Choose.When condition={values.expenseCharge === 'custom_pirce'}>
|
||||||
|
<CustomPirceField />
|
||||||
|
</Choose.When>
|
||||||
|
</Choose>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Form } from 'formik';
|
||||||
|
import ProjectExpenseFormFields from './ProjectExpenseFormFields';
|
||||||
|
import ProjectExpneseFormFloatingActions from './ProjectExpneseFormFloatingActions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expense form content.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ProjectExpenseFormContent() {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<ProjectExpenseFormFields />
|
||||||
|
<ProjectExpneseFormFloatingActions />
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ProjectExpenseFormProvider } from './ProjectExpenseFormProvider';
|
||||||
|
import ProjectExpenseForm from './ProjectExpenseForm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project expense form dialog content.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ProjectExpenseFormDialogContent({
|
||||||
|
// #ownProps
|
||||||
|
dialogName,
|
||||||
|
expense,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ProjectExpenseFormProvider dialogName={dialogName} expenseId={expense}>
|
||||||
|
<ProjectExpenseForm />
|
||||||
|
</ProjectExpenseFormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
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 '@/constants/classes';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
FFormGroup,
|
||||||
|
FInputGroup,
|
||||||
|
FDateInput,
|
||||||
|
FormattedMessage as T,
|
||||||
|
} from '@/components';
|
||||||
|
import {
|
||||||
|
ExpenseSelect,
|
||||||
|
FInputGroupComponent,
|
||||||
|
ChangeTypesSelect,
|
||||||
|
} from '../../components';
|
||||||
|
import ExpenseFormChargeFields from './ProjectExpenseFormChargeFields';
|
||||||
|
import { momentFormatter } from '@/utils';
|
||||||
|
import { useProjectExpenseFormContext } from './ProjectExpenseFormProvider';
|
||||||
|
import { expenseChargeOption } from '../common/modalChargeOptions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project expense form fields.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function ProjectExpenseFormFields() {
|
||||||
|
return (
|
||||||
|
<div className={Classes.DIALOG_BODY}>
|
||||||
|
{/*------------ Expense Name -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
label={intl.get('project_expense.dialog.expense_name')}
|
||||||
|
name={'expenseName'}
|
||||||
|
>
|
||||||
|
<FInputGroup name="expenseName" />
|
||||||
|
</FFormGroup>
|
||||||
|
{/*------------ Track to Expense -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
name={'estimatedExpense'}
|
||||||
|
label={intl.get('project_expense.dialog.track_expense')}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
>
|
||||||
|
<ExpenseSelect
|
||||||
|
name={'estimatedExpense'}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
expenses={[{ id: 1, name: 'Expense 1' }]}
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/*------------ Extimated Date -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
label={intl.get('project_expense.dialog.expense_date')}
|
||||||
|
name={'expemseDate'}
|
||||||
|
className={classNames(CLASSES.FILL, 'form-group--date')}
|
||||||
|
>
|
||||||
|
<FDateInput
|
||||||
|
{...momentFormatter('YYYY/MM/DD')}
|
||||||
|
name="expemseDate"
|
||||||
|
formatDate={(date) => date.toLocaleString()}
|
||||||
|
popoverProps={{
|
||||||
|
position: Position.BOTTOM,
|
||||||
|
minimal: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
{/*------------ Quantity -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
label={intl.get('project_expense.dialog.quantity')}
|
||||||
|
name={'expenseQuantity'}
|
||||||
|
>
|
||||||
|
<FInputGroupComponent name="expenseQuantity" />
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<MetaLineLabel>
|
||||||
|
<T id={'project_expense.dialog.cost_to_you'} />
|
||||||
|
</MetaLineLabel>
|
||||||
|
{/*------------ Unit Price -----------*/}
|
||||||
|
<ControlGroup className={Classes.FILL}>
|
||||||
|
<FFormGroup
|
||||||
|
name={'unitPrice'}
|
||||||
|
label={intl.get('project_expense.dialog.unit_price')}
|
||||||
|
>
|
||||||
|
<FInputGroupComponent name="expenseUnitPrice" />
|
||||||
|
</FFormGroup>
|
||||||
|
<FFormGroup
|
||||||
|
name={'expenseTotal'}
|
||||||
|
label={intl.get('project_expense.dialog.expense_total')}
|
||||||
|
>
|
||||||
|
<FInputGroup name="expenseTotal" />
|
||||||
|
</FFormGroup>
|
||||||
|
</ControlGroup>
|
||||||
|
|
||||||
|
<MetaLineLabel>
|
||||||
|
<T id={'project_expense.dialog.what_you_ll_charge'} />
|
||||||
|
</MetaLineLabel>
|
||||||
|
{/*------------ Charge -----------*/}
|
||||||
|
<FFormGroup
|
||||||
|
name={'expenseCharge'}
|
||||||
|
label={<T id={'project_expense.dialog.charge'} />}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
>
|
||||||
|
<ChangeTypesSelect
|
||||||
|
name="expenseCharge"
|
||||||
|
items={expenseChargeOption}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
filterable={false}
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
{/*------------ Charge Fields -----------*/}
|
||||||
|
<ExpenseFormChargeFields />
|
||||||
|
|
||||||
|
{/*------------ Total -----------*/}
|
||||||
|
<ExpenseTotalBase>
|
||||||
|
<ExpenseTotalLabel>
|
||||||
|
<T id={'project_expense.dialog.total'} />
|
||||||
|
</ExpenseTotalLabel>
|
||||||
|
<ExpenseTotal>0.00</ExpenseTotal>
|
||||||
|
</ExpenseTotalBase>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetaLineLabel = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ExpenseTotalBase = styled.div`
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ExpenseTotalLabel = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
opacity: 0.75;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ExpenseTotal = styled.div`
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding-left: 14px;
|
||||||
|
line-height: 2rem;
|
||||||
|
`;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DialogContent } from '@/components';
|
||||||
|
|
||||||
|
const ProjectExpenseFormContext = React.createContext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project expense form provider.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectExpenseFormProvider({
|
||||||
|
//#OwnProps
|
||||||
|
dialogName,
|
||||||
|
expenseId,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
// state provider.
|
||||||
|
const provider = {
|
||||||
|
dialogName,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<ProjectExpenseFormContext.Provider value={provider} {...props} />
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useProjectExpenseFormContext = () =>
|
||||||
|
React.useContext(ProjectExpenseFormContext);
|
||||||
|
export { ProjectExpenseFormProvider, useProjectExpenseFormContext };
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||||
|
import { FormattedMessage as T } from '@/components';
|
||||||
|
import { useProjectExpenseFormContext } from './ProjectExpenseFormProvider';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
function ProjectExpneseFormFloatingActions({
|
||||||
|
// #withDialogActions
|
||||||
|
closeDialog,
|
||||||
|
}) {
|
||||||
|
// Formik context.
|
||||||
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
// expense form dialog context.
|
||||||
|
const { dialogName } = useProjectExpenseFormContext();
|
||||||
|
|
||||||
|
// Handle close button click.
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
closeDialog(dialogName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
|
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
|
||||||
|
<T id={'cancel'} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
loading={isSubmitting}
|
||||||
|
style={{ minWidth: '75px' }}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{<T id={'save'} />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogActions)(ProjectExpneseFormFloatingActions);
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
|
||||||
|
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
const ProjectExpenseFormeDialogContent = React.lazy(
|
||||||
|
() => import('./ProjectExpenseFormDialogContent'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project expense form dialog.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectExpenseFormDialog({
|
||||||
|
dialogName,
|
||||||
|
payload: { projectId = null },
|
||||||
|
isOpen,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ProjectExpenseFormDialogRoot
|
||||||
|
name={dialogName}
|
||||||
|
title={<T id={'project_expense.dialog.label'} />}
|
||||||
|
isOpen={isOpen}
|
||||||
|
autoFocus={true}
|
||||||
|
canEscapeKeyClose={true}
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
>
|
||||||
|
<DialogSuspense>
|
||||||
|
<ProjectExpenseFormeDialogContent
|
||||||
|
dialogName={dialogName}
|
||||||
|
expense={projectId}
|
||||||
|
/>
|
||||||
|
</DialogSuspense>
|
||||||
|
</ProjectExpenseFormDialogRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogRedux())(ProjectExpenseFormDialog);
|
||||||
|
|
||||||
|
const ProjectExpenseFormDialogRoot = styled(Dialog)`
|
||||||
|
.bp3-dialog-body {
|
||||||
|
.bp3-form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
label.bp3-label {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bp3-dialog-footer {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { DATATYPES_LENGTH } from 'common/dataTypes';
|
|
||||||
|
|
||||||
const Schema = Yup.object().shape({
|
const Schema = Yup.object().shape({
|
||||||
contact: Yup.string().label(intl.get('project.schema.label.contact')),
|
contact_id: Yup.string().label(intl.get('project.schema.label.contact')),
|
||||||
projectName: Yup.string()
|
name: Yup.string()
|
||||||
.label(intl.get('project.schema.label.project_name'))
|
.label(intl.get('project.schema.label.project_name'))
|
||||||
.required(),
|
.required(),
|
||||||
projectDeadline: Yup.date()
|
deadline: Yup.date()
|
||||||
.label(intl.get('project.schema.label.deadline'))
|
.label(intl.get('project.schema.label.deadline'))
|
||||||
.required(),
|
.required(),
|
||||||
projectState: Yup.boolean().label(
|
published: Yup.boolean().label(
|
||||||
intl.get('project.schema.label.project_state'),
|
intl.get('project.schema.label.project_state'),
|
||||||
),
|
),
|
||||||
projectCost: Yup.number().label(
|
cost_estimate: Yup.number().label(
|
||||||
intl.get('project.schema.label.project_cost'),
|
intl.get('project.schema.label.project_cost'),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { AppToaster } from 'components';
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
import ProjectFormContent from './ProjectFormContent';
|
import ProjectFormContent from './ProjectFormContent';
|
||||||
import { CreateProjectFormSchema } from './ProjectForm.schema';
|
import { CreateProjectFormSchema } from './ProjectForm.schema';
|
||||||
import { useProjectFormContext } from './ProjectFormProvider';
|
import { useProjectFormContext } from './ProjectFormProvider';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
import { compose } from 'utils';
|
import { compose, transformToForm } from '@/utils';
|
||||||
|
|
||||||
const defaultInitialValues = {
|
const defaultInitialValues = {
|
||||||
contact: '',
|
contact_id: '',
|
||||||
projectName: '',
|
name: '',
|
||||||
projectDeadline: moment(new Date()).format('YYYY-MM-DD'),
|
deadline: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
projectState: false,
|
published: false,
|
||||||
projectCost: '',
|
cost_estimate: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,20 +28,37 @@ function ProjectForm({
|
|||||||
closeDialog,
|
closeDialog,
|
||||||
}) {
|
}) {
|
||||||
// project form dialog context.
|
// project form dialog context.
|
||||||
const { dialogName } = useProjectFormContext();
|
const {
|
||||||
|
dialogName,
|
||||||
|
project,
|
||||||
|
isNewMode,
|
||||||
|
projectId,
|
||||||
|
createProjectMutate,
|
||||||
|
editProjectMutate,
|
||||||
|
} = useProjectFormContext();
|
||||||
|
|
||||||
// Initial form values
|
// Initial form values
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
...defaultInitialValues,
|
...defaultInitialValues,
|
||||||
|
...transformToForm(project, defaultInitialValues),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles the form submit.
|
// Handles the form submit.
|
||||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
const form = {};
|
setSubmitting(true);
|
||||||
|
const form = { ...values };
|
||||||
|
|
||||||
// Handle request response success.
|
// Handle request response success.
|
||||||
const onSuccess = (response) => {
|
const onSuccess = (response) => {
|
||||||
AppToaster.show({});
|
AppToaster.show({
|
||||||
|
message: intl.get(
|
||||||
|
isNewMode
|
||||||
|
? 'projects.dialog.success_message'
|
||||||
|
: 'projects.dialog.edit_success_message',
|
||||||
|
),
|
||||||
|
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
closeDialog(dialogName);
|
closeDialog(dialogName);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,6 +70,12 @@ function ProjectForm({
|
|||||||
}) => {
|
}) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isNewMode) {
|
||||||
|
createProjectMutate(form).then(onSuccess).catch(onError);
|
||||||
|
} else {
|
||||||
|
editProjectMutate([projectId, form]).then(onSuccess).catch(onError);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
import { Classes, Position, FormGroup, ControlGroup } from '@blueprintjs/core';
|
import { Classes, Position, FormGroup, ControlGroup } from '@blueprintjs/core';
|
||||||
import { FastField } from 'formik';
|
import { FastField } from 'formik';
|
||||||
import { CLASSES } from 'common/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
FFormGroup,
|
FFormGroup,
|
||||||
@@ -17,13 +16,13 @@ import {
|
|||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
FieldRequiredHint,
|
FieldRequiredHint,
|
||||||
CustomerSelectField,
|
CustomerSelectField,
|
||||||
} from 'components';
|
} from '@/components';
|
||||||
import {
|
import {
|
||||||
inputIntent,
|
inputIntent,
|
||||||
momentFormatter,
|
momentFormatter,
|
||||||
tansformDateValue,
|
tansformDateValue,
|
||||||
handleDateChange,
|
handleDateChange,
|
||||||
} from 'utils';
|
} from '@/utils';
|
||||||
import { useProjectFormContext } from './ProjectFormProvider';
|
import { useProjectFormContext } from './ProjectFormProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,7 +39,7 @@ function ProjectFormFields() {
|
|||||||
return (
|
return (
|
||||||
<div className={Classes.DIALOG_BODY}>
|
<div className={Classes.DIALOG_BODY}>
|
||||||
{/*------------ Contact -----------*/}
|
{/*------------ Contact -----------*/}
|
||||||
<FastField name={'contact'}>
|
<FastField name={'contact_id'}>
|
||||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={intl.get('projects.dialog.contact')}
|
label={intl.get('projects.dialog.contact')}
|
||||||
@@ -53,7 +52,7 @@ function ProjectFormFields() {
|
|||||||
selectedContactId={value}
|
selectedContactId={value}
|
||||||
defaultSelectText={'Select Contact Account'}
|
defaultSelectText={'Select Contact Account'}
|
||||||
onContactSelected={(customer) => {
|
onContactSelected={(customer) => {
|
||||||
form.setFieldValue('contact', customer.id);
|
form.setFieldValue('contact_id', customer.id);
|
||||||
}}
|
}}
|
||||||
allowCreate={true}
|
allowCreate={true}
|
||||||
popoverFill={true}
|
popoverFill={true}
|
||||||
@@ -64,19 +63,20 @@ function ProjectFormFields() {
|
|||||||
{/*------------ Project Name -----------*/}
|
{/*------------ Project Name -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
label={intl.get('projects.dialog.project_name')}
|
label={intl.get('projects.dialog.project_name')}
|
||||||
name={'projectName'}
|
name={'name'}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
>
|
>
|
||||||
<FInputGroup name="projectName" />
|
<FInputGroup name="name" />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
{/*------------ DeadLine -----------*/}
|
{/*------------ DeadLine -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
label={intl.get('projects.dialog.deadline')}
|
label={intl.get('projects.dialog.deadline')}
|
||||||
name={'projectDeadline'}
|
name={'deadline'}
|
||||||
className={classNames(CLASSES.FILL, 'form-group--date')}
|
className={classNames(CLASSES.FILL, 'form-group--date')}
|
||||||
>
|
>
|
||||||
<FDateInput
|
<FDateInput
|
||||||
{...momentFormatter('YYYY/MM/DD')}
|
{...momentFormatter('YYYY/MM/DD')}
|
||||||
name="projectDeadline"
|
name="deadline"
|
||||||
formatDate={(date) => date.toLocaleString()}
|
formatDate={(date) => date.toLocaleString()}
|
||||||
popoverProps={{
|
popoverProps={{
|
||||||
position: Position.BOTTOM,
|
position: Position.BOTTOM,
|
||||||
@@ -86,22 +86,23 @@ function ProjectFormFields() {
|
|||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
{/*------------ CheckBox -----------*/}
|
{/*------------ CheckBox -----------*/}
|
||||||
<FFormGroup name={'projectState'}>
|
<FFormGroup name={'published'}>
|
||||||
<FCheckbox
|
<FCheckbox
|
||||||
name="projectState"
|
name="published"
|
||||||
label={intl.get('projects.dialog.calculator_expenses')}
|
label={intl.get('projects.dialog.calculator_expenses')}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
{/*------------ Cost Estimate -----------*/}
|
{/*------------ Cost Estimate -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'projectCost'}
|
name={'cost_estimate'}
|
||||||
label={intl.get('projects.dialog.cost_estimate')}
|
label={intl.get('projects.dialog.cost_estimate')}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
>
|
>
|
||||||
<ControlGroup>
|
<ControlGroup>
|
||||||
<InputPrependText text={'USD'} />
|
<InputPrependText text={'USD'} />
|
||||||
<FMoneyInputGroup
|
<FMoneyInputGroup
|
||||||
disabled={values.projectState}
|
disabled={values.published}
|
||||||
name={'project_cost'}
|
name={'cost_estimate'}
|
||||||
allowDecimals={true}
|
allowDecimals={true}
|
||||||
allowNegativeValue={true}
|
allowNegativeValue={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { Intent, Button, Classes } from '@blueprintjs/core';
|
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T } from 'components';
|
import { FormattedMessage as T } from '@/components';
|
||||||
import { useProjectFormContext } from './ProjectFormProvider';
|
import { useProjectFormContext } from './ProjectFormProvider';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project form floating actions.
|
* Project form floating actions.
|
||||||
@@ -15,12 +14,12 @@ function ProjectFormFloatingActions({
|
|||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
closeDialog,
|
closeDialog,
|
||||||
}) {
|
}) {
|
||||||
// project form dialog context.
|
|
||||||
const { dialogName } = useProjectFormContext();
|
|
||||||
|
|
||||||
// Formik context.
|
// Formik context.
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
// project form dialog context.
|
||||||
|
const { dialogName } = useProjectFormContext();
|
||||||
|
|
||||||
// Handle close button click.
|
// Handle close button click.
|
||||||
const handleCancelBtnClick = () => {
|
const handleCancelBtnClick = () => {
|
||||||
closeDialog(dialogName);
|
closeDialog(dialogName);
|
||||||
@@ -29,7 +28,7 @@ function ProjectFormFloatingActions({
|
|||||||
return (
|
return (
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
<div className={Classes.DIALOG_FOOTER}>
|
||||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||||
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
|
<Button onClick={handleCancelBtnClick} style={{ minWidth: '85px' }}>
|
||||||
<T id={'cancel'} />
|
<T id={'cancel'} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useCustomers } from 'hooks/query';
|
import { useCustomers } from '@/hooks/query';
|
||||||
import { DialogContent } from 'components';
|
import { useCreateProject, useEditProject, useProject } from '../../hooks';
|
||||||
|
import { DialogContent } from '@/components';
|
||||||
|
|
||||||
const ProjectFormContext = React.createContext();
|
const ProjectFormContext = React.createContext();
|
||||||
|
|
||||||
@@ -15,20 +15,36 @@ function ProjectFormProvider({
|
|||||||
projectId,
|
projectId,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
|
// Create and edit project mutations.
|
||||||
|
const { mutateAsync: createProjectMutate } = useCreateProject();
|
||||||
|
const { mutateAsync: editProjectMutate } = useEditProject();
|
||||||
|
|
||||||
|
// Handle fetch project detail.
|
||||||
|
const { data: project, isLoading: isProjectLoading } = useProject(projectId, {
|
||||||
|
enabled: !!projectId,
|
||||||
|
});
|
||||||
|
|
||||||
// Handle fetch customers data table or list
|
// Handle fetch customers data table or list
|
||||||
const {
|
const {
|
||||||
data: { customers },
|
data: { customers },
|
||||||
isLoading: isCustomersLoading,
|
isLoading: isCustomersLoading,
|
||||||
} = useCustomers({ page_size: 10000 });
|
} = useCustomers({ page_size: 10000 });
|
||||||
|
|
||||||
|
const isNewMode = !projectId;
|
||||||
|
|
||||||
// State provider.
|
// State provider.
|
||||||
const provider = {
|
const provider = {
|
||||||
customers,
|
customers,
|
||||||
dialogName,
|
dialogName,
|
||||||
|
project,
|
||||||
|
projectId,
|
||||||
|
isNewMode,
|
||||||
|
createProjectMutate,
|
||||||
|
editProjectMutate,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent isLoading={isCustomersLoading}>
|
<DialogContent isLoading={isCustomersLoading || isProjectLoading}>
|
||||||
<ProjectFormContext.Provider value={provider} {...props} />
|
<ProjectFormContext.Provider value={provider} {...props} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
|
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
|
||||||
import withDialogRedux from 'components/DialogReduxConnect';
|
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
const ProjectDialogContent = React.lazy(
|
const ProjectDialogContent = React.lazy(
|
||||||
() => import('./ProjectFormDialogContent'),
|
() => import('./ProjectFormDialogContent'),
|
||||||
@@ -14,13 +14,19 @@ const ProjectDialogContent = React.lazy(
|
|||||||
*/
|
*/
|
||||||
function ProjectFormDialog({
|
function ProjectFormDialog({
|
||||||
dialogName,
|
dialogName,
|
||||||
payload: { projectId = null },
|
payload: { projectId = null, action },
|
||||||
isOpen,
|
isOpen,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ProjectFormDialogRoot
|
<ProjectFormDialogRoot
|
||||||
name={dialogName}
|
name={dialogName}
|
||||||
title={<T id={'projects.label.new_project'} />}
|
title={
|
||||||
|
action === 'edit' ? (
|
||||||
|
<T id="projects.dialog.edit_project" />
|
||||||
|
) : (
|
||||||
|
<T id={'projects.dialog.new_project'} />
|
||||||
|
)
|
||||||
|
}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
canEscapeKeyClose={true}
|
canEscapeKeyClose={true}
|
||||||
@@ -39,6 +45,7 @@ const ProjectFormDialogRoot = styled(Dialog)`
|
|||||||
.bp3-dialog-body {
|
.bp3-dialog-body {
|
||||||
.bp3-form-group {
|
.bp3-form-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
label.bp3-label {
|
label.bp3-label {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { DATATYPES_LENGTH } from 'common/dataTypes';
|
|
||||||
|
|
||||||
const Schema = Yup.object().shape({
|
const Schema = Yup.object().shape({
|
||||||
taskName: Yup.string()
|
taskName: Yup.string()
|
||||||
.label(intl.get('task.schema.label.task_name'))
|
.label(intl.get('task.schema.label.task_name'))
|
||||||
.required(),
|
.required(),
|
||||||
taskHouse: Yup.string().label(intl.get('task.schema.label.task_house')),
|
taskHouse: Yup.string().label(intl.get('task.schema.label.task_house')),
|
||||||
taskCharge: Yup.string().label(intl.get('task.schema.label.charge')).required(),
|
taskCharge: Yup.string()
|
||||||
|
.label(intl.get('task.schema.label.charge'))
|
||||||
|
.required(),
|
||||||
taskamount: Yup.number().label(intl.get('task.schema.label.amount')),
|
taskamount: Yup.number().label(intl.get('task.schema.label.amount')),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CreateTaskFormSchema = Schema;
|
export const CreateProjectTaskFormSchema = Schema;
|
||||||
@@ -1,31 +1,30 @@
|
|||||||
//@ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { CreateTaskFormSchema } from './TaskForm.schema';
|
import { CreateProjectTaskFormSchema } from './ProjectTaskForm.schema';
|
||||||
import { useTaskFormContext } from './TaskFormProvider';
|
import { useProjectTaskFormContext } from './ProjectTaskFormProvider';
|
||||||
import { AppToaster } from 'components';
|
import { AppToaster } from '@/components';
|
||||||
import TaskFormContent from './TaskFormContent';
|
import ProjectTaskFormContent from './ProjectTaskFormContent';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
const defaultInitialValues = {
|
const defaultInitialValues = {
|
||||||
taskName: '',
|
taskName: '',
|
||||||
taskHouse: '00:00',
|
taskHouse: '00:00',
|
||||||
taskCharge: 'Hourly rate',
|
taskCharge: 'hourly_rate',
|
||||||
taskamount: '100000000',
|
taskamount: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task form.
|
* Project task form.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function TaskForm({
|
function ProjectTaskForm({
|
||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
closeDialog,
|
closeDialog,
|
||||||
}) {
|
}) {
|
||||||
// task form dialog context.
|
// task form dialog context.
|
||||||
const { dialogName } = useTaskFormContext();
|
const { dialogName } = useProjectTaskFormContext();
|
||||||
|
|
||||||
// Initial form values
|
// Initial form values
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
@@ -51,12 +50,12 @@ function TaskForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
validationSchema={CreateTaskFormSchema}
|
validationSchema={CreateProjectTaskFormSchema}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
component={TaskFormContent}
|
component={ProjectTaskFormContent}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(withDialogActions)(TaskForm);
|
export default compose(withDialogActions)(ProjectTaskForm);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Form } from 'formik';
|
||||||
|
import ProjectTaskFormFields from './ProjectTaskFormFields';
|
||||||
|
import ProjectTaskFormFloatingActions from './ProjectTaskFormFloatingActions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task form content.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function TaskFormContent() {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<ProjectTaskFormFields />
|
||||||
|
<ProjectTaskFormFloatingActions />
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ProjectTaskFormProvider } from './ProjectTaskFormProvider';
|
||||||
|
import ProjectTaskForm from './ProjectTaskForm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project task form dialog content.
|
||||||
|
*/
|
||||||
|
export default function ProjectTaskFormDialogContent({
|
||||||
|
// #ownProps
|
||||||
|
dialogName,
|
||||||
|
task,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ProjectTaskFormProvider taskId={task} dialogName={dialogName}>
|
||||||
|
<ProjectTaskForm />
|
||||||
|
</ProjectTaskFormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,30 +8,29 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Row,
|
Row,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
} from 'components';
|
} from '@/components';
|
||||||
import { modalChargeOptions } from '../../../../common/modalChargeOptions';
|
import { taskChargeOptions } from '../common/modalChargeOptions';
|
||||||
|
import { ChangeTypesSelect } from '../../components';
|
||||||
import { TaskModalChargeSelect } from './components';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task form fields.
|
* Project task form fields.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function TaskFormFields() {
|
function ProjectTaskFormFields() {
|
||||||
// Formik context.
|
// Formik context.
|
||||||
const { values } = useFormikContext();
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={Classes.DIALOG_BODY}>
|
<div className={Classes.DIALOG_BODY}>
|
||||||
{/*------------ Task Name -----------*/}
|
{/*------------ Task Name -----------*/}
|
||||||
<FFormGroup label={<T id={'task.dialog.task_name'} />} name={'taskName'}>
|
<FFormGroup label={<T id={'project_task.dialog.task_name'} />} name={'taskName'}>
|
||||||
<FInputGroup name="taskName" />
|
<FInputGroup name="taskName" />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
{/*------------ Estimated Hours -----------*/}
|
{/*------------ Estimated Hours -----------*/}
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={4}>
|
<Col xs={4}>
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
label={<T id={'task.dialog.estimated_hours'} />}
|
label={<T id={'project_task.dialog.estimated_hours'} />}
|
||||||
name={'taskHouse'}
|
name={'taskHouse'}
|
||||||
>
|
>
|
||||||
<FInputGroup name="taskHouse" />
|
<FInputGroup name="taskHouse" />
|
||||||
@@ -42,16 +41,19 @@ function TaskFormFields() {
|
|||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'taskCharge'}
|
name={'taskCharge'}
|
||||||
className={'form-group--select-list'}
|
className={'form-group--select-list'}
|
||||||
label={<T id={'task.dialog.charge'} />}
|
label={<T id={'project_task.dialog.charge'} />}
|
||||||
>
|
>
|
||||||
<ControlGroup>
|
<ControlGroup>
|
||||||
<TaskModalChargeSelect
|
<ChangeTypesSelect
|
||||||
name="taskCharge"
|
name="taskCharge"
|
||||||
items={modalChargeOptions}
|
items={taskChargeOptions}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
filterable={false}
|
filterable={false}
|
||||||
/>
|
/>
|
||||||
<FInputGroup name="taskamount" />
|
<FInputGroup
|
||||||
|
name="taskamount"
|
||||||
|
disabled={values?.taskCharge === 'Non-chargeable'}
|
||||||
|
/>
|
||||||
</ControlGroup>
|
</ControlGroup>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -59,21 +61,22 @@ function TaskFormFields() {
|
|||||||
{/*------------ Estimated Amount -----------*/}
|
{/*------------ Estimated Amount -----------*/}
|
||||||
<EstimatedAmountBase>
|
<EstimatedAmountBase>
|
||||||
<EstimatedAmountContent>
|
<EstimatedAmountContent>
|
||||||
<T id={'task.dialog.estimated_amount'} />
|
<T id={'project_task.dialog.estimated_amount'} />
|
||||||
<EstimateAmount>$100000</EstimateAmount>
|
<EstimateAmount>0.00</EstimateAmount>
|
||||||
</EstimatedAmountContent>
|
</EstimatedAmountContent>
|
||||||
</EstimatedAmountBase>
|
</EstimatedAmountBase>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TaskFormFields;
|
export default ProjectTaskFormFields;
|
||||||
|
|
||||||
const EstimatedAmountBase = styled.div`
|
const EstimatedAmountBase = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
/* opacity: 0.7; */
|
line-height: 1.5rem;
|
||||||
|
opacity: 0.75;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EstimatedAmountContent = styled.span`
|
const EstimatedAmountContent = styled.span`
|
||||||
@@ -82,7 +85,7 @@ const EstimatedAmountContent = styled.span`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const EstimateAmount = styled.span`
|
const EstimateAmount = styled.span`
|
||||||
font-size: 13px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
`;
|
`;
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { Intent, Button, Classes } from '@blueprintjs/core';
|
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T } from 'components';
|
import { FormattedMessage as T } from '@/components';
|
||||||
import { useTaskFormContext } from './TaskFormProvider';
|
import { useProjectTaskFormContext } from './ProjectTaskFormProvider';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Task form floating actions.
|
* Task form floating actions.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function TaskFormFloatingActions({
|
function ProjectTaskFormFloatingActions({
|
||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
closeDialog,
|
closeDialog,
|
||||||
}) {
|
}) {
|
||||||
@@ -19,7 +18,7 @@ function TaskFormFloatingActions({
|
|||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
// Task form dialog context.
|
// Task form dialog context.
|
||||||
const { dialogName } = useTaskFormContext();
|
const { dialogName } = useProjectTaskFormContext();
|
||||||
|
|
||||||
// Handle close button click.
|
// Handle close button click.
|
||||||
const handleCancelBtnClick = () => {
|
const handleCancelBtnClick = () => {
|
||||||
@@ -45,4 +44,4 @@ function TaskFormFloatingActions({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(withDialogActions)(TaskFormFloatingActions);
|
export default compose(withDialogActions)(ProjectTaskFormFloatingActions);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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 };
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
|
||||||
|
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
const ProjectTaskFormDialogContent = React.lazy(
|
||||||
|
() => import('./ProjectTaskFormDialogContent'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project task form dialog.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ProjectTaskFormDialog({
|
||||||
|
dialogName,
|
||||||
|
payload: { taskId = null },
|
||||||
|
isOpen,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
name={dialogName}
|
||||||
|
title={intl.get('project_task.dialog.new_task')}
|
||||||
|
isOpen={isOpen}
|
||||||
|
autoFocus={true}
|
||||||
|
canEscapeKeyClose={true}
|
||||||
|
style={{ width: '500px' }}
|
||||||
|
>
|
||||||
|
<DialogSuspense>
|
||||||
|
<ProjectTaskFormDialogContent dialogName={dialogName} task={taskId} />
|
||||||
|
</DialogSuspense>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default compose(withDialogRedux())(ProjectTaskFormDialog);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import * as Yup from 'yup';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
const Schema = Yup.object().shape({
|
||||||
|
date: Yup.date()
|
||||||
|
.label(intl.get('project_time_entry.schema.label.date'))
|
||||||
|
.required(),
|
||||||
|
projectId: Yup.string()
|
||||||
|
.label(intl.get('project_time_entry.schema.label.project_name'))
|
||||||
|
.required(),
|
||||||
|
taskId: Yup.string()
|
||||||
|
.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('project_time_entry.schema.label.duration'))
|
||||||
|
.required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateProjectTimeEntryFormSchema = Schema;
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { AppToaster } from 'components';
|
import { AppToaster } from '@/components';
|
||||||
|
|
||||||
import TimeEntryFormContent from './TimeEntryFormContent';
|
import ProjectTimeEntryFormContent from './ProjectTimeEntryFormContent';
|
||||||
import { CreateTimeEntryFormSchema } from './TimeEntryForm.schema';
|
import { CreateProjectTimeEntryFormSchema } from './ProjectTimeEntryForm.schema';
|
||||||
import { useTimeEntryFormContext } from './TimeEntryFormProvider';
|
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
const defaultInitialValues = {
|
const defaultInitialValues = {
|
||||||
date: moment(new Date()).format('YYYY-MM-DD'),
|
date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
@@ -21,15 +20,15 @@ const defaultInitialValues = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time entry form.
|
* Project Time entry form.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function TimeEntryForm({
|
function ProjectTimeEntryForm({
|
||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
closeDialog,
|
closeDialog,
|
||||||
}) {
|
}) {
|
||||||
// time entry form dialog context.
|
// time entry form dialog context.
|
||||||
const { dialogName } = useTimeEntryFormContext();
|
const { dialogName } = useProjectTimeEntryFormContext();
|
||||||
|
|
||||||
// Initial form values
|
// Initial form values
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
@@ -58,12 +57,12 @@ function TimeEntryForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
validationSchema={CreateTimeEntryFormSchema}
|
validationSchema={CreateProjectTimeEntryFormSchema}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
component={TimeEntryFormContent}
|
component={ProjectTimeEntryFormContent}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(withDialogActions)(TimeEntryForm);
|
export default compose(withDialogActions)(ProjectTimeEntryForm);
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Form } from 'formik';
|
||||||
|
import ProjectTimeEntryFormFields from './ProjectTimeEntryFormFields';
|
||||||
|
import ProjectTimeEntryFormFloatingActions from './ProjectTimeEntryFormFloatingActions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time entry form content.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function TimeEntryFormContent() {
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
<ProjectTimeEntryFormFields />
|
||||||
|
<ProjectTimeEntryFormFloatingActions />
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ProjectTimeEntryFormProvider } from './ProjectTimeEntryFormProvider';
|
||||||
|
import ProjectTimeEntryForm from './ProjectTimeEntryForm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project time entry form dialog content.
|
||||||
|
* @returns {ReactNode}
|
||||||
|
*/
|
||||||
|
export default function ProjectTimeEntryFormDialogContent({
|
||||||
|
// #ownProps
|
||||||
|
dialogName,
|
||||||
|
project,
|
||||||
|
timeEntry,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ProjectTimeEntryFormProvider
|
||||||
|
projectId={project}
|
||||||
|
timeEntryId={timeEntry}
|
||||||
|
dialogName={dialogName}
|
||||||
|
>
|
||||||
|
<ProjectTimeEntryForm />
|
||||||
|
</ProjectTimeEntryFormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Classes, Intent, Position } from '@blueprintjs/core';
|
import { Classes, Intent, Position } from '@blueprintjs/core';
|
||||||
import { CLASSES } from 'common/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
FFormGroup,
|
FFormGroup,
|
||||||
@@ -13,26 +12,26 @@ import {
|
|||||||
FEditableText,
|
FEditableText,
|
||||||
FieldRequiredHint,
|
FieldRequiredHint,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
} from 'components';
|
} from '@/components';
|
||||||
import { ProjectSelect, TaskSelect } from './components';
|
import { TaskSelect, ProjectsSelect } from '../../components';
|
||||||
import { momentFormatter } from 'utils';
|
import { momentFormatter } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time entry form fields.
|
* Project time entry form fields.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function TimeEntryFormFields() {
|
function ProjectTimeEntryFormFields() {
|
||||||
return (
|
return (
|
||||||
<div className={Classes.DIALOG_BODY}>
|
<div className={Classes.DIALOG_BODY}>
|
||||||
{/*------------ Project -----------*/}
|
{/*------------ Project -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'projectId'}
|
name={'projectId'}
|
||||||
label={<T id={'time_entry.dialog.project'} />}
|
label={<T id={'project_time_entry.dialog.project'} />}
|
||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
>
|
>
|
||||||
<ProjectSelect
|
<ProjectsSelect
|
||||||
name={'tesc'}
|
name={'projectId'}
|
||||||
projects={[]}
|
projects={[]}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
/>
|
/>
|
||||||
@@ -40,7 +39,7 @@ function TimeEntryFormFields() {
|
|||||||
{/*------------ Task -----------*/}
|
{/*------------ Task -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'taskId'}
|
name={'taskId'}
|
||||||
label={<T id={'time_entry.dialog.task'} />}
|
label={<T id={'project_time_entry.dialog.task'} />}
|
||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
>
|
>
|
||||||
@@ -53,7 +52,7 @@ function TimeEntryFormFields() {
|
|||||||
|
|
||||||
{/*------------ Duration -----------*/}
|
{/*------------ Duration -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
label={intl.get('time_entry.dialog.duration')}
|
label={intl.get('project_time_entry.dialog.duration')}
|
||||||
name={'duration'}
|
name={'duration'}
|
||||||
labelInfo={<FieldRequiredHint />}
|
labelInfo={<FieldRequiredHint />}
|
||||||
>
|
>
|
||||||
@@ -61,7 +60,7 @@ function TimeEntryFormFields() {
|
|||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
{/*------------ Date -----------*/}
|
{/*------------ Date -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
label={intl.get('time_entry.dialog.date')}
|
label={intl.get('project_time_entry.dialog.date')}
|
||||||
name={'date'}
|
name={'date'}
|
||||||
className={classNames(CLASSES.FILL, 'form-group--date')}
|
className={classNames(CLASSES.FILL, 'form-group--date')}
|
||||||
>
|
>
|
||||||
@@ -78,20 +77,13 @@ function TimeEntryFormFields() {
|
|||||||
{/*------------ Description -----------*/}
|
{/*------------ Description -----------*/}
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'description'}
|
name={'description'}
|
||||||
label={intl.get('time_entry.dialog.description')}
|
label={intl.get('project_time_entry.dialog.description')}
|
||||||
className={'form-group--description'}
|
className={'form-group--description'}
|
||||||
>
|
>
|
||||||
<FTextArea name={'description'} />
|
<FTextArea name={'description'} />
|
||||||
{/* <FEditableText
|
|
||||||
multiline={true}
|
|
||||||
// minLines={1.78}
|
|
||||||
// maxLines={1.78}
|
|
||||||
name={'description'}
|
|
||||||
placeholder=""
|
|
||||||
/> */}
|
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimeEntryFormFields;
|
export default ProjectTimeEntryFormFields;
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { Intent, Button, Classes } from '@blueprintjs/core';
|
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T } from 'components';
|
import { FormattedMessage as T } from '@/components';
|
||||||
import { useTimeEntryFormContext } from './TimeEntryFormProvider';
|
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time entry form floating actions.
|
* Projcet time entry form floating actions.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function TimeEntryFormFloatingActions({
|
function ProjectTimeEntryFormFloatingActions({
|
||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
closeDialog,
|
closeDialog,
|
||||||
}) {
|
}) {
|
||||||
// time entry form dialog context.
|
// time entry form dialog context.
|
||||||
const { dialogName } = useTimeEntryFormContext();
|
const { dialogName } = useProjectTimeEntryFormContext();
|
||||||
|
|
||||||
// Formik context.
|
// Formik context.
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
@@ -45,4 +44,4 @@ function TimeEntryFormFloatingActions({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(withDialogActions)(TimeEntryFormFloatingActions);
|
export default compose(withDialogActions)(ProjectTimeEntryFormFloatingActions);
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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 };
|
||||||
@@ -1,45 +1,45 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
|
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
|
||||||
import withDialogRedux from 'components/DialogReduxConnect';
|
import withDialogRedux from '@/components/DialogReduxConnect';
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
const TimeEntryFormDialogContent = React.lazy(
|
const ProjectTimeEntryFormDialogContent = React.lazy(
|
||||||
() => import('./TimeEntryFormDialogContent'),
|
() => import('./ProjectTimeEntryFormDialogContent'),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time entry form dialog.
|
* Project time entry form dialog.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function TimeEntryFormDialog({
|
function ProjectTimeEntryFormDialog({
|
||||||
dialogName,
|
dialogName,
|
||||||
isOpen,
|
isOpen,
|
||||||
payload: { projectId = null, timeEntryId = null },
|
payload: { projectId = null, timeEntryId = null },
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TimeEntryFormDialogRoot
|
<ProjectTimeEntryFormDialogRoot
|
||||||
name={dialogName}
|
name={dialogName}
|
||||||
title={<T id={'time_entry.dialog.label'} />}
|
title={<T id={'project_time_entry.dialog.label'} />}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
canEscapeKeyClose={true}
|
canEscapeKeyClose={true}
|
||||||
style={{ width: '400px' }}
|
style={{ width: '400px' }}
|
||||||
>
|
>
|
||||||
<DialogSuspense>
|
<DialogSuspense>
|
||||||
<TimeEntryFormDialogContent
|
<ProjectTimeEntryFormDialogContent
|
||||||
dialogName={dialogName}
|
dialogName={dialogName}
|
||||||
project={projectId}
|
project={projectId}
|
||||||
timeEntry={timeEntryId}
|
timeEntry={timeEntryId}
|
||||||
/>
|
/>
|
||||||
</DialogSuspense>
|
</DialogSuspense>
|
||||||
</TimeEntryFormDialogRoot>
|
</ProjectTimeEntryFormDialogRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(withDialogRedux())(TimeEntryFormDialog);
|
export default compose(withDialogRedux())(ProjectTimeEntryFormDialog);
|
||||||
|
|
||||||
const TimeEntryFormDialogRoot = styled(Dialog)`
|
const ProjectTimeEntryFormDialogRoot = styled(Dialog)`
|
||||||
.bp3-dialog-body {
|
.bp3-dialog-body {
|
||||||
.bp3-form-group {
|
.bp3-form-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
@@ -13,17 +13,16 @@ import {
|
|||||||
DashboardFilterButton,
|
DashboardFilterButton,
|
||||||
DashboardRowsHeightButton,
|
DashboardRowsHeightButton,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
} from 'components';
|
DashboardActionsBar,
|
||||||
|
} from '@/components';
|
||||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
|
||||||
|
|
||||||
import withProjects from './withProjects';
|
import withProjects from './withProjects';
|
||||||
import withProjectsActions from './withProjectsActions';
|
import withProjectsActions from './withProjectsActions';
|
||||||
import withSettings from '../../../Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import withSettingsActions from '../../../Settings/withSettingsActions';
|
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Projects actions bar.
|
* Projects actions bar.
|
||||||
|
|||||||
@@ -1,54 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { DataTable } from 'components';
|
import { DataTable,TableSkeletonRows ,TableSkeletonHeader } from '@/components';
|
||||||
import { TABLES } from 'common/tables';
|
import { TABLES } from '@/constants/tables';
|
||||||
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
|
import ProjectsEmptyStatus from './ProjectsEmptyStatus';
|
||||||
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
|
|
||||||
import { useProjectsListContext } from './ProjectsListProvider';
|
import { useProjectsListContext } from './ProjectsListProvider';
|
||||||
import { useMemorizedColumnsWidths } from 'hooks';
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
import { useProjectsListColumns, ActionsMenu } from './components';
|
import { useProjectsListColumns, ActionsMenu } from './components';
|
||||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import withAlertsActions from '@/containers/Alert/withAlertActions';
|
||||||
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import withProjectsActions from './withProjectsActions';
|
import withProjectsActions from './withProjectsActions';
|
||||||
import withSettings from '../../../Settings/withSettings';
|
|
||||||
|
|
||||||
import { compose } from 'utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
const projects = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Maroon Bronze',
|
|
||||||
deadline: '2022-06-08T22:00:00.000Z',
|
|
||||||
display_name: 'Kyrie Rearden',
|
|
||||||
cost_estimate: '40000',
|
|
||||||
task_amount: '0',
|
|
||||||
is_process: true,
|
|
||||||
is_closed: false,
|
|
||||||
is_draft: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Project Sherwood',
|
|
||||||
deadline: '2022-06-08T22:00:00.000Z',
|
|
||||||
display_name: 'Ella-Grace Miller',
|
|
||||||
cost_estimate: '700',
|
|
||||||
task_amount: '300',
|
|
||||||
is_process: false,
|
|
||||||
is_closed: false,
|
|
||||||
is_draft: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Tax Compliance',
|
|
||||||
deadline: '2022-06-23T22:00:00.000Z',
|
|
||||||
display_name: 'Abby & Wells',
|
|
||||||
cost_estimate: '3000',
|
|
||||||
task_amount: '0',
|
|
||||||
is_process: true,
|
|
||||||
is_closed: false,
|
|
||||||
is_draft: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Projects list datatable.
|
* Projects list datatable.
|
||||||
@@ -58,11 +22,23 @@ function ProjectsDataTable({
|
|||||||
// #withDial
|
// #withDial
|
||||||
openDialog,
|
openDialog,
|
||||||
|
|
||||||
|
// #withAlertsActions
|
||||||
|
openAlert,
|
||||||
|
|
||||||
// #withSettings
|
// #withSettings
|
||||||
projectsTableSize,
|
projectsTableSize,
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
// Projects list context.
|
||||||
|
const { projects, isEmptyStatus, isProjectsLoading, isProjectsFetching } =
|
||||||
|
useProjectsListContext();
|
||||||
|
|
||||||
|
// Handle delete project.
|
||||||
|
const handleDeleteProject = ({ id }) => {
|
||||||
|
openAlert('project-delete', { projectId: id });
|
||||||
|
};
|
||||||
|
|
||||||
// Retrieve projects table columns.
|
// Retrieve projects table columns.
|
||||||
const columns = useProjectsListColumns();
|
const columns = useProjectsListColumns();
|
||||||
|
|
||||||
@@ -78,12 +54,13 @@ function ProjectsDataTable({
|
|||||||
const handleEditProject = (project) => {
|
const handleEditProject = (project) => {
|
||||||
openDialog('project-form', {
|
openDialog('project-form', {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
|
action: 'edit',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle new task button click.
|
// Handle new task button click.
|
||||||
const handleNewTaskButtonClick = () => {
|
const handleNewTaskButtonClick = () => {
|
||||||
openDialog('task-form');
|
openDialog('project-task-form');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Local storage memorizing columns widths.
|
// Local storage memorizing columns widths.
|
||||||
@@ -98,13 +75,18 @@ function ProjectsDataTable({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Display project empty status instead of the table.
|
||||||
|
if (isEmptyStatus) {
|
||||||
|
return <ProjectsEmptyStatus />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectsTable
|
<ProjectsTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={projects}
|
data={projects}
|
||||||
// loading={}
|
loading={isProjectsLoading}
|
||||||
// headerLoading={}
|
headerLoading={isProjectsLoading}
|
||||||
// progressBarLoading={}
|
progressBarLoading={isProjectsFetching}
|
||||||
manualSortBy={true}
|
manualSortBy={true}
|
||||||
noInitialFetch={true}
|
noInitialFetch={true}
|
||||||
sticky={true}
|
sticky={true}
|
||||||
@@ -119,6 +101,7 @@ function ProjectsDataTable({
|
|||||||
payload={{
|
payload={{
|
||||||
onViewDetails: handleViewDetailProject,
|
onViewDetails: handleViewDetailProject,
|
||||||
onEdit: handleEditProject,
|
onEdit: handleEditProject,
|
||||||
|
onDelete: handleDeleteProject,
|
||||||
onNewTask: handleNewTaskButtonClick,
|
onNewTask: handleNewTaskButtonClick,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -127,6 +110,7 @@ function ProjectsDataTable({
|
|||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
withDialogActions,
|
withDialogActions,
|
||||||
|
withAlertsActions,
|
||||||
withProjectsActions,
|
withProjectsActions,
|
||||||
withSettings(({ projectSettings }) => ({
|
withSettings(({ projectSettings }) => ({
|
||||||
projectsTableSize: projectSettings?.tableSize,
|
projectsTableSize: projectSettings?.tableSize,
|
||||||
@@ -138,8 +122,8 @@ const ProjectsTable = styled(DataTable)`
|
|||||||
.tr .td {
|
.tr .td {
|
||||||
padding: 0.5rem 0.8rem;
|
padding: 0.5rem 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar.td {
|
.avatar.td {
|
||||||
|
.cell-inner {
|
||||||
.avatar {
|
.avatar {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: #adbcc9;
|
background: #adbcc9;
|
||||||
@@ -163,6 +147,7 @@ const ProjectsTable = styled(DataTable)`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.table-size--small {
|
.table-size--small {
|
||||||
.tbody .tr {
|
.tbody .tr {
|
||||||
height: 45px;
|
height: 45px;
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Intent } from '@blueprintjs/core';
|
||||||
|
import { EmptyStatus, FormattedMessage as T } from '@/components';
|
||||||
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
function ProjectsEmptyStatus({
|
||||||
|
// #withDialogActions
|
||||||
|
openDialog,
|
||||||
|
}) {
|
||||||
|
// Handle new project button click.
|
||||||
|
const handleNewProjectClick = () => {
|
||||||
|
openDialog('project-form', {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmptyStatus
|
||||||
|
title={<T id="projects.empty_status.title" />}
|
||||||
|
description={
|
||||||
|
<p>
|
||||||
|
<T id="projects.empty_status.description" />
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
action={
|
||||||
|
<React.Fragment>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
large={true}
|
||||||
|
onClick={handleNewProjectClick}
|
||||||
|
>
|
||||||
|
<T id="projects.empty_status.action" />
|
||||||
|
</Button>
|
||||||
|
<Button intent={Intent.NONE} large={true}>
|
||||||
|
<T id={'learn_more'} />
|
||||||
|
</Button>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withDialogActions)(ProjectsEmptyStatus);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DashboardPageContent, DashboardContentTable } from 'components';
|
import { DashboardPageContent, DashboardContentTable } from '@/components';
|
||||||
|
|
||||||
import ProjectsActionsBar from './ProjectsActionsBar';
|
import ProjectsActionsBar from './ProjectsActionsBar';
|
||||||
import ProjectsViewTabs from './ProjectsViewTabs';
|
import ProjectsViewTabs from './ProjectsViewTabs';
|
||||||
@@ -9,7 +9,7 @@ import withProjects from './withProjects';
|
|||||||
import withProjectsActions from './withProjectsActions';
|
import withProjectsActions from './withProjectsActions';
|
||||||
|
|
||||||
import { ProjectsListProvider } from './ProjectsListProvider';
|
import { ProjectsListProvider } from './ProjectsListProvider';
|
||||||
import { compose, transformTableStateToQuery } from 'utils';
|
import { compose, transformTableStateToQuery } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Projects list.
|
* Projects list.
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
//@ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useResourceViews, useResourceMeta } from 'hooks/query';
|
import { isEmpty } from 'lodash';
|
||||||
import DashboardInsider from '../../../../components/Dashboard/DashboardInsider';
|
import { useResourceViews, useResourceMeta } from '@/hooks/query';
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { useProjects } from '../../hooks';
|
||||||
|
|
||||||
const ProjectsListContext = React.createContext();
|
const ProjectsListContext = React.createContext();
|
||||||
|
|
||||||
@@ -14,16 +15,32 @@ function ProjectsListProvider({ query, tableStateChanged, ...props }) {
|
|||||||
const { data: projectsViews, isLoading: isViewsLoading } =
|
const { data: projectsViews, isLoading: isViewsLoading } =
|
||||||
useResourceViews('projects');
|
useResourceViews('projects');
|
||||||
|
|
||||||
|
// Fetch accounts list according to the given custom view id.
|
||||||
|
const {
|
||||||
|
data: { projects },
|
||||||
|
isFetching: isProjectsFetching,
|
||||||
|
isLoading: isProjectsLoading,
|
||||||
|
} = useProjects(query, { keepPreviousData: true });
|
||||||
|
|
||||||
|
// Detarmines the datatable empty status.
|
||||||
|
const isEmptyStatus =
|
||||||
|
isEmpty(projects) && !tableStateChanged && !isProjectsLoading;
|
||||||
|
|
||||||
// provider payload.
|
// provider payload.
|
||||||
const provider = {
|
const provider = {
|
||||||
|
projects,
|
||||||
|
|
||||||
projectsViews,
|
projectsViews,
|
||||||
|
|
||||||
|
isProjectsLoading,
|
||||||
|
isProjectsFetching,
|
||||||
|
isViewsLoading,
|
||||||
|
|
||||||
|
isEmptyStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardInsider
|
<DashboardInsider loading={isViewsLoading} name={'projects'}>
|
||||||
// loading={isViewsLoading}
|
|
||||||
name={'projects'}
|
|
||||||
>
|
|
||||||
<ProjectsListContext.Provider value={provider} {...props} />
|
<ProjectsListContext.Provider value={provider} {...props} />
|
||||||
</DashboardInsider>
|
</DashboardInsider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
//@ts-nocheck
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
|
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
|
||||||
|
|
||||||
import { DashboardViewsTabs } from 'components';
|
import { DashboardViewsTabs } from '@/components';
|
||||||
|
|
||||||
import withProjects from './withProjects';
|
import withProjects from './withProjects';
|
||||||
import withProjectsActions from './withProjectsActions';
|
import withProjectsActions from './withProjectsActions';
|
||||||
import { useProjectsListContext } from './ProjectsListProvider';
|
import { useProjectsListContext } from './ProjectsListProvider';
|
||||||
|
|
||||||
import { compose, transfromViewsToTabs } from 'utils';
|
import { compose, transfromViewsToTabs } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Projects views tabs.
|
* Projects views tabs.
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
Intent,
|
Intent,
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { Icon, FormatDate, Choose, FormattedMessage as T } from 'components';
|
import { Icon, FormatDate, Choose, FormattedMessage as T } from '@/components';
|
||||||
import { safeCallback, firstLettersArgs, calculateStatus } from 'utils';
|
import { safeCallback, firstLettersArgs, calculateStatus } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* project status.
|
* project status.
|
||||||
@@ -58,7 +58,7 @@ export const StatusAccessor = (project) => {
|
|||||||
*/
|
*/
|
||||||
export const AvatarCell = ({ row: { original }, size }) => (
|
export const AvatarCell = ({ row: { original }, size }) => (
|
||||||
<span className="avatar" data-size={size}>
|
<span className="avatar" data-size={size}>
|
||||||
{firstLettersArgs(original?.display_name, original?.name)}
|
{firstLettersArgs(original?.contact_display_name, original?.name)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -102,13 +102,15 @@ export const ActionsMenu = ({
|
|||||||
export const ProjectsAccessor = (row) => (
|
export const ProjectsAccessor = (row) => (
|
||||||
<ProjectItemsWrap>
|
<ProjectItemsWrap>
|
||||||
<ProjectItemsHeader>
|
<ProjectItemsHeader>
|
||||||
<ProjectItemContactName>{row.display_name}</ProjectItemContactName>
|
<ProjectItemContactName>
|
||||||
|
{row.contact_display_name}
|
||||||
|
</ProjectItemContactName>
|
||||||
<ProjectItemProjectName>{row.name}</ProjectItemProjectName>
|
<ProjectItemProjectName>{row.name}</ProjectItemProjectName>
|
||||||
</ProjectItemsHeader>
|
</ProjectItemsHeader>
|
||||||
<ProjectItemDescription>
|
<ProjectItemDescription>
|
||||||
<FormatDate value={row.deadline} />
|
<FormatDate value={row.deadline_formatted} />
|
||||||
{intl.get('projects.label.cost_estimate', {
|
{intl.get('projects.label.cost_estimate', {
|
||||||
value: row.cost_estimate,
|
value: row.cost_estimate_formatted,
|
||||||
})}
|
})}
|
||||||
</ProjectItemDescription>
|
</ProjectItemDescription>
|
||||||
</ProjectItemsWrap>
|
</ProjectItemsWrap>
|
||||||
@@ -175,10 +177,10 @@ const ProjectItemDescription = styled.div`
|
|||||||
const ProjectStatusRoot = styled.div`
|
const ProjectStatusRoot = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* justify-content: flex-end; */
|
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ProjectStatusTaskAmount = styled.div`
|
const ProjectStatusTaskAmount = styled.div`
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -198,6 +200,7 @@ const ProjectProgressBar = styled(ProgressBar)`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StatusTag = styled(Tag)`
|
const StatusTag = styled(Tag)`
|
||||||
min-width: 65px;
|
min-width: 65px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
|||||||
import {
|
import {
|
||||||
getProjectsTableStateFactory,
|
getProjectsTableStateFactory,
|
||||||
isProjectsTableStateChangedFactory,
|
isProjectsTableStateChangedFactory,
|
||||||
} from '../../../../store/Project/projects.selectors';
|
} from '@/store/Project/projects.selectors';
|
||||||
|
|
||||||
export default (mapState) => {
|
export default (mapState) => {
|
||||||
const getProjectsTableState = getProjectsTableStateFactory();
|
const getProjectsTableState = getProjectsTableStateFactory();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||||||
import {
|
import {
|
||||||
setProjectsTableState,
|
setProjectsTableState,
|
||||||
resetProjectsTableState,
|
resetProjectsTableState,
|
||||||
} from '../../../../store/Project/projects.actions';
|
} from '@/store/Project/projects.actions';
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setProjectsTableState: (state) => dispatch(setProjectsTableState(state)),
|
setProjectsTableState: (state) => dispatch(setProjectsTableState(state)),
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Form } from 'formik';
|
|
||||||
import TaskFormFields from './TaskFormFields';
|
|
||||||
import TaskFormFloatingActions from './TaskFormFloatingActions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task form content.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function TaskFormContent() {
|
|
||||||
return (
|
|
||||||
<Form>
|
|
||||||
<TaskFormFields />
|
|
||||||
<TaskFormFloatingActions />
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { TaskFormProvider } from './TaskFormProvider';
|
|
||||||
import TaskForm from './TaskForm';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task form dialog content.
|
|
||||||
*/
|
|
||||||
export default function TaskFormDialogContent({
|
|
||||||
// #ownProps
|
|
||||||
dialogName,
|
|
||||||
task,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<TaskFormProvider taskId={task} dialogName={dialogName}>
|
|
||||||
<TaskForm />
|
|
||||||
</TaskFormProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
//@ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { DialogContent } from 'components';
|
|
||||||
|
|
||||||
const TaskFormContext = React.createContext();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task form provider.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function TaskFormProvider({
|
|
||||||
// #ownProps
|
|
||||||
dialogName,
|
|
||||||
taskId,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
// State provider.
|
|
||||||
const provider = {
|
|
||||||
dialogName,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogContent>
|
|
||||||
<TaskFormContext.Provider value={provider} {...props} />
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const useTaskFormContext = () => React.useContext(TaskFormContext);
|
|
||||||
|
|
||||||
export { TaskFormProvider, useTaskFormContext };
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
|
|
||||||
import withDialogRedux from 'components/DialogReduxConnect';
|
|
||||||
import { compose } from 'utils';
|
|
||||||
|
|
||||||
const TaskFormDialogContent = React.lazy(
|
|
||||||
() => import('./TaskFormDialogContent'),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task form dialog.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function TaskFormDialog({ dialogName, payload: { taskId = null }, isOpen }) {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
name={dialogName}
|
|
||||||
title={intl.get('task.dialog.new_task')}
|
|
||||||
isOpen={isOpen}
|
|
||||||
autoFocus={true}
|
|
||||||
canEscapeKeyClose={true}
|
|
||||||
style={{ width: '500px' }}
|
|
||||||
>
|
|
||||||
<DialogSuspense>
|
|
||||||
<TaskFormDialogContent dialogName={dialogName} task={taskId} />
|
|
||||||
</DialogSuspense>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default compose(withDialogRedux())(TaskFormDialog);
|
|
||||||
|
|
||||||
const TaskFormDialogRoot = styled(Dialog)``;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import * as Yup from 'yup';
|
|
||||||
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(),
|
|
||||||
projectId: Yup.string()
|
|
||||||
.label(intl.get('time_entry.schema.label.project_name'))
|
|
||||||
.required(),
|
|
||||||
taskId: Yup.string()
|
|
||||||
.label(intl.get('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'))
|
|
||||||
.required(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const CreateTimeEntryFormSchema = Schema;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Form } from 'formik';
|
|
||||||
import TimeEntryFormFields from './TimeEntryFormFields';
|
|
||||||
import TimeEntryFormFloatingActions from './TimeEntryFormFloatingActions';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time entry form content.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function TimeEntryFormContent() {
|
|
||||||
return (
|
|
||||||
<Form>
|
|
||||||
<TimeEntryFormFields />
|
|
||||||
<TimeEntryFormFloatingActions />
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { TimeEntryFormProvider } from './TimeEntryFormProvider';
|
|
||||||
import TimeEntryForm from './TimeEntryForm';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time entry form dialog content.
|
|
||||||
* @returns {ReactNode}
|
|
||||||
*/
|
|
||||||
export default function TimeEntryFormDialogContent({
|
|
||||||
// #ownProps
|
|
||||||
dialogName,
|
|
||||||
project,
|
|
||||||
timeEntry,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<TimeEntryFormProvider
|
|
||||||
projectId={project}
|
|
||||||
timeEntryId={timeEntry}
|
|
||||||
dialogName={dialogName}
|
|
||||||
>
|
|
||||||
<TimeEntryForm />
|
|
||||||
</TimeEntryFormProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
//@ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { DialogContent } from 'components';
|
|
||||||
|
|
||||||
const TimeEntryFormContext = React.createContext();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time entry form provider.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function TimeEntryFormProvider({
|
|
||||||
// #ownProps
|
|
||||||
dialogName,
|
|
||||||
projectId,
|
|
||||||
timeEntryId,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const provider = {
|
|
||||||
dialogName,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogContent>
|
|
||||||
<TimeEntryFormContext.Provider value={provider} {...props} />
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const useTimeEntryFormContext = () => React.useContext(TimeEntryFormContext);
|
|
||||||
|
|
||||||
export { TimeEntryFormProvider, useTimeEntryFormContext };
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import { MenuItem, Button } from '@blueprintjs/core';
|
|
||||||
import { FSelect } from '../../../../../components/Forms';
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} query
|
|
||||||
* @param {*} project
|
|
||||||
* @param {*} _index
|
|
||||||
* @param {*} exactMatch
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const projectItemPredicate = (query, project, _index, exactMatch) => {
|
|
||||||
const normalizedTitle = project.name.toLowerCase();
|
|
||||||
const normalizedQuery = query.toLowerCase();
|
|
||||||
|
|
||||||
if (exactMatch) {
|
|
||||||
return normalizedTitle === normalizedQuery;
|
|
||||||
} else {
|
|
||||||
return `${project.name}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {*} project
|
|
||||||
* @param {*} param1
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const projectItemRenderer = (project, { handleClick, modifiers, query }) => {
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
active={modifiers.active}
|
|
||||||
disabled={modifiers.disabled}
|
|
||||||
key={project.id}
|
|
||||||
onClick={handleClick}
|
|
||||||
text={project.name}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const projectSelectProps = {
|
|
||||||
// itemPredicate: projectItemPredicate,
|
|
||||||
itemRenderer: projectItemRenderer,
|
|
||||||
valueAccessor: 'id',
|
|
||||||
labelAccessor: 'name',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ProjectSelect({ projects, ...rest }) {
|
|
||||||
return (
|
|
||||||
<FSelect
|
|
||||||
items={projects}
|
|
||||||
{...projectSelectProps}
|
|
||||||
{...rest}
|
|
||||||
input={ProjectSelectButton}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProjectSelectButton({ label }) {
|
|
||||||
return <Button text={label ? label : intl.get('find_or_choose_a_project')} />;
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './ProjectSelect';
|
|
||||||
export * from './TaskSelect';
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
|
export const taskChargeOptions = [
|
||||||
|
{ name: intl.get('task.dialog.hourly_rate'), value: 'hourly_rate' },
|
||||||
|
{ name: intl.get('task.dialog.fixed_price'), value: 'fixed_price' },
|
||||||
|
{ name: intl.get('task.dialog.non_chargeable'), value: 'non_chargeable' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const expenseChargeOption = [
|
||||||
|
{
|
||||||
|
name: intl.get('expenses.dialog.markup'),
|
||||||
|
value: 'markup',
|
||||||
|
},
|
||||||
|
{ name: intl.get('expenses.dialog.pass_cost_on'), value: 'pass_cost_on' },
|
||||||
|
{ name: intl.get('expenses.dialog.custom_pirce'), value: 'custom_pirce' },
|
||||||
|
{ name: intl.get('expenses.dialog.non_chargeable'), value: 'non_chargeable' },
|
||||||
|
];
|
||||||
124
src/containers/Projects/hooks/index.ts
Normal file
124
src/containers/Projects/hooks/index.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { useQueryClient, useMutation } from 'react-query';
|
||||||
|
import { useRequestQuery } from '@/hooks/useQueryRequest';
|
||||||
|
import { transformPagination } from '@/utils';
|
||||||
|
import useApiRequest from '@/hooks/useRequest';
|
||||||
|
import t from './type';
|
||||||
|
|
||||||
|
// Common invalidate queries.
|
||||||
|
const commonInvalidateQueries = (queryClient) => {
|
||||||
|
// Invalidate projects.
|
||||||
|
queryClient.invalidateQueries(t.PROJECT);
|
||||||
|
queryClient.invalidateQueries(t.PROJECTS);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new project
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
export function useCreateProject(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation((values) => apiRequest.post('projects', values), {
|
||||||
|
onSuccess: (res, values) => {
|
||||||
|
// Common invalidate queries.
|
||||||
|
commonInvalidateQueries(queryClient);
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit the given project
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useEditProject(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
([id, values]) => apiRequest.post(`/projects/${id}`, values),
|
||||||
|
{
|
||||||
|
onSuccess: (res, [id, values]) => {
|
||||||
|
// Invalidate specific project.
|
||||||
|
queryClient.invalidateQueries([t.PROJECT, id]);
|
||||||
|
|
||||||
|
commonInvalidateQueries(queryClient);
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the given project
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
export function useDeleteProject(props) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation((id) => apiRequest.delete(`projects/${id}`), {
|
||||||
|
onSuccess: (res, id) => {
|
||||||
|
// Invalidate specific project.
|
||||||
|
queryClient.invalidateQueries([t.PROJECT, id]);
|
||||||
|
|
||||||
|
// Common invalidate queries.
|
||||||
|
commonInvalidateQueries(queryClient);
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the projects details.
|
||||||
|
* @param projectId The project id
|
||||||
|
* @param props
|
||||||
|
* @param requestProps
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useProject(projectId, props, requestProps) {
|
||||||
|
return useRequestQuery(
|
||||||
|
[t.PROJECT, projectId],
|
||||||
|
{ method: 'get', url: `projects/${projectId}`, ...requestProps },
|
||||||
|
{
|
||||||
|
select: (res) => res.data.project,
|
||||||
|
defaultData: {},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformProjects = (res) => ({
|
||||||
|
projects: res.data.projects,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve projects list with pagination meta.
|
||||||
|
* @param query
|
||||||
|
* @param props
|
||||||
|
*/
|
||||||
|
export function useProjects(query, props) {
|
||||||
|
return useRequestQuery(
|
||||||
|
[t.PROJECTS, query],
|
||||||
|
{ method: 'get', url: 'projects', params: query },
|
||||||
|
{
|
||||||
|
select: transformProjects,
|
||||||
|
defaultData: {
|
||||||
|
projects: [],
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRefreshInvoices() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return {
|
||||||
|
refresh: () => {
|
||||||
|
queryClient.invalidateQueries(t.PROJECTS);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
11
src/containers/Projects/hooks/type.ts
Normal file
11
src/containers/Projects/hooks/type.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const CUSTOMERS = {
|
||||||
|
CUSTOMERS: 'CUSTOMERS',
|
||||||
|
CUSTOMER: 'CUSTOMER',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROJECTS = {
|
||||||
|
PROJECT: 'PROJECT',
|
||||||
|
PROJECTS: 'PROJECTS',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { ...PROJECTS, ...CUSTOMERS };
|
||||||
@@ -1717,7 +1717,6 @@
|
|||||||
"bill.convert_to_credit_note": "Convert to Vendor Credit",
|
"bill.convert_to_credit_note": "Convert to Vendor Credit",
|
||||||
"bill.allocate_landed_coast": "Allocate Landed Cost",
|
"bill.allocate_landed_coast": "Allocate Landed Cost",
|
||||||
"overdue": "Overdue",
|
"overdue": "Overdue",
|
||||||
"invite_user.label.role_name": "Role Name",
|
|
||||||
"permissions.column.view": "View",
|
"permissions.column.view": "View",
|
||||||
"permissions.column.full_access": "Full Access",
|
"permissions.column.full_access": "Full Access",
|
||||||
"permissions.column.delete": "Delete",
|
"permissions.column.delete": "Delete",
|
||||||
@@ -2058,20 +2057,32 @@
|
|||||||
"projects.dialog.cost_estimate": "Cost Estimate",
|
"projects.dialog.cost_estimate": "Cost Estimate",
|
||||||
"projects.label.create": "Create",
|
"projects.label.create": "Create",
|
||||||
"projects.label.cost_estimate": " • Estimate {value}",
|
"projects.label.cost_estimate": " • Estimate {value}",
|
||||||
"task.dialog.new_task": "New Task",
|
"projects.dialog.success_message": "The project has been created successfully.",
|
||||||
"task.dialog.task_name": "Task Name",
|
"projects.dialog.edit_success_message": "The project has been edited successfully.",
|
||||||
"task.dialog.estimated_hours": "Estimate Hours",
|
"projects.dialog.new_project": "New Project",
|
||||||
"task.dialog.charge": "Charge",
|
"projects.dialog.edit_project": "Edit Project",
|
||||||
"task.dialog.estimated_amount": "Estimated Amount",
|
"projects.alert.delete_message": "The deleted project has been deleted successfully.",
|
||||||
|
"projects.alert.once_delete_this_project": "Once you delete this project, you won't be able to restore it later. Are you sure you want to delete this project?",
|
||||||
|
"projects.empty_status.title":"",
|
||||||
|
"projects.empty_status.description":"",
|
||||||
|
"projects.empty_status.action":"New Project",
|
||||||
|
"project_task.dialog.new_task": "New Task",
|
||||||
|
"project_task.dialog.task_name": "Task Name",
|
||||||
|
"project_task.dialog.estimated_hours": "Estimate Hours",
|
||||||
|
"project_task.dialog.charge": "Charge",
|
||||||
|
"project_task.dialog.estimated_amount": "Estimated Amount",
|
||||||
|
"project_task.dialog.hourly_rate": "Hourly rate",
|
||||||
|
"project_task.dialog.fixed_price": "Fixed price",
|
||||||
|
"project_task.dialog.non_chargeable": "Non-chargeable",
|
||||||
"project.schema.label.contact": "Contact",
|
"project.schema.label.contact": "Contact",
|
||||||
"project.schema.label.project_name": "Project name",
|
"project.schema.label.project_name": "Project name",
|
||||||
"project.schema.label.deadline": "Deadline",
|
"project.schema.label.deadline": "Deadline",
|
||||||
"project.schema.label.project_state": "Project state",
|
"project.schema.label.project_state": "Project state",
|
||||||
"project.schema.label.project_cost": "Project cost",
|
"project.schema.label.project_cost": "Project cost",
|
||||||
"task.schema.label.task_name": "Task name",
|
"project_task.schema.label.task_name": "Task name",
|
||||||
"task.schema.label.task_house": "Task house",
|
"project_task.schema.label.task_house": "Task house",
|
||||||
"task.schema.label.charge": "Charge",
|
"project_task.schema.label.charge": "Charge",
|
||||||
"task.schema.label.amount": "Amount",
|
"project_task.schema.label.amount": "Amount",
|
||||||
"projcet_details.action.new_transaction": "New Transaction",
|
"projcet_details.action.new_transaction": "New Transaction",
|
||||||
"projcet_details.action.time_entry": "Time",
|
"projcet_details.action.time_entry": "Time",
|
||||||
"projcet_details.action.edit_project": "Edit Project",
|
"projcet_details.action.edit_project": "Edit Project",
|
||||||
@@ -2080,23 +2091,75 @@
|
|||||||
"project_details.label.purchases": "Purchases",
|
"project_details.label.purchases": "Purchases",
|
||||||
"project_details.label.sales": "Sales",
|
"project_details.label.sales": "Sales",
|
||||||
"project_details.label.journals": "Journals",
|
"project_details.label.journals": "Journals",
|
||||||
|
"project_details.new_expenses": "New Expenses",
|
||||||
|
"project_details.new_estimated_expenses": "New Estimated Expenses",
|
||||||
"timesheets.actions.delete_timesheet": "Delete",
|
"timesheets.actions.delete_timesheet": "Delete",
|
||||||
"timesheets.column.date": "Date",
|
"timesheets.column.date": "Date",
|
||||||
"timesheets.column.task": "Task",
|
"timesheets.column.task": "Task",
|
||||||
"timesheets.column.user": "User",
|
"timesheets.column.user": "User",
|
||||||
"timesheets.column.time": "Time",
|
"timesheets.column.time": "Time",
|
||||||
"timesheets.column.billing_status": "Billing Status",
|
"timesheets.column.billing_status": "Billing Status",
|
||||||
"time_entry.dialog.label": "New Time Entry",
|
"project_time_entry.dialog.label": "New Time Entry",
|
||||||
"time_entry.dialog.project": "Project",
|
"project_time_entry.dialog.project": "Project",
|
||||||
"time_entry.dialog.task": "Task",
|
"project_time_entry.dialog.task": "Task",
|
||||||
"time_entry.dialog.description": "Description",
|
"project_time_entry.dialog.description": "Description",
|
||||||
"time_entry.dialog.duration": "Duration",
|
"project_time_entry.dialog.duration": "Duration",
|
||||||
"time_entry.dialog.date": "Date",
|
"project_time_entry.dialog.date": "Date",
|
||||||
"time_entry.dialog.create": "Create",
|
"project_time_entry.dialog.create": "Create",
|
||||||
"time_entry.schema.label.project_name": "Project name",
|
"project_time_entry.schema.label.project_name": "Project name",
|
||||||
"time_entry.schema.label.task_name": "Task name",
|
"project_time_entry.schema.label.task_name": "Task name",
|
||||||
"time_entry.schema.label.duration": "Duration",
|
"project_time_entry.schema.label.duration": "Duration",
|
||||||
"time_entry.schema.label.date": "Date",
|
"project_time_entry.schema.label.date": "Date",
|
||||||
"find_or_choose_a_project": "Find or choose a project",
|
"find_or_choose_a_project": "Find or choose a project",
|
||||||
"choose_a_task": "Choose a task"
|
"choose_a_task": "Choose a task",
|
||||||
|
"project_expense.dialog.label": "New Expense",
|
||||||
|
"project_expense.dialog.expense_name": "Expense Name",
|
||||||
|
"project_expense.dialog.expense_date": "Date",
|
||||||
|
"project_expense.dialog.quantity": "Quantity",
|
||||||
|
"project_expense.dialog.charge": "Charge",
|
||||||
|
"project_expense.dialog.track_expense": "Track to Expense",
|
||||||
|
"project_expense.dialog.unit_price": "Unit Price",
|
||||||
|
"project_expense.dialog.expense_total": "Total",
|
||||||
|
"project_expense.dialog.percentage": "Percentage",
|
||||||
|
"project_expense.dialog.total": "Total:",
|
||||||
|
"project_expense.dialog.markup": "% Markup",
|
||||||
|
"project_expense.dialog.pass_cost_on": "Pass cost on",
|
||||||
|
"project_expense.dialog.custom_pirce": "Custom Pirce",
|
||||||
|
"project_expense.dialog.non_chargeable": "Non-chargeable",
|
||||||
|
"project_expense.dialog.cost_to_you": "Cost to you",
|
||||||
|
"project_expense.dialog.what_you_ll_charge": "What you'll charge",
|
||||||
|
"project_expense.schema.label.expense_name": "Expense name",
|
||||||
|
"project_expense.schema.label.estimated_expense": "Estimated expense",
|
||||||
|
"project_expense.schema.label.quantity": "Quantity",
|
||||||
|
"project_expense.schema.label.unit_price": "Unit price",
|
||||||
|
"choose_an_estimated_expense": "Choose an estimated expense",
|
||||||
|
"estimated_expenses.dialog.label": "New Estimated Expense",
|
||||||
|
"estimated_expenses.dialog.estimated_expense": "Estimated Expense Name",
|
||||||
|
"estimated_expenses.dialog.quantity": "Quantity",
|
||||||
|
"estimated_expenses.dialog.unit_price": "Unit Price",
|
||||||
|
"estimated_expenses.dialog.total": "Total",
|
||||||
|
"estimated_expenses.dialog.charge": "Charge",
|
||||||
|
"estimated_expenses.dialog.percentage": "Percentage",
|
||||||
|
"estimated_expenses.dialog.estimated_amount": "Estimated Amount:",
|
||||||
|
"estimated_expenses.dialog.cost_to_you": "Cost to you",
|
||||||
|
"estimated_expenses.dialog.what_you_ll_charge": "What you'll charge",
|
||||||
|
"estimated_expense.schema.label.estimated_expense": "Estimated expense name",
|
||||||
|
"estimated_expense.schema.label.quantity": "Quantity",
|
||||||
|
"estimated_expense.schema.label.unit_price": "Unit price",
|
||||||
|
"purchases.column.date": "Date",
|
||||||
|
"purchases.column.type": "Type",
|
||||||
|
"purchases.column.transaction_no": "Transaction No",
|
||||||
|
"purchases.column.due_date": "Due Date",
|
||||||
|
"purchases.column.balance": "Balance",
|
||||||
|
"purchases.column.total": "Total",
|
||||||
|
"purchases.column.status": "Status",
|
||||||
|
"purchases.action.delete": "Delete",
|
||||||
|
"sales.column.date": "Date",
|
||||||
|
"sales.column.type": "Type",
|
||||||
|
"sales.column.transaction_no": "Transaction No",
|
||||||
|
"sales.column.due_date": "Due Date",
|
||||||
|
"sales.column.balance": "Balance",
|
||||||
|
"sales.column.total": "Total",
|
||||||
|
"sales.column.status": "Status",
|
||||||
|
"sales.action.delete": "Delete"
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user