Compare commits

...

39 Commits

Author SHA1 Message Date
elforjani13
754618aa7a fix edit project. 2022-07-15 20:56:52 +02:00
elforjani13
50c905eabb feat: add empty status & fix edit project. 2022-07-15 20:46:04 +02:00
elforjani13
709e06a646 feat: add project api 2022-07-14 18:29:22 +02:00
elforjani13
8826d2bc5b fix: notes. 2022-07-10 17:41:50 +02:00
elforjani13
38a961b899 fix: additional notes. 2022-07-06 13:44:00 +02:00
elforjani13
7ef7e126e5 fix: rename time entry form to project time entry form. 2022-07-06 13:42:15 +02:00
elforjani13
bcf0ec25b8 fix: rename task form to project task form. 2022-07-06 13:41:17 +02:00
elforjani13
965a8966f6 fix: rename expense form to project expense form. 2022-07-06 13:39:33 +02:00
elforjani13
b030d6ea37 fix: add estimated & expense dialog. 2022-07-04 11:14:24 +02:00
elforjani13
31fef21362 feat: add purchases & sales tables. 2022-06-30 22:05:35 +02:00
elforjani13
6f2a456a56 feat: add expense form. 2022-06-30 22:03:50 +02:00
elforjani13
6134ad5598 feat: add estimated expense. 2022-06-30 21:59:12 +02:00
elforjani13
cd08d0ee16 feat: add transaction select. 2022-06-23 21:51:34 +02:00
elforjani13
f268b8a95a feat: project status. 2022-06-23 19:41:09 +02:00
elforjani13
6a06950654 feat: project status. 2022-06-23 19:37:29 +02:00
elforjani13
d9de3341fe feat: add timesheet header. 2022-06-23 17:50:14 +02:00
elforjani13
6b6081e32e feat: add project & timesheet table. 2022-06-23 00:15:47 +02:00
elforjani13
7be568b8ac feat: time entry dialog. 2022-06-23 00:14:49 +02:00
elforjani13
50522af72d fix: task form dialog. 2022-06-23 00:13:46 +02:00
elforjani13
0b454d6d4d feat: project table. 2022-06-23 00:12:17 +02:00
elforjani13
4ba64cc4ff feat: add project timesheet. 2022-06-23 00:10:06 +02:00
elforjani13
5128c021b0 fix: project form. 2022-06-23 00:07:21 +02:00
elforjani13
5a8fcc8fb5 feat: add time entry form. 2022-06-15 16:09:21 +02:00
elforjani13
9cf1b993dd feat: add time entry form. 2022-06-15 16:08:50 +02:00
elforjani13
f443a1b106 fix: project detail tabs. 2022-06-15 11:38:20 +02:00
elforjani13
0eb0aee1ef feat: project detail tabs. 2022-06-14 17:19:59 +02:00
elforjani13
4b992c4bb4 fix: project details. 2022-06-13 17:55:52 +02:00
elforjani13
051681e6f3 feat: add timesheet & project details. 2022-06-13 17:33:54 +02:00
Ahmed Bouhuolia
629c790430 Merge pull request #57 from bigcapitalhq/BIG-379-create-a-project
`BIG-379` Add project & task dialog & projects list.
2022-06-12 13:07:29 +02:00
elforjani13
bdadc5d795 fix: remove the inner container. 2022-06-12 12:55:37 +02:00
elforjani13
23bb9c4cc3 fix: rename project & task form dialog. 2022-06-12 12:43:03 +02:00
elforjani13
8136378725 fix: project & task dialog. 2022-06-12 11:56:06 +02:00
elforjani13
4eac2239b1 feat: add projects view tabs. 2022-06-12 09:43:19 +02:00
elforjani13
a44f548ff9 feat: projects actions bar. 2022-06-11 15:30:11 +02:00
elforjani13
327916da4b fix: add FDateInput 2022-06-11 13:58:04 +02:00
elforjani13
bee7896279 fix: project form. 2022-06-11 13:29:33 +02:00
elforjani13
cb0a315ca6 feat: add task form dialog. 2022-06-11 00:45:31 +02:00
elforjani13
d2c907541a feat: add project form dialog. 2022-06-11 00:38:00 +02:00
elforjani13
928d4d3f00 feat: add projects list. 2022-06-11 00:36:18 +02:00
104 changed files with 4589 additions and 7 deletions

View File

@@ -16,7 +16,11 @@ export const TABLES = {
CASHFLOW_Transactions: 'cashflow_transactions',
CREDIT_NOTES: 'credit_notes',
VENDOR_CREDITS: 'vendor_credits',
WAREHOUSE_TRANSFERS:'warehouse_transfers'
WAREHOUSE_TRANSFERS: 'warehouse_transfers',
PROJECTS: 'projects',
TIMESHEETS: 'timesheets',
PURCHASES: 'purchases',
SALES: 'sales',
};
export const TABLE_SIZE = {

View File

@@ -197,6 +197,7 @@ export default function DataTable(props) {
DataTable.defaultProps = {
pagination: false,
hidePaginationNoPages: true,
hideTableHeader: false,
size: null,
spinnerProps: { size: 30 },

View File

@@ -80,12 +80,23 @@ function TableHeaderGroup({ headerGroup }) {
export default function TableHeader() {
const {
table: { headerGroups, page },
props: { TableHeaderSkeletonRenderer, headerLoading, progressBarLoading },
props: {
TableHeaderSkeletonRenderer,
headerLoading,
progressBarLoading,
hideTableHeader,
},
} = useContext(TableContext);
// Can't contiunue if the thead is disabled.
if (hideTableHeader) {
return null;
}
if (headerLoading && TableHeaderSkeletonRenderer) {
return <TableHeaderSkeletonRenderer />;
}
return (
<ScrollSyncPane>
<div className="thead">

View File

@@ -40,6 +40,11 @@ import BranchActivateDialog from '../containers/Dialogs/BranchActivateDialog';
import WarehouseActivateDialog from '../containers/Dialogs/WarehouseActivateDialog';
import CustomerOpeningBalanceDialog from '../containers/Dialogs/CustomerOpeningBalanceDialog';
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';
/**
* Dialogs container.
@@ -90,6 +95,11 @@ export default function DialogsContainer() {
<WarehouseActivateDialog dialogName={'warehouse-activate'} />
<CustomerOpeningBalanceDialog dialogName={'customer-opening-balance'} />
<VendorOpeningBalanceDialog dialogName={'vendor-opening-balance'} />
<ProjectFormDialog dialogName={'project-form'} />
<ProjectTaskFormDialog dialogName={'project-task-form'} />
<ProjectTimeEntryFormDialog dialogName={'project-time-entry-form'} />
<ProjectExpenseForm dialogName={'project-expense-form'} />
<EstimatedExpenseFormDialog dialogName={'estimated-expense-form'} />
</div>
);
}

View File

@@ -9,6 +9,7 @@ import {
TextArea,
} from '@blueprintjs-formik/core';
import { Select, MultiSelect } from '@blueprintjs-formik/select';
import { DateInput } from '@blueprintjs-formik/datetime';
export {
FormGroup as FFormGroup,
@@ -21,4 +22,5 @@ export {
MultiSelect as FMultiSelect,
EditableText as FEditableText,
TextArea as FTextArea,
DateInput as FDateInput,
};

View File

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

View File

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

View File

@@ -69,6 +69,7 @@ export enum ISidebarMenuOverlayIds {
Contacts = 'Contacts',
Cashflow = 'Cashflow',
Expenses = 'Expenses',
Projects = 'Projects',
}
export enum ISidebarSubscriptionAbility {

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from 'components';
/**
*
* @param {*} query
* @param {*} project
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const projectsItemPredicate = (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 projectsItemRenderer = (project, { handleClick, modifiers, query }) => {
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
key={project.id}
onClick={handleClick}
text={project.name}
/>
);
};
const projectSelectProps = {
itemPredicate: projectsItemPredicate,
itemRenderer: projectsItemRenderer,
valueAccessor: 'id',
labelAccessor: 'name',
};
export function ProjectsSelect({ projects, ...rest }) {
return (
<FSelect
items={projects}
{...projectSelectProps}
{...rest}
input={ProjectSelectButton}
/>
);
}
function ProjectSelectButton({ label }) {
return <Button text={label ? label : intl.get('find_or_choose_a_project')} />;
}

View File

@@ -0,0 +1,64 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from 'components';
/**
*
* @param {*} query
* @param {*} task
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const taskItemPredicate = (query, task, _index, exactMatch) => {
const normalizedTitle = task.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${task.name}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param {*} task
* @param {*} param1
* @returns
*/
const taskItemRenderer = (task, { handleClick, modifiers, query }) => {
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
key={task.id}
onClick={handleClick}
text={task.name}
/>
);
};
const taskSelectProps = {
itemPredicate: taskItemPredicate,
itemRenderer: taskItemRenderer,
valueAccessor: 'id',
labelAccessor: 'name',
};
export function TaskSelect({ tasks, ...rest }) {
return (
<FSelect
items={tasks}
{...taskSelectProps}
{...rest}
input={TaskSelectButton}
/>
);
}
function TaskSelectButton({ label }) {
return <Button text={label ? label : intl.get('choose_a_task')} />;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,131 @@
// @ts-nocheck
import React from 'react';
import { useHistory } from 'react-router-dom';
import {
Button,
Classes,
NavbarDivider,
NavbarGroup,
Alignment,
} from '@blueprintjs/core';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import {
Icon,
FormattedMessage as T,
DashboardRowsHeightButton,
} from 'components';
import { ProjectTransactionsSelect } from './components';
import withSettings from '../../../Settings/withSettings';
import withSettingsActions from '../../../Settings/withSettingsActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { projectTranslations } from './common';
import { useProjectDetailContext } from './ProjectDetailProvider';
import { compose } from 'utils';
/**
* Project detail actions bar.
* @returns
*/
function ProjectDetailActionsBar({
// #withDialogActions
openDialog,
// #withSettings
timesheetsTableSize,
// #withSettingsActions
addSetting,
}) {
const history = useHistory();
const { projectId } = useProjectDetailContext();
// Handle new transaction button click.
const handleNewTransactionBtnClick = ({ path }) => {
switch (path) {
case 'expense':
openDialog('project-expense-form', { projectId });
break;
case 'estimated_expense':
openDialog('estimated-expense-form', { projectId });
}
};
const handleEditProjectBtnClick = () => {
openDialog('project-form', {
projectId,
});
};
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
addSetting('timesheets', 'tableSize', size) &&
addSetting('sales', 'tableSize', size) &&
addSetting('purchases', 'tableSize', size);
};
const handleTimeEntryBtnClick = () => {
openDialog('project-time-entry-form', {
projectId,
});
};
// Handle the refresh button click.
const handleRefreshBtnClick = () => {};
return (
<DashboardActionsBar>
<NavbarGroup>
<ProjectTransactionsSelect
transactions={projectTranslations}
onItemSelect={handleNewTransactionBtnClick}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'time-24'} iconSize={16} />}
text={<T id={'projcet_details.action.time_entry'} />}
onClick={handleTimeEntryBtnClick}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="pen-18" />}
text={<T id={'projcet_details.action.edit_project'} />}
onClick={handleEditProjectBtnClick}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
/>
<NavbarDivider />
<DashboardRowsHeightButton
initialValue={timesheetsTableSize}
onChange={handleTableRowSizeChange}
/>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
onClick={handleRefreshBtnClick}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withDialogActions,
withSettingsActions,
withSettings(({ timesheetsSettings }) => ({
timesheetsTableSize: timesheetsSettings?.tableSize,
})),
)(ProjectDetailActionsBar);

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
import React from 'react';
import DashboardInsider from '../../../../components/Dashboard/DashboardInsider';
const ProjectDetailContext = React.createContext();
/**
* Project detail provider.
* @returns
*/
function ProjectDetailProvider({
projectId,
// #ownProps
...props
}) {
// State provider.
const provider = {
projectId,
};
return (
<DashboardInsider class="timesheets">
<ProjectDetailContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useProjectDetailContext = () => React.useContext(ProjectDetailContext);
export { ProjectDetailProvider, useProjectDetailContext };

View File

@@ -0,0 +1,79 @@
import React from 'react';
import styled from 'styled-components';
import intl from 'react-intl-universal';
import { Tabs, Tab } from '@blueprintjs/core';
import ProjectTimeSheets from './ProjectTimeSheets';
import ProjectPurchasesTable from './ProjectPurchasesTable';
import ProjectSalesTable from './ProjectSalesTable';
/**
* Project detail tabs.
* @returns
*/
export default function ProjectDetailTabs() {
return (
<ProjectTabsContent>
<Tabs
animate={true}
large={true}
renderActiveTabPanelOnly={true}
defaultSelectedTabId={'purchases'}
>
<Tab id="overview" title={intl.get('project_details.label.overview')} />
<Tab
id="timesheet"
title={intl.get('project_details.label.timesheet')}
panel={<ProjectTimeSheets />}
/>
<Tab
id="purchases"
title={intl.get('project_details.label.purchases')}
panel={<ProjectPurchasesTable />}
/>
<Tab
id="sales"
title={intl.get('project_details.label.sales')}
panel={<ProjectSalesTable />}
/>
<Tab id="journals" title={intl.get('project_details.label.journals')} />
</Tabs>
</ProjectTabsContent>
);
}
const ProjectTabsContent = styled.div`
.bp3-tabs {
.bp3-tab-list {
padding: 0 20px;
background-color: #fff;
border-bottom: 1px solid #d2dce2;
> *:not(:last-child) {
margin-right: 0;
}
&.bp3-large > .bp3-tab {
font-size: 15px;
font-weight: 400;
color: #7f8596;
margin: 0 0.9rem;
&[aria-selected='true'],
&:not([aria-disabled='true']):hover {
color: #0052cc;
}
}
.bp3-tab-indicator-wrapper .bp3-tab-indicator {
height: 2px;
bottom: -2px;
}
}
.bp3-tab-panel {
/* margin: 20px 32px; */
/* margin: 20px; */
/* margin-top: 20px;
margin-bottom: 20px;
padding: 0 25px; */
}
}
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
//@ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Intent } from '@blueprintjs/core';
import { FormatDate } from 'components';
import {
DetailFinancialCard,
DetailFinancialSection,
FinancialProgressBar,
FinancialCardText,
} from '../components';
import { calculateStatus } from 'utils';
/**
* Project Timesheets header
* @returns
*/
export function ProjectTimesheetsHeader() {
return (
<DetailFinancialSection>
<DetailFinancialCard label={'Project estimate'} value={'3.14'} />
<DetailFinancialCard label={'Invoiced'} value={'0.00'}>
<FinancialCardText>0% of project estimate</FinancialCardText>
<FinancialProgressBar intent={Intent.NONE} value={0} />
</DetailFinancialCard>
<DetailFinancialCard label={'Time & Expenses'} value={'0.00'}>
<FinancialCardText>0% of project estimate</FinancialCardText>
<FinancialProgressBar intent={Intent.NONE} value={0} />
</DetailFinancialCard>
<DetailFinancialCard label={'To be invoiced'} value={'3.14'} />
<DetailFinancialCard
label={'Deadline'}
value={<FormatDate value={'2022-06-08T22:00:00.000Z'} />}
>
<FinancialCardText>4 days to go</FinancialCardText>
</DetailFinancialCard>
</DetailFinancialSection>
);
}

View File

@@ -0,0 +1,104 @@
// @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 { ActionsMenu } from './components';
import { useProjectTimesheetColumns } from './hooks';
import { TABLES } from 'common/tables';
import { useMemorizedColumnsWidths } from 'hooks';
import withSettings from '../../../../Settings/withSettings';
import { compose } from 'utils';
/**
* 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;
}
}
`;

View File

@@ -0,0 +1,78 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { FormatDate, Icon } from 'components';
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
import { safeCallback, firstLettersArgs } from 'utils';
/**
* Table actions cell.
*/
export function ActionsMenu({
payload: { onDelete, onViewDetails },
row: { original },
}) {
return (
<Menu>
<MenuItem
text={intl.get('timesheets.actions.delete_timesheet')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Avatar cell.
*/
export const AvatarCell = ({ row: { original }, size }) => (
<span className="avatar" data-size={size}>
{firstLettersArgs(original?.display_name, original?.name)}
</span>
);
/**
* Timesheet accessor.
*/
export const TimesheetAccessor = (timesheet) => (
<React.Fragment>
<TimesheetHeader>
<TimesheetTitle>{timesheet.display_name}</TimesheetTitle>
<TimesheetSubTitle>{timesheet.name}</TimesheetSubTitle>
</TimesheetHeader>
<TimesheetContent>
<FormatDate value={timesheet.date} />
<TimesheetDescription>{timesheet.description}</TimesheetDescription>
</TimesheetContent>
</React.Fragment>
);
const TimesheetHeader = styled.div`
display: flex;
align-items: baseline;
flex-flow: wrap;
`;
const TimesheetTitle = styled.span`
font-weight: 500;
margin-right: 12px;
line-height: 1.5rem;
`;
const TimesheetSubTitle = styled.span``;
const TimesheetContent = styled.div`
display: block;
white-space: nowrap;
font-size: 13px;
opacity: 0.75;
margin-bottom: 0.1rem;
line-height: 1.2rem;
`;
const TimesheetDescription = styled.span`
&::before {
content: '•';
margin: 0.3rem;
}
`;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,80 @@
import React from 'react';
import styled from 'styled-components';
import { ProgressBar } from '@blueprintjs/core';
export function DetailFinancialSection({ children }) {
return <FinancialSectionWrap>{children}</FinancialSectionWrap>;
}
export function DetailFinancialCard({ label, value, children }) {
return (
<React.Fragment>
<FinancialSectionCard>
<FinancialSectionCardContent>
<FinancialCardTitle>{label}</FinancialCardTitle>
<FinancialCardValue>{value}</FinancialCardValue>
{children}
</FinancialSectionCardContent>
</FinancialSectionCard>
</React.Fragment>
);
}
export const FinancialDescription = ({ childern }) => {
return <FinancialCardText>{childern}</FinancialCardText>;
};
export const FinancialProgressBar = ({ ...rest }) => {
return <FinancialCardProgressBar animate={false} stripes={false} {...rest} />;
};
const FinancialSectionWrap = styled.div`
display: flex;
margin: 22px 32px;
gap: 10px;
`;
const FinancialSectionCard = styled.div`
display: flex;
flex-direction: column;
flex-shrink: 1;
border-radius: 3px;
width: 230px;
height: 116px;
background-color: #fff;
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
`;
const FinancialSectionCardContent = styled.div`
margin: 16px;
`;
const FinancialCardWrap = styled.div``;
const FinancialCardTitle = styled.div`
font-size: 15px;
color: #000;
white-space: nowrap;
font-weight: 400;
line-height: 1.5rem;
`;
const FinancialCardValue = styled.div`
font-size: 21px;
line-height: 2rem;
font-weight: 700;
`;
const FinancialCardStatus = styled.div``;
export const FinancialCardText = styled.div`
font-size: 13px;
line-height: 1.5rem;
`;
export const FinancialCardProgressBar = styled(ProgressBar)`
&.bp3-progress-bar {
height: 3px;
&,
.bp3-progress-meter {
border-radius: 0;
}
}
`;

View File

@@ -0,0 +1,60 @@
//@ts-nocheck
import React from 'react';
import {
MenuItem,
Button,
Position,
PopoverInteractionKind,
} from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { Icon, FormattedMessage as T } from 'components';
/**
*
* @param {*} film
* @param {*} param1
* @returns
*/
const projectTransactionItemRenderer = (
transaction,
{ handleClick, modifiers, query },
) => {
return (
<MenuItem
disabled={modifiers.disabled}
key={transaction.path}
onClick={handleClick}
text={transaction.name}
/>
);
};
const projectTransactionSelectProps = {
itemRenderer: projectTransactionItemRenderer,
filterable: false,
popoverProps: {
minimal: true,
position: Position.BOTTOM_LEFT,
interactionKind: PopoverInteractionKind.CLICK,
modifiers: {
offset: { offset: '0, 4' },
},
},
};
/**
* Project transactions select
* @param
* @returns
*/
export function ProjectTransactionsSelect({ transactions, ...rest }) {
return (
<Select {...projectTransactionSelectProps} items={transactions} {...rest}>
<Button
minimal={true}
icon={<Icon icon={'plus'} />}
text={<T id={'projcet_details.action.new_transaction'} />}
/>
</Select>
);
}

View File

@@ -0,0 +1,2 @@
export * from './ProjectTransactionsSelect';
export * from './FinancialSection';

View File

@@ -0,0 +1,37 @@
//@ts-nocheck
import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import ProjectDetailActionsBar from './ProjectDetailActionsBar';
import ProjectDetailTabs from './ProjectDetailTabs';
import { DashboardPageContent } from 'components';
import { ProjectDetailProvider } from './ProjectDetailProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/**
* Project tabs.
* @returns
*/
function ProjectTabs({
// #withDashboardActions
changePageTitle,
}) {
const {
state: { projectName, projectId },
} = useLocation();
useEffect(() => {
changePageTitle(projectName);
}, [changePageTitle, projectName]);
return (
<ProjectDetailProvider projectId={projectId}>
<ProjectDetailActionsBar />
<DashboardPageContent>
<ProjectDetailTabs />
</DashboardPageContent>
</ProjectDetailProvider>
);
}
export default compose(withDashboardActions)(ProjectTabs);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
const Schema = Yup.object().shape({
contact_id: Yup.string().label(intl.get('project.schema.label.contact')),
name: Yup.string()
.label(intl.get('project.schema.label.project_name'))
.required(),
deadline: Yup.date()
.label(intl.get('project.schema.label.deadline'))
.required(),
published: Yup.boolean().label(
intl.get('project.schema.label.project_state'),
),
cost_estimate: Yup.number().label(
intl.get('project.schema.label.project_cost'),
),
});
export const CreateProjectFormSchema = Schema;

View File

@@ -0,0 +1,92 @@
// @ts-nocheck
import React from 'react';
import moment from 'moment';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import ProjectFormContent from './ProjectFormContent';
import { CreateProjectFormSchema } from './ProjectForm.schema';
import { useProjectFormContext } from './ProjectFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose, transformToForm } from 'utils';
const defaultInitialValues = {
contact_id: '',
name: '',
deadline: moment(new Date()).format('YYYY-MM-DD'),
published: false,
cost_estimate: '',
};
/**
* Project form
* @returns
*/
function ProjectForm({
// #withDialogActions
closeDialog,
}) {
// project form dialog context.
const {
dialogName,
project,
isNewMode,
projectId,
createProjectMutate,
editProjectMutate,
} = useProjectFormContext();
// Initial form values
const initialValues = {
...defaultInitialValues,
...transformToForm(project, defaultInitialValues),
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
const form = { ...values };
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
? 'projects.dialog.success_message'
: 'projects.dialog.edit_success_message',
),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
};
if (isNewMode) {
createProjectMutate(form).then(onSuccess).catch(onError);
} else {
editProjectMutate([projectId, form]).then(onSuccess).catch(onError);
}
};
return (
<Formik
validationSchema={CreateProjectFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
component={ProjectFormContent}
/>
);
}
export default compose(withDialogActions)(ProjectForm);

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Form } from 'formik';
import ProjectFormFields from './ProjectFormFields';
import ProjectFormFloatingActions from './ProjectFormFloatingActions';
/**
* Project form content.
*/
export default function ProjectFormContent() {
return (
<Form>
<ProjectFormFields />
<ProjectFormFloatingActions />
</Form>
);
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { ProjectFormProvider } from './ProjectFormProvider';
import ProjectForm from './ProjectForm';
/**
* Project form dialog content.
* @returns {ReactNode}
*/
export default function ProjectFormDialogContent({
// #ownProps
dialogName,
project,
}) {
return (
<ProjectFormProvider projectId={project} dialogName={dialogName}>
<ProjectForm />
</ProjectFormProvider>
);
}

View File

@@ -0,0 +1,116 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { useFormikContext } from 'formik';
import { Classes, Position, FormGroup, ControlGroup } from '@blueprintjs/core';
import { FastField } from 'formik';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import {
FFormGroup,
FInputGroup,
FCheckbox,
FDateInput,
FMoneyInputGroup,
InputPrependText,
FormattedMessage as T,
FieldRequiredHint,
CustomerSelectField,
} from 'components';
import {
inputIntent,
momentFormatter,
tansformDateValue,
handleDateChange,
} from 'utils';
import { useProjectFormContext } from './ProjectFormProvider';
/**
* Project form fields.
* @returns
*/
function ProjectFormFields() {
// project form dialog context.
const { customers } = useProjectFormContext();
// Formik context.
const { values } = useFormikContext();
return (
<div className={Classes.DIALOG_BODY}>
{/*------------ Contact -----------*/}
<FastField name={'contact_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={intl.get('projects.dialog.contact')}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={inputIntent({ error, touched })}
>
<CustomerSelectField
contacts={customers}
selectedContactId={value}
defaultSelectText={'Select Contact Account'}
onContactSelected={(customer) => {
form.setFieldValue('contact_id', customer.id);
}}
allowCreate={true}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/*------------ Project Name -----------*/}
<FFormGroup
label={intl.get('projects.dialog.project_name')}
name={'name'}
labelInfo={<FieldRequiredHint />}
>
<FInputGroup name="name" />
</FFormGroup>
{/*------------ DeadLine -----------*/}
<FFormGroup
label={intl.get('projects.dialog.deadline')}
name={'deadline'}
className={classNames(CLASSES.FILL, 'form-group--date')}
>
<FDateInput
{...momentFormatter('YYYY/MM/DD')}
name="deadline"
formatDate={(date) => date.toLocaleString()}
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
/>
</FFormGroup>
{/*------------ CheckBox -----------*/}
<FFormGroup name={'published'}>
<FCheckbox
name="published"
label={intl.get('projects.dialog.calculator_expenses')}
/>
</FFormGroup>
{/*------------ Cost Estimate -----------*/}
<FFormGroup
name={'cost_estimate'}
label={intl.get('projects.dialog.cost_estimate')}
labelInfo={<FieldRequiredHint />}
>
<ControlGroup>
<InputPrependText text={'USD'} />
<FMoneyInputGroup
disabled={values.published}
name={'cost_estimate'}
allowDecimals={true}
allowNegativeValue={true}
/>
</ControlGroup>
</FFormGroup>
</div>
);
}
export default ProjectFormFields;

View File

@@ -0,0 +1,48 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { useProjectFormContext } from './ProjectFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Project form floating actions.
* @returns
*/
function ProjectFormFloatingActions({
// #withDialogActions
closeDialog,
}) {
// Formik context.
const { isSubmitting } = useFormikContext();
// project form dialog context.
const { dialogName } = useProjectFormContext();
// 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: '85px' }}>
<T id={'cancel'} />
</Button>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '75px' }}
type="submit"
>
<T id={'projects.label.create'} />
</Button>
</div>
</div>
);
}
export default compose(withDialogActions)(ProjectFormFloatingActions);

View File

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

View File

@@ -0,0 +1,59 @@
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 ProjectDialogContent = React.lazy(
() => import('./ProjectFormDialogContent'),
);
/**
* Project form dialog.
* @returns
*/
function ProjectFormDialog({
dialogName,
payload: { projectId = null, action },
isOpen,
}) {
return (
<ProjectFormDialogRoot
name={dialogName}
title={
action === 'edit' ? (
<T id="projects.dialog.edit_project" />
) : (
<T id={'projects.dialog.new_project'} />
)
}
isOpen={isOpen}
autoFocus={true}
canEscapeKeyClose={true}
style={{ width: '400px' }}
>
<DialogSuspense>
<ProjectDialogContent dialogName={dialogName} project={projectId} />
</DialogSuspense>
</ProjectFormDialogRoot>
);
}
export default compose(withDialogRedux())(ProjectFormDialog);
const ProjectFormDialogRoot = styled(Dialog)`
.bp3-dialog-body {
.bp3-form-group {
margin-bottom: 15px;
margin-top: 15px;
label.bp3-label {
margin-bottom: 3px;
font-size: 13px;
}
}
}
.bp3-dialog-footer {
padding-top: 10px;
}
`;

View File

@@ -0,0 +1,16 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({
taskName: Yup.string()
.label(intl.get('task.schema.label.task_name'))
.required(),
taskHouse: Yup.string().label(intl.get('task.schema.label.task_house')),
taskCharge: Yup.string()
.label(intl.get('task.schema.label.charge'))
.required(),
taskamount: Yup.number().label(intl.get('task.schema.label.amount')),
});
export const CreateProjectTaskFormSchema = Schema;

View File

@@ -0,0 +1,62 @@
//@ts-nocheck
import React from 'react';
import { Formik } from 'formik';
import { CreateProjectTaskFormSchema } from './ProjectTaskForm.schema';
import { useProjectTaskFormContext } from './ProjectTaskFormProvider';
import { AppToaster } from 'components';
import ProjectTaskFormContent from './ProjectTaskFormContent';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const defaultInitialValues = {
taskName: '',
taskHouse: '00:00',
taskCharge: 'hourly_rate',
taskamount: '',
};
/**
* Project task form.
* @returns
*/
function ProjectTaskForm({
// #withDialogActions
closeDialog,
}) {
// task form dialog context.
const { dialogName } = useProjectTaskFormContext();
// Initial form values
const initialValues = {
...defaultInitialValues,
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {};
// Handle request response success.
const onSuccess = (response) => {};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
};
};
return (
<Formik
validationSchema={CreateProjectTaskFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
component={ProjectTaskFormContent}
/>
);
}
export default compose(withDialogActions)(ProjectTaskForm);

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
//@ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { useFormikContext } from 'formik';
import { Classes, ControlGroup } from '@blueprintjs/core';
import {
FFormGroup,
FInputGroup,
Col,
Row,
FormattedMessage as T,
} from 'components';
import { taskChargeOptions } from 'containers/Projects/containers/common/modalChargeOptions';
import { ChangeTypesSelect } from '../../components';
/**
* Project task form fields.
* @returns
*/
function ProjectTaskFormFields() {
// Formik context.
const { values } = useFormikContext();
return (
<div className={Classes.DIALOG_BODY}>
{/*------------ Task Name -----------*/}
<FFormGroup label={<T id={'project_task.dialog.task_name'} />} name={'taskName'}>
<FInputGroup name="taskName" />
</FFormGroup>
{/*------------ Estimated Hours -----------*/}
<Row>
<Col xs={4}>
<FFormGroup
label={<T id={'project_task.dialog.estimated_hours'} />}
name={'taskHouse'}
>
<FInputGroup name="taskHouse" />
</FFormGroup>
</Col>
{/*------------ Charge -----------*/}
<Col xs={8}>
<FFormGroup
name={'taskCharge'}
className={'form-group--select-list'}
label={<T id={'project_task.dialog.charge'} />}
>
<ControlGroup>
<ChangeTypesSelect
name="taskCharge"
items={taskChargeOptions}
popoverProps={{ minimal: true }}
filterable={false}
/>
<FInputGroup
name="taskamount"
disabled={values?.taskCharge === 'Non-chargeable'}
/>
</ControlGroup>
</FFormGroup>
</Col>
</Row>
{/*------------ Estimated Amount -----------*/}
<EstimatedAmountBase>
<EstimatedAmountContent>
<T id={'project_task.dialog.estimated_amount'} />
<EstimateAmount>0.00</EstimateAmount>
</EstimatedAmountContent>
</EstimatedAmountBase>
</div>
);
}
export default ProjectTaskFormFields;
const EstimatedAmountBase = styled.div`
display: flex;
justify-content: flex-end;
font-size: 14px;
line-height: 1.5rem;
opacity: 0.75;
`;
const EstimatedAmountContent = styled.span`
background-color: #fffdf5;
padding: 0.1rem 0;
`;
const EstimateAmount = styled.span`
font-size: 15px;
font-weight: 700;
margin-left: 10px;
`;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
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('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;

View File

@@ -0,0 +1,69 @@
// @ts-nocheck
import React from 'react';
import moment from 'moment';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { AppToaster } from 'components';
import ProjectTimeEntryFormContent from './ProjectTimeEntryFormContent';
import { CreateProjectTimeEntryFormSchema } from './ProjectTimeEntryForm.schema';
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const defaultInitialValues = {
date: moment(new Date()).format('YYYY-MM-DD'),
projectId: '',
taskId: '',
description: '',
duration: '',
};
/**
* Project Time entry form.
* @returns
*/
function ProjectTimeEntryForm({
// #withDialogActions
closeDialog,
}) {
// time entry form dialog context.
const { dialogName } = useProjectTimeEntryFormContext();
// Initial form values
const initialValues = {
...defaultInitialValues,
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {};
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
};
};
return (
<Formik
validationSchema={CreateProjectTimeEntryFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
component={ProjectTimeEntryFormContent}
/>
);
}
export default compose(withDialogActions)(ProjectTimeEntryForm);

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Classes, Intent, Position } from '@blueprintjs/core';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import {
FFormGroup,
FInputGroup,
FDateInput,
FTextArea,
FEditableText,
FieldRequiredHint,
FormattedMessage as T,
} from 'components';
import { TaskSelect, ProjectsSelect } from '../../components';
import { momentFormatter } from 'utils';
/**
* Project time entry form fields.
* @returns
*/
function ProjectTimeEntryFormFields() {
return (
<div className={Classes.DIALOG_BODY}>
{/*------------ Project -----------*/}
<FFormGroup
name={'projectId'}
label={<T id={'project_time_entry.dialog.project'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<ProjectsSelect
name={'projectId'}
projects={[]}
popoverProps={{ minimal: true }}
/>
</FFormGroup>
{/*------------ Task -----------*/}
<FFormGroup
name={'taskId'}
label={<T id={'project_time_entry.dialog.task'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<TaskSelect
name={'taskId'}
tasks={[]}
popoverProps={{ minimal: true }}
/>
</FFormGroup>
{/*------------ Duration -----------*/}
<FFormGroup
label={intl.get('project_time_entry.dialog.duration')}
name={'duration'}
labelInfo={<FieldRequiredHint />}
>
<FInputGroup name="duration" inputProps={{}} />
</FFormGroup>
{/*------------ Date -----------*/}
<FFormGroup
label={intl.get('project_time_entry.dialog.date')}
name={'date'}
className={classNames(CLASSES.FILL, 'form-group--date')}
>
<FDateInput
{...momentFormatter('YYYY/MM/DD')}
name="date"
formatDate={(date) => date.toLocaleString()}
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
/>
</FFormGroup>
{/*------------ Description -----------*/}
<FFormGroup
name={'description'}
label={intl.get('project_time_entry.dialog.description')}
className={'form-group--description'}
>
<FTextArea name={'description'} />
</FFormGroup>
</div>
);
}
export default ProjectTimeEntryFormFields;

View File

@@ -0,0 +1,48 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { useProjectTimeEntryFormContext } from './ProjectTimeEntryFormProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Projcet time entry form floating actions.
* @returns
*/
function ProjectTimeEntryFormFloatingActions({
// #withDialogActions
closeDialog,
}) {
// time entry form dialog context.
const { dialogName } = useProjectTimeEntryFormContext();
// Formik context.
const { isSubmitting } = useFormikContext();
// 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={'time_entry.dialog.create'} />
</Button>
</div>
</div>
);
}
export default compose(withDialogActions)(ProjectTimeEntryFormFloatingActions);

View File

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

View File

@@ -0,0 +1,67 @@
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 ProjectTimeEntryFormDialogContent = React.lazy(
() => import('./ProjectTimeEntryFormDialogContent'),
);
/**
* Project time entry form dialog.
* @returns
*/
function ProjectTimeEntryFormDialog({
dialogName,
isOpen,
payload: { projectId = null, timeEntryId = null },
}) {
return (
<ProjectTimeEntryFormDialogRoot
name={dialogName}
title={<T id={'project_time_entry.dialog.label'} />}
isOpen={isOpen}
autoFocus={true}
canEscapeKeyClose={true}
style={{ width: '400px' }}
>
<DialogSuspense>
<ProjectTimeEntryFormDialogContent
dialogName={dialogName}
project={projectId}
timeEntry={timeEntryId}
/>
</DialogSuspense>
</ProjectTimeEntryFormDialogRoot>
);
}
export default compose(withDialogRedux())(ProjectTimeEntryFormDialog);
const ProjectTimeEntryFormDialogRoot = styled(Dialog)`
.bp3-dialog-body {
.bp3-form-group {
margin-bottom: 15px;
label.bp3-label {
margin-bottom: 3px;
font-size: 13px;
}
}
.form-group {
&--description {
.bp3-form-content {
textarea {
width: 100%;
min-width: 100%;
font-size: 14px;
}
}
}
}
}
.bp3-dialog-footer {
padding-top: 10px;
}
`;

View File

@@ -0,0 +1,130 @@
import React from 'react';
import {
Button,
NavbarGroup,
Classes,
NavbarDivider,
Alignment,
} from '@blueprintjs/core';
import {
Icon,
AdvancedFilterPopover,
DashboardActionViewsList,
DashboardFilterButton,
DashboardRowsHeightButton,
FormattedMessage as T,
} from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withProjects from './withProjects';
import withProjectsActions from './withProjectsActions';
import withSettings from '../../../Settings/withSettings';
import withSettingsActions from '../../../Settings/withSettingsActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* Projects actions bar.
* @returns
*/
function ProjectsActionsBar({
// #withDialogActions
openDialog,
// #withProjects
projectsFilterRoles,
// #withProjectsActions
setProjectsTableState,
// #withSettings
projectsTableSize,
// #withSettingsActions
addSetting,
}) {
// Handle tab change.
const handleTabChange = (view) => {
setProjectsTableState({
viewSlug: view ? view.slug : null,
});
};
// Handle click a refresh projects list.
const handleRefreshBtnClick = () => {};
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
addSetting('projects', 'tableSize', size);
};
// Handle new project button click.
const handleNewProjectBtnClick = () => {
openDialog('project-form');
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'projects'}
allMenuItem={true}
allMenuItemText={<T id={'all'} />}
views={[]}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'projects.label.new_project'} />}
onClick={handleNewProjectBtnClick}
/>
{/* AdvancedFilterPopover */}
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
/>
<NavbarDivider />
<DashboardRowsHeightButton
initialValue={projectsTableSize}
onChange={handleTableRowSizeChange}
/>
<NavbarDivider />
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
onClick={handleRefreshBtnClick}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withDialogActions,
withProjectsActions,
withSettingsActions,
withProjects(({ projectsTableState }) => ({
projectsFilterRoles: projectsTableState?.filterRoles,
})),
withSettings(({ projectSettings }) => ({
projectsTableSize: projectSettings?.tableSize,
})),
)(ProjectsActionsBar);

View File

@@ -0,0 +1,159 @@
//@ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
import { DataTable } from 'components';
import { TABLES } from 'common/tables';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import ProjectsEmptyStatus from './ProjectsEmptyStatus';
import { useProjectsListContext } from './ProjectsListProvider';
import { useMemorizedColumnsWidths } from 'hooks';
import { useProjectsListColumns, ActionsMenu } from './components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withProjectsActions from './withProjectsActions';
import withSettings from '../../../Settings/withSettings';
import { compose } from 'utils';
/**
* Projects list datatable.
* @returns
*/
function ProjectsDataTable({
// #withDial
openDialog,
// #withAlertsActions
openAlert,
// #withSettings
projectsTableSize,
}) {
const history = useHistory();
// Projects list context.
const { projects, isEmptyStatus, isProjectsLoading, isProjectsFetching } =
useProjectsListContext();
// Handle delete project.
const handleDeleteProject = ({ id }) => {
openAlert('project-delete', { projectId: id });
};
// Retrieve projects table columns.
const columns = useProjectsListColumns();
// Handle cell click.
const handleCellClick = ({ row: { original } }) => {
return history.push(`/projects/${original?.id}/details`, {
projectId: original.id,
projectName: original.name,
});
};
// Handle edit project.
const handleEditProject = (project) => {
openDialog('project-form', {
projectId: project.id,
action: 'edit',
});
};
// Handle new task button click.
const handleNewTaskButtonClick = () => {
openDialog('project-task-form');
};
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.PROJECTS);
// Handle view detail project.
const handleViewDetailProject = (project) => {
return history.push(`/projects/${project.id}/details`, {
projectId: project.id,
projectName: project.name,
});
};
// Display project empty status instead of the table.
if (isEmptyStatus) {
return <ProjectsEmptyStatus />;
}
return (
<ProjectsTable
columns={columns}
data={projects}
loading={isProjectsLoading}
headerLoading={isProjectsLoading}
progressBarLoading={isProjectsFetching}
manualSortBy={true}
noInitialFetch={true}
sticky={true}
hideTableHeader={true}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
onCellClick={handleCellClick}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={projectsTableSize}
payload={{
onViewDetails: handleViewDetailProject,
onEdit: handleEditProject,
onDelete: handleDeleteProject,
onNewTask: handleNewTaskButtonClick,
}}
/>
);
}
export default compose(
withDialogActions,
withAlertsActions,
withProjectsActions,
withSettings(({ projectSettings }) => ({
projectsTableSize: projectSettings?.tableSize,
})),
)(ProjectsDataTable);
const ProjectsTable = styled(DataTable)`
.tbody {
.tr .td {
padding: 0.5rem 0.8rem;
}
.avatar.td {
.cell-inner {
.avatar {
display: inline-block;
background: #adbcc9;
border-radius: 8%;
text-align: center;
font-weight: 400;
color: #fff;
&[data-size='medium'] {
height: 30px;
width: 30px;
line-height: 30px;
font-size: 14px;
}
&[data-size='small'] {
height: 25px;
width: 25px;
line-height: 25px;
font-size: 12px;
}
}
}
}
}
.table-size--small {
.tbody .tr {
height: 45px;
}
}
`;

View File

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

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { DashboardPageContent, DashboardContentTable } from 'components';
import ProjectsActionsBar from './ProjectsActionsBar';
import ProjectsViewTabs from './ProjectsViewTabs';
import ProjectsDataTable from './ProjectsDataTable';
import withProjects from './withProjects';
import withProjectsActions from './withProjectsActions';
import { ProjectsListProvider } from './ProjectsListProvider';
import { compose, transformTableStateToQuery } from 'utils';
/**
* Projects list.
* @returns
*/
function ProjectsList({
// #withProjects
projectsTableState,
projectsTableStateChanged,
// #withProjectsActions
resetProjectsTableState,
}) {
// Resets the projects table state once the page unmount.
React.useEffect(
() => () => {
resetProjectsTableState();
},
[resetProjectsTableState],
);
return (
<ProjectsListProvider
query={transformTableStateToQuery(projectsTableState)}
tableStateChanged={projectsTableStateChanged}
>
<ProjectsActionsBar />
<DashboardPageContent>
<ProjectsViewTabs />
<DashboardContentTable>
<ProjectsDataTable />
</DashboardContentTable>
</DashboardPageContent>
</ProjectsListProvider>
);
}
export default compose(
withProjects(({ projectsTableState, projectsTableStateChanged }) => ({
projectsTableState,
projectsTableStateChanged,
})),
withProjectsActions,
)(ProjectsList);

View File

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

View File

@@ -0,0 +1,54 @@
//@ts-nocheck
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { DashboardViewsTabs } from 'components';
import withProjects from './withProjects';
import withProjectsActions from './withProjectsActions';
import { useProjectsListContext } from './ProjectsListProvider';
import { compose, transfromViewsToTabs } from 'utils';
/**
* Projects views tabs.
* @returns
*/
function ProjectsViewTabs({
// #withProjects
projectsCurrentView,
// #withProjectsActions
setProjectsTableState,
}) {
// Projects list context.
const { projectsViews } = useProjectsListContext();
// Projects views.
const tabs = transfromViewsToTabs(projectsViews);
// Handle tab change.
const handleTabsChange = (viewSlug) => {
setProjectsTableState({ viewSlug: viewSlug || null });
};
return (
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
currentViewSlug={projectsCurrentView}
resourceName={'projects'}
tabs={tabs}
onChange={handleTabsChange}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withProjects(({ projectsTableState }) => ({
projectsCurrentView: projectsTableState?.viewSlug,
})),
withProjectsActions,
)(ProjectsViewTabs);

View File

@@ -0,0 +1,207 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import {
Menu,
MenuDivider,
MenuItem,
Tag,
Intent,
ProgressBar,
} from '@blueprintjs/core';
import { Icon, FormatDate, Choose, FormattedMessage as T } from 'components';
import { safeCallback, firstLettersArgs, calculateStatus } from 'utils';
/**
* project status.
*/
export function ProjectStatus({ project }) {
return (
<ProjectStatusRoot>
<ProjectStatusTaskAmount>{project.task_amount}</ProjectStatusTaskAmount>
<ProjectProgressBar
animate={false}
stripes={false}
// intent={Intent.PRIMARY}
value={calculateStatus(project.task_amount, project.cost_estimate)}
/>
</ProjectStatusRoot>
);
}
/**
* status accessor.
*/
export const StatusAccessor = (project) => {
return (
<Choose>
<Choose.When condition={project.is_process}>
<ProjectStatus project={project} />
</Choose.When>
<Choose.When condition={project.is_closed}>
<StatusTag minimal={true} intent={Intent.SUCCESS} round={true}>
<T id={'closed'} />
</StatusTag>
</Choose.When>
<Choose.When condition={project.is_draft}>
<StatusTag round={true} minimal={true}>
<T id={'draft'} />
</StatusTag>
</Choose.When>
</Choose>
);
};
/**
* Avatar cell.
*/
export const AvatarCell = ({ row: { original }, size }) => (
<span className="avatar" data-size={size}>
{firstLettersArgs(original?.display_name, original?.name)}
</span>
);
/**
* Table actions cell.
*/
export const ActionsMenu = ({
row: { original },
payload: { onEdit, onDelete, onViewDetails, onNewTask },
}) => (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('projects.action.edit_project')}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
icon={<Icon icon="plus" />}
text={intl.get('projects.action.new_task')}
onClick={safeCallback(onNewTask, original)}
/>
<MenuDivider />
<MenuItem
text={intl.get('projects.action.delete_project')}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
</Menu>
);
/**
* Projects accessor.
*/
export const ProjectsAccessor = (row) => (
<ProjectItemsWrap>
<ProjectItemsHeader>
<ProjectItemContactName>
{row.contact_display_name}
</ProjectItemContactName>
<ProjectItemProjectName>{row.name}</ProjectItemProjectName>
</ProjectItemsHeader>
<ProjectItemDescription>
<FormatDate value={row.deadline_formatted} />
{intl.get('projects.label.cost_estimate', {
value: row.cost_estimate_formatted,
})}
</ProjectItemDescription>
</ProjectItemsWrap>
);
/**
* Retrieve projects list columns columns.
*/
export const useProjectsListColumns = () => {
return React.useMemo(
() => [
{
id: 'avatar',
Header: '',
Cell: AvatarCell,
className: 'avatar',
width: 45,
disableResizing: true,
disableSortBy: true,
clickable: true,
},
{
id: 'name',
Header: '',
accessor: ProjectsAccessor,
width: 240,
className: 'name',
clickable: true,
},
{
id: 'status',
Header: '',
accessor: StatusAccessor,
width: 50,
className: 'status',
},
],
[],
);
};
const ProjectItemsWrap = styled.div``;
const ProjectItemsHeader = styled.div`
display: flex;
align-items: baseline;
line-height: 1.3rem;
`;
const ProjectItemContactName = styled.div`
font-weight: 500;
padding-right: 4px;
`;
const ProjectItemProjectName = styled.div``;
const ProjectItemDescription = styled.div`
display: inline-block;
font-size: 13px;
opacity: 0.75;
margin-top: 0.2rem;
line-height: 1;
`;
const ProjectStatusRoot = styled.div`
display: flex;
align-items: center;
margin-right: 0.5rem;
flex-direction: row-reverse;
`;
const ProjectStatusTaskAmount = styled.div`
text-align: right;
font-weight: 400;
line-height: 1.5rem;
margin-left: 20px;
`;
const ProjectProgressBar = styled(ProgressBar)`
&.bp3-progress-bar {
display: block;
flex-shrink: 0;
height: 3px;
max-width: 110px;
&,
.bp3-progress-meter {
border-radius: 0;
}
}
`;
const StatusTag = styled(Tag)`
min-width: 65px;
text-align: center;
`;

View File

@@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import {
getProjectsTableStateFactory,
isProjectsTableStateChangedFactory,
} from '../../../../store/Project/projects.selectors';
export default (mapState) => {
const getProjectsTableState = getProjectsTableStateFactory();
const isProjectsTableStateChanged = isProjectsTableStateChangedFactory();
const mapStateToProps = (state, props) => {
const mapped = {
projectsTableState: getProjectsTableState(state, props),
projectsTableStateChanged: isProjectsTableStateChanged(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import {
setProjectsTableState,
resetProjectsTableState,
} from '../../../../store/Project/projects.actions';
const mapDispatchToProps = (dispatch) => ({
setProjectsTableState: (state) => dispatch(setProjectsTableState(state)),
resetProjectsTableState: () => dispatch(resetProjectsTableState()),
});
export default connect(null, mapDispatchToProps);

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

@@ -969,6 +969,23 @@ export const getDashboardRoutes = () => [
),
pageTitle: intl.get('sidebar.transactions_locaking'),
},
{
path: '/projects/:id/details',
component: lazy(() =>
import('../containers/Projects/containers/ProjectDetails'),
),
sidebarExpand: false,
backLink: true,
},
{
path: '/projects',
component: lazy(() =>
import('../containers/Projects/containers/ProjectsLanding/ProjectsList'),
),
pageTitle: intl.get('sidebar.projects'),
},
// Homepage
{
path: `/`,

View File

@@ -419,11 +419,11 @@ export default {
],
viewBox: '0 0 24 24',
},
'duplicate-16': {
'duplicate-24': {
path: [
'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z',
'M14.15 16.05Q14.525 16.45 15.075 16.45Q15.625 16.45 16.05 16.025Q16.425 15.65 16.425 15.088Q16.425 14.525 16.05 14.125L13.375 11.425V8.275Q13.375 7.725 12.988 7.337Q12.6 6.95 12.025 6.95Q11.45 6.95 11.05 7.35Q10.65 7.75 10.65 8.3V11.975Q10.65 12.275 10.75 12.512Q10.85 12.75 11.075 12.975ZM12 22.875Q9.75 22.875 7.763 22.025Q5.775 21.175 4.3 19.7Q2.825 18.225 1.975 16.238Q1.125 14.25 1.125 12Q1.125 9.725 1.975 7.737Q2.825 5.75 4.3 4.275Q5.775 2.8 7.763 1.962Q9.75 1.125 12 1.125Q14.275 1.125 16.262 1.962Q18.25 2.8 19.725 4.275Q21.2 5.75 22.038 7.737Q22.875 9.725 22.875 12Q22.875 14.275 22.038 16.25Q21.2 18.225 19.725 19.7Q18.25 21.175 16.262 22.025Q14.275 22.875 12 22.875ZM12 12Q12 12 12 12Q12 12 12 12Q12 12 12 12Q12 12 12 12Q12 12 12 12Q12 12 12 12Q12 12 12 12Q12 12 12 12ZM12 20.05Q15.35 20.05 17.7 17.712Q20.05 15.375 20.05 12Q20.05 8.625 17.7 6.287Q15.35 3.95 12 3.95Q8.65 3.95 6.3 6.287Q3.95 8.625 3.95 12Q3.95 15.375 6.3 17.712Q8.65 20.05 12 20.05Z',
],
viewBox: '0 0 16 16',
viewBox: '0 0 24 24',
},
'caret-down-16': {
path: [
@@ -540,10 +540,16 @@ export default {
],
viewBox: '0 0 3 13',
},
'time-24': {
path: [
'M0 7c0-3.873 3.127-7 7-7s7 3.127 7 7-3.127 7-7 7a6.99 6.99 0 0 1-7-7zm1.5 0c0 3.043 2.457 5.5 5.5 5.5s5.5-2.457 5.5-5.5S10.043 1.5 7 1.5A5.493 5.493 0 0 0 1.5 7zM6 4h1v3h3v1H6V4z',
],
viewBox: '0 0 14 14',
},
'star-18dp': {
path: [
'M12,17.27L18.18,21l-1.64-7.03L22,9.24l-7.19-0.61L12,2L9.19,8.63L2,9.24l5.46,4.73L5.82,21L12,17.27z',
],
viewBox: '0 0 24 24',
}
},
};

View File

@@ -0,0 +1,14 @@
import t from 'store/types';
export const setProjectsTableState = (queries) => {
return {
type: t.PROJECTS_TABLE_STATE_SET,
payload: { queries },
};
};
export const resetProjectsTableState = () => {
return {
type: t.PROJECTS_TABLE_STATE_RESET,
};
};

View File

@@ -0,0 +1,33 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { createTableStateReducers } from 'store/tableState.reducer';
import t from 'store/types';
export const defaultTableQuery = {
pageSize: 20,
pageIndex: 0,
filterRoles: [],
viewSlug: null,
};
const initialState = {
tableState: defaultTableQuery,
};
const STORAGE_KEY = 'bigcapital:projects';
const CONFIG = {
key: STORAGE_KEY,
whitelist: [],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('PROJECTS', defaultTableQuery),
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
});
export default persistReducer(CONFIG, reducerInstance);

View File

@@ -0,0 +1,24 @@
import { isEqual } from 'lodash';
import { createDeepEqualSelector } from 'utils';
import { paginationLocationQuery } from 'store/selectors';
import { defaultTableQuery } from './projects.reducer';
const projectsTableState = (state) => state.projects.tableState;
// Retrieve projects table query.
export const getProjectsTableStateFactory = () =>
createDeepEqualSelector(
paginationLocationQuery,
projectsTableState,
(locationQuery, tableState) => {
return {
...locationQuery,
...tableState,
};
},
);
export const isProjectsTableStateChangedFactory = () =>
createDeepEqualSelector(projectsTableState, (tableState) => {
return !isEqual(tableState, defaultTableQuery);
});

Some files were not shown because too many files have changed in this diff Show More