mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 15:50:32 +00:00
Compare commits
39 Commits
v0.7.6
...
BIG-386-li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
754618aa7a | ||
|
|
50c905eabb | ||
|
|
709e06a646 | ||
|
|
8826d2bc5b | ||
|
|
38a961b899 | ||
|
|
7ef7e126e5 | ||
|
|
bcf0ec25b8 | ||
|
|
965a8966f6 | ||
|
|
b030d6ea37 | ||
|
|
31fef21362 | ||
|
|
6f2a456a56 | ||
|
|
6134ad5598 | ||
|
|
cd08d0ee16 | ||
|
|
f268b8a95a | ||
|
|
6a06950654 | ||
|
|
d9de3341fe | ||
|
|
6b6081e32e | ||
|
|
7be568b8ac | ||
|
|
50522af72d | ||
|
|
0b454d6d4d | ||
|
|
4ba64cc4ff | ||
|
|
5128c021b0 | ||
|
|
5a8fcc8fb5 | ||
|
|
9cf1b993dd | ||
|
|
f443a1b106 | ||
|
|
0eb0aee1ef | ||
|
|
4b992c4bb4 | ||
|
|
051681e6f3 | ||
|
|
629c790430 | ||
|
|
bdadc5d795 | ||
|
|
23bb9c4cc3 | ||
|
|
8136378725 | ||
|
|
4eac2239b1 | ||
|
|
a44f548ff9 | ||
|
|
327916da4b | ||
|
|
bee7896279 | ||
|
|
cb0a315ca6 | ||
|
|
d2c907541a | ||
|
|
928d4d3f00 |
@@ -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 = {
|
||||
|
||||
@@ -197,6 +197,7 @@ export default function DataTable(props) {
|
||||
DataTable.defaultProps = {
|
||||
pagination: false,
|
||||
hidePaginationNoPages: true,
|
||||
hideTableHeader: false,
|
||||
|
||||
size: null,
|
||||
spinnerProps: { size: 30 },
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
// ---------------
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -69,6 +69,7 @@ export enum ISidebarMenuOverlayIds {
|
||||
Contacts = 'Contacts',
|
||||
Cashflow = 'Cashflow',
|
||||
Expenses = 'Expenses',
|
||||
Projects = 'Projects',
|
||||
}
|
||||
|
||||
export enum ISidebarSubscriptionAbility {
|
||||
|
||||
50
src/containers/Projects/components/ChangeTypesSelect.tsx
Normal file
50
src/containers/Projects/components/ChangeTypesSelect.tsx
Normal 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} />;
|
||||
}
|
||||
67
src/containers/Projects/components/ExpenseSelect.tsx
Normal file
67
src/containers/Projects/components/ExpenseSelect.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { FSelect } from 'components';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param query
|
||||
* @param expense
|
||||
* @param _index
|
||||
* @param exactMatch
|
||||
*/
|
||||
const expenseItemPredicate = (query, expense, _index, exactMatch) => {
|
||||
const normalizedTitle = expense.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${expense.name}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param expense
|
||||
* @param param1
|
||||
* @returns
|
||||
*/
|
||||
const expenseItemRenderer = (expense, { handleClick, modifiers, query }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
key={expense.id}
|
||||
onClick={handleClick}
|
||||
text={expense.name}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const expenseSelectProps = {
|
||||
itemPredicate: expenseItemPredicate,
|
||||
itemRenderer: expenseItemRenderer,
|
||||
valueAccessor: 'id',
|
||||
labelAccessor: 'name',
|
||||
};
|
||||
|
||||
export function ExpenseSelect({ expenses, defaultText, ...rest }) {
|
||||
return (
|
||||
<FSelect
|
||||
items={expenses}
|
||||
{...expenseSelectProps}
|
||||
{...rest}
|
||||
input={ExpenseSelectButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpenseSelectButton({ label, ...rest }) {
|
||||
return (
|
||||
<Button
|
||||
text={label ? label : intl.get('choose_an_estimated_expense')}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
20
src/containers/Projects/components/FInputGroupComponent.tsx
Normal file
20
src/containers/Projects/components/FInputGroupComponent.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import { FInputGroup } from 'components';
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
export function FInputGroupComponent({ toField, ...props }) {
|
||||
const { values, setFieldValue } = useFormikContext();
|
||||
const { expenseQuantity, expenseUnitPrice } = values;
|
||||
const total = expenseQuantity * expenseUnitPrice;
|
||||
|
||||
const handleBlur = () => {
|
||||
setFieldValue(toField, total);
|
||||
};
|
||||
|
||||
const inputGroupProps = {
|
||||
onBlur: handleBlur,
|
||||
...props,
|
||||
};
|
||||
return <FInputGroup {...inputGroupProps} />;
|
||||
}
|
||||
63
src/containers/Projects/components/ProjectsSelect.tsx
Normal file
63
src/containers/Projects/components/ProjectsSelect.tsx
Normal 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')} />;
|
||||
}
|
||||
64
src/containers/Projects/components/TaskSelect.tsx
Normal file
64
src/containers/Projects/components/TaskSelect.tsx
Normal 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')} />;
|
||||
}
|
||||
5
src/containers/Projects/components/index.ts
Normal file
5
src/containers/Projects/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './ExpenseSelect';
|
||||
export * from './ChangeTypesSelect';
|
||||
export * from './TaskSelect';
|
||||
export * from './ProjectsSelect';
|
||||
export * from './FInputGroupComponent';
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as Yup from 'yup';
|
||||
import intl from 'react-intl-universal';
|
||||
import { DATATYPES_LENGTH } from 'common/dataTypes';
|
||||
|
||||
const Schema = Yup.object().shape({
|
||||
estimatedExpense: Yup.number().label(
|
||||
intl.get('estimated_expense.schema.label.estimated_expense'),
|
||||
),
|
||||
quantity: Yup.number().label(
|
||||
intl.get('estimated_expense.schema.label.quantity'),
|
||||
),
|
||||
unitPrice: Yup.number().label(
|
||||
intl.get('estimated_expense.schema.label.unit_price'),
|
||||
),
|
||||
expenseTotal: Yup.number(),
|
||||
charge: Yup.string(),
|
||||
});
|
||||
|
||||
export const CreateEstimatedExpenseFormSchema = Schema;
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { Formik } from 'formik';
|
||||
import { AppToaster } from 'components';
|
||||
import { CreateEstimatedExpenseFormSchema } from './EstimatedExpense.schema';
|
||||
import EstimatedExpenseFormConent from './EstimatedExpenseFormConent';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const defaultInitialValues = {
|
||||
estimatedExpense: '',
|
||||
unitPrice: '',
|
||||
quantity: 1,
|
||||
charge: '% markup',
|
||||
percentage: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Estimated expense form dialog.
|
||||
* @returns
|
||||
*/
|
||||
function EstimatedExpenseForm({
|
||||
//#withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
const initialValues = {
|
||||
...defaultInitialValues,
|
||||
};
|
||||
|
||||
// Handles the form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
// Handle request response success.
|
||||
const onSuccess = (response) => {
|
||||
AppToaster.show({});
|
||||
};
|
||||
|
||||
// Handle request response errors.
|
||||
const onError = ({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
setSubmitting(false);
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
validationSchema={CreateEstimatedExpenseFormSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
component={EstimatedExpenseFormConent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogActions)(EstimatedExpenseForm);
|
||||
@@ -0,0 +1,54 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Classes, ControlGroup } from '@blueprintjs/core';
|
||||
import { FFormGroup, FInputGroup, Choose } from 'components';
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
function PercentageFormField() {
|
||||
return (
|
||||
<FFormGroup
|
||||
label={intl.get('estimated_expenses.dialog.percentage')}
|
||||
name={'percentage'}
|
||||
>
|
||||
<FInputGroup name="percentage" />
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomPirceField() {
|
||||
return (
|
||||
<ControlGroup className={Classes.FILL}>
|
||||
<FFormGroup
|
||||
name={'unitPrice'}
|
||||
label={intl.get('estimated_expenses.dialog.unit_price')}
|
||||
>
|
||||
<FInputGroup name="unitPrice" />
|
||||
</FFormGroup>
|
||||
<FFormGroup
|
||||
name={'unitPrice'}
|
||||
label={intl.get('estimated_expenses.dialog.total')}
|
||||
>
|
||||
<FInputGroup name="total" />
|
||||
</FFormGroup>
|
||||
</ControlGroup>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* estimate expense form charge fields.
|
||||
* @returns
|
||||
*/
|
||||
export default function EstimatedExpenseFormChargeFields() {
|
||||
const { values } = useFormikContext();
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={values.charge === 'markup'}>
|
||||
<PercentageFormField />
|
||||
</Choose.When>
|
||||
<Choose.When condition={values.charge === 'custom_pirce'}>
|
||||
<CustomPirceField />
|
||||
</Choose.When>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Form } from 'formik';
|
||||
import EstimatedExpenseFormFields from './EstimatedExpenseFormFields';
|
||||
import EstimatedExpenseFormFloatingActions from './EstimatedExpenseFormFloatingActions';
|
||||
|
||||
/**
|
||||
* Estimated expense form content.
|
||||
* @returns
|
||||
*/
|
||||
export default function EstimatedExpenseFormConent() {
|
||||
return (
|
||||
<Form>
|
||||
<EstimatedExpenseFormFields />
|
||||
<EstimatedExpenseFormFloatingActions />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { EstimatedExpenseFormProvider } from './EstimatedExpenseFormProvider';
|
||||
import EstimatedExpenseForm from './EstimatedExpenseForm';
|
||||
|
||||
/**
|
||||
* Estimate expense form dialog.
|
||||
* @return
|
||||
*/
|
||||
export default function EstimatedExpenseFormDialogContent({
|
||||
//#ownProps
|
||||
dialogName,
|
||||
estimatedExpense,
|
||||
}) {
|
||||
return (
|
||||
<EstimatedExpenseFormProvider
|
||||
dialogName={dialogName}
|
||||
estimatedExpenseId={estimatedExpense}
|
||||
>
|
||||
<EstimatedExpenseForm />
|
||||
</EstimatedExpenseFormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Classes, ControlGroup } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FormattedMessage as T,
|
||||
FieldRequiredHint,
|
||||
} from 'components';
|
||||
import { ExpenseSelect, FInputGroupComponent } from '../../components';
|
||||
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
|
||||
import EstimatedExpenseFormChargeFields from './EstimatedExpenseFormChargeFields';
|
||||
import { ChangeTypesSelect } from '../../components';
|
||||
import { expenseChargeOption } from 'containers/Projects/containers/common/modalChargeOptions';
|
||||
|
||||
/**
|
||||
* Estimated expense form fields.
|
||||
* @returns
|
||||
*/
|
||||
export default function EstimatedExpenseFormFields() {
|
||||
return (
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
{/*------------ Estimated Expense -----------*/}
|
||||
<FFormGroup
|
||||
name={'estimatedExpense'}
|
||||
label={intl.get('estimated_expenses.dialog.estimated_expense')}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
<ExpenseSelect
|
||||
name={'estimatedExpense'}
|
||||
popoverProps={{ minimal: true }}
|
||||
expenses={[]}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
{/*------------ Quantity -----------*/}
|
||||
<FFormGroup
|
||||
label={intl.get('estimated_expenses.dialog.quantity')}
|
||||
name={'quantity'}
|
||||
>
|
||||
<FInputGroupComponent name="quantity" />
|
||||
</FFormGroup>
|
||||
|
||||
<MetaLineLabel>
|
||||
<T id={'estimated_expenses.dialog.cost_to_you'} />
|
||||
</MetaLineLabel>
|
||||
{/*------------ Unit Price -----------*/}
|
||||
<ControlGroup className={Classes.FILL}>
|
||||
<FFormGroup
|
||||
name={'unitPrice'}
|
||||
label={intl.get('estimated_expenses.dialog.unit_price')}
|
||||
>
|
||||
<FInputGroupComponent name="unitPrice" />
|
||||
</FFormGroup>
|
||||
<FFormGroup
|
||||
name={'unitPrice'}
|
||||
label={intl.get('estimated_expenses.dialog.total')}
|
||||
>
|
||||
<FInputGroup name="expenseTotal" />
|
||||
</FFormGroup>
|
||||
</ControlGroup>
|
||||
|
||||
<MetaLineLabel>
|
||||
<T id={'estimated_expenses.dialog.what_you_ll_charge'} />
|
||||
</MetaLineLabel>
|
||||
{/*------------ Charge -----------*/}
|
||||
<FFormGroup
|
||||
name={'charge'}
|
||||
label={<T id={'estimated_expenses.dialog.charge'} />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
<ChangeTypesSelect
|
||||
name="charge"
|
||||
items={expenseChargeOption}
|
||||
popoverProps={{ minimal: true }}
|
||||
filterable={false}
|
||||
/>
|
||||
</FFormGroup>
|
||||
<EstimatedExpenseFormChargeFields />
|
||||
{/*------------ Estimated Amount -----------*/}
|
||||
<EstimatedAmountWrap>
|
||||
<EstimatedAmountLabel>
|
||||
<T id={'estimated_expenses.dialog.estimated_amount'} />
|
||||
</EstimatedAmountLabel>
|
||||
<EstimatedAmount>0.00</EstimatedAmount>
|
||||
</EstimatedAmountWrap>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MetaLineLabel = styled.div`
|
||||
font-size: 14px;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const EstimatedAmountWrap = styled.div`
|
||||
display: block;
|
||||
text-align: right;
|
||||
`;
|
||||
const EstimatedAmountLabel = styled.span`
|
||||
font-size: 14px;
|
||||
line-height: 1.5rem;
|
||||
opacity: 0.75;
|
||||
`;
|
||||
const EstimatedAmount = styled.span`
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding-left: 14px;
|
||||
line-height: 2rem;
|
||||
`;
|
||||
@@ -0,0 +1,48 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import { useEstimatedExpenseFormContext } from './EstimatedExpenseFormProvider';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Estimated expense form floating actions.
|
||||
* @returns
|
||||
*/
|
||||
function EstimatedExpenseFormFloatingActions({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
// Formik context.
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
// expense form dialog context.
|
||||
const { dialogName } = useEstimatedExpenseFormContext();
|
||||
|
||||
// Handle close button click.
|
||||
const handleCancelBtnClick = () => {
|
||||
closeDialog(dialogName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
|
||||
<T id={'cancel'} />
|
||||
</Button>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
loading={isSubmitting}
|
||||
style={{ minWidth: '75px' }}
|
||||
type="submit"
|
||||
>
|
||||
{<T id={'save'} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogActions)(EstimatedExpenseFormFloatingActions);
|
||||
@@ -0,0 +1,31 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import { DialogContent } from 'components';
|
||||
|
||||
const EstimatedExpenseFormContext = React.createContext();
|
||||
|
||||
/**
|
||||
* Estimated expense form provider.
|
||||
* @returns
|
||||
*/
|
||||
function EstimatedExpenseFormProvider({
|
||||
//#OwnProps
|
||||
dialogName,
|
||||
estimatedExpenseId,
|
||||
...props
|
||||
}) {
|
||||
// state provider.
|
||||
const provider = {
|
||||
dialogName,
|
||||
};
|
||||
return (
|
||||
<DialogContent>
|
||||
<EstimatedExpenseFormContext.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const useEstimatedExpenseFormContext = () =>
|
||||
React.useContext(EstimatedExpenseFormContext);
|
||||
|
||||
export { EstimatedExpenseFormProvider, useEstimatedExpenseFormContext };
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
|
||||
import withDialogRedux from 'components/DialogReduxConnect';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const EstimatedExpenseFormDialogContent = React.lazy(
|
||||
() => import('./EstimatedExpenseFormDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Estimate expense form dialog.
|
||||
* @returns
|
||||
*/
|
||||
function EstimatedExpenseFormDialog({
|
||||
dialogName,
|
||||
payload: { projectId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<EstimateExpenseFormDialogRoot
|
||||
name={dialogName}
|
||||
title={<T id={'estimated_expenses.dialog.label'} />}
|
||||
isOpen={isOpen}
|
||||
autoFocus={true}
|
||||
canEscapeKeyClose={true}
|
||||
style={{ width: '400px' }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<EstimatedExpenseFormDialogContent
|
||||
dialogName={dialogName}
|
||||
estimatedExpense={projectId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</EstimateExpenseFormDialogRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogRedux())(EstimatedExpenseFormDialog);
|
||||
|
||||
const EstimateExpenseFormDialogRoot = styled(Dialog)`
|
||||
.bp3-dialog-body {
|
||||
.bp3-form-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
label.bp3-label {
|
||||
margin-bottom: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bp3-dialog-footer {
|
||||
padding-top: 10px;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,79 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { FormattedMessage as T, FormattedHTMLMessage } from 'components';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import { AppToaster } from 'components';
|
||||
import { useDeleteProject } from '../../hooks';
|
||||
|
||||
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
|
||||
import withAlertActions from 'containers/Alert/withAlertActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Project delete alert.
|
||||
*/
|
||||
function ProjectDeleteAlert({
|
||||
name,
|
||||
|
||||
// #withAlertStoreConnect
|
||||
isOpen,
|
||||
payload: { projectId },
|
||||
|
||||
// #withAlertActions
|
||||
closeAlert,
|
||||
|
||||
// #withDrawerActions
|
||||
closeDrawer,
|
||||
}) {
|
||||
const { mutateAsync: deleteProjectMutate, isLoading } = useDeleteProject();
|
||||
|
||||
// handle cancel delete project alert.
|
||||
const handleCancelDeleteAlert = () => {
|
||||
closeAlert(name);
|
||||
};
|
||||
|
||||
// handleConfirm delete project
|
||||
const handleConfirmProjectDelete = () => {
|
||||
deleteProjectMutate(projectId)
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: intl.get('projects.alert.delete_message'),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(
|
||||
({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {},
|
||||
)
|
||||
.finally(() => {
|
||||
closeAlert(name);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
cancelButtonText={<T id={'cancel'} />}
|
||||
confirmButtonText={<T id={'delete'} />}
|
||||
icon="trash"
|
||||
intent={Intent.DANGER}
|
||||
isOpen={isOpen}
|
||||
onCancel={handleCancelDeleteAlert}
|
||||
onConfirm={handleConfirmProjectDelete}
|
||||
loading={isLoading}
|
||||
>
|
||||
<p>
|
||||
<FormattedHTMLMessage id={'projects.alert.once_delete_this_project'} />
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertStoreConnect(),
|
||||
withAlertActions,
|
||||
)(ProjectDeleteAlert);
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
const ProjectDeleteAlert = React.lazy(() => import('./ProjectDeleteAlert'));
|
||||
|
||||
/**
|
||||
* Project alerts.
|
||||
*/
|
||||
export default [{ name: 'project-delete', component: ProjectDeleteAlert }];
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
@@ -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; */
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DataTable } from 'components';
|
||||
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
|
||||
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
|
||||
import { TABLES } from 'common/tables';
|
||||
import { useMemorizedColumnsWidths } from 'hooks';
|
||||
import { ActionMenu } from './components';
|
||||
import { useProjectPurchasesColumns } from './hooks';
|
||||
import withSettings from '../../../../Settings/withSettings';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Project Purchases DataTable.
|
||||
* @returns
|
||||
*/
|
||||
function ProjectPurchasesTableRoot({
|
||||
// #withSettings
|
||||
purchasesTableSize,
|
||||
}) {
|
||||
// Retrieve purchases table columns.
|
||||
const columns = useProjectPurchasesColumns();
|
||||
|
||||
// Handle delete purchase.
|
||||
const handleDeletePurchase = () => {};
|
||||
|
||||
// Local storage memorizing columns widths.
|
||||
const [initialColumnsWidths, , handleColumnResizing] =
|
||||
useMemorizedColumnsWidths(TABLES.PURCHASES);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={[]}
|
||||
manualSortBy={true}
|
||||
selectionColumn={true}
|
||||
noInitialFetch={true}
|
||||
sticky={true}
|
||||
ContextMenu={ActionMenu}
|
||||
TableLoadingRenderer={TableSkeletonRows}
|
||||
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
||||
initialColumnsWidths={initialColumnsWidths}
|
||||
onColumnResizing={handleColumnResizing}
|
||||
size={purchasesTableSize}
|
||||
payload={{
|
||||
onDelete: handleDeletePurchase,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const ProjectPurchasesTable = compose(
|
||||
withSettings(({ purchasesSettings }) => ({
|
||||
purchasesTableSize: purchasesSettings?.tableSize,
|
||||
})),
|
||||
)(ProjectPurchasesTableRoot);
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Icon } from 'components';
|
||||
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
|
||||
import { safeCallback } from 'utils';
|
||||
|
||||
/**
|
||||
* Table actions cell.
|
||||
*/
|
||||
export function ActionMenu({ payload: { onDelete }, row: { original } }) {
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={intl.get('purchases.action.delete')}
|
||||
intent={Intent.DANGER}
|
||||
onClick={safeCallback(onDelete, original)}
|
||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useMemo } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import clsx from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import { FormatDateCell } from 'components';
|
||||
|
||||
export function useProjectPurchasesColumns() {
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'date',
|
||||
Header: intl.get('purchases.column.date'),
|
||||
accessor: 'date',
|
||||
Cell: FormatDateCell,
|
||||
width: 120,
|
||||
className: 'date',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
Header: intl.get('purchases.column.type'),
|
||||
accessor: 'type',
|
||||
width: 120,
|
||||
className: 'type',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'transaction_no',
|
||||
Header: intl.get('purchases.column.transaction_no'),
|
||||
accessor: 'transaction_no',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
id: 'due_date',
|
||||
Header: intl.get('purchases.column.due_date'),
|
||||
accessor: 'due_date',
|
||||
Cell: FormatDateCell,
|
||||
width: 120,
|
||||
className: 'due_date',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'balance',
|
||||
Header: intl.get('purchases.column.balance'),
|
||||
accessor: 'balance',
|
||||
width: 120,
|
||||
clickable: true,
|
||||
align: 'right',
|
||||
className: clsx(CLASSES.FONT_BOLD),
|
||||
},
|
||||
{
|
||||
id: 'total',
|
||||
Header: intl.get('purchases.column.total'),
|
||||
accessor: 'total',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
className: clsx(CLASSES.FONT_BOLD),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
Header: intl.get('purchases.column.status'),
|
||||
accessor: 'status',
|
||||
width: 120,
|
||||
className: 'status',
|
||||
clickable: true,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ProjectPurchasesTable } from './ProjectPurchasesTable';
|
||||
import { DashboardContentTable } from 'components';
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export default function ProjectPurchasesTableRoot() {
|
||||
return (
|
||||
<DashboardContentTable>
|
||||
<ProjectPurchasesTable />
|
||||
</DashboardContentTable>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import { DataTable } from 'components';
|
||||
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
|
||||
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
|
||||
import { TABLES } from 'common/tables';
|
||||
import { useMemorizedColumnsWidths } from 'hooks';
|
||||
import { ActionMenu } from './components';
|
||||
import { useProjectSalesColumns } from './hooks';
|
||||
import withSettings from '../../../../Settings/withSettings';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Porject sales datatable.
|
||||
* @returns
|
||||
*/
|
||||
function ProjectSalesTableRoot({
|
||||
// #withSettings
|
||||
salesTableSize,
|
||||
}) {
|
||||
// Retrieve project sales table columns.
|
||||
const columns = useProjectSalesColumns();
|
||||
|
||||
// Handle delete sale.
|
||||
const handleDeleteSale = () => {};
|
||||
|
||||
// Local storage memorizing columns widths.
|
||||
const [initialColumnsWidths, , handleColumnResizing] =
|
||||
useMemorizedColumnsWidths(TABLES.SALES);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={[]}
|
||||
manualSortBy={true}
|
||||
selectionColumn={true}
|
||||
noInitialFetch={true}
|
||||
sticky={true}
|
||||
ContextMenu={ActionMenu}
|
||||
TableLoadingRenderer={TableSkeletonRows}
|
||||
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
||||
initialColumnsWidths={initialColumnsWidths}
|
||||
onColumnResizing={handleColumnResizing}
|
||||
size={salesTableSize}
|
||||
payload={{
|
||||
onDelete: handleDeleteSale,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const ProjectSalesTable = compose(
|
||||
withSettings(({ salesSettings }) => ({
|
||||
salesTableSize: salesSettings?.tableSize,
|
||||
})),
|
||||
)(ProjectSalesTableRoot);
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import { FormatDateCell, Icon, FormattedMessage as T } from 'components';
|
||||
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
|
||||
import { safeCallback } from 'utils';
|
||||
|
||||
/**
|
||||
* Table actions cell.
|
||||
*/
|
||||
export function ActionMenu({ payload: { onDelete }, row: { original } }) {
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={intl.get('sales.action.delete')}
|
||||
intent={Intent.DANGER}
|
||||
onClick={safeCallback(onDelete, original)}
|
||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useMemo } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import clsx from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import { FormatDateCell } from 'components';
|
||||
|
||||
export function useProjectSalesColumns() {
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'date',
|
||||
Header: intl.get('sales.column.date'),
|
||||
accessor: 'date',
|
||||
Cell: FormatDateCell,
|
||||
width: 120,
|
||||
className: 'date',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
Header: intl.get('sales.column.type'),
|
||||
accessor: 'type',
|
||||
width: 120,
|
||||
className: 'type',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'transaction_no',
|
||||
Header: intl.get('sales.column.transaction_no'),
|
||||
accessor: 'transaction_no',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
id: 'due_date',
|
||||
Header: intl.get('sales.column.due_date'),
|
||||
accessor: 'due_date',
|
||||
Cell: FormatDateCell,
|
||||
width: 120,
|
||||
className: 'due_date',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'balance',
|
||||
Header: intl.get('sales.column.balance'),
|
||||
accessor: 'balance',
|
||||
width: 120,
|
||||
clickable: true,
|
||||
align: 'right',
|
||||
className: clsx(CLASSES.FONT_BOLD),
|
||||
},
|
||||
{
|
||||
id: 'total',
|
||||
Header: intl.get('sales.column.total'),
|
||||
accessor: 'total',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
className: clsx(CLASSES.FONT_BOLD),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
Header: intl.get('sales.column.status'),
|
||||
accessor: 'status',
|
||||
width: 120,
|
||||
className: 'status',
|
||||
clickable: true,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ProjectSalesTable } from './ProjectSalesTable';
|
||||
import { DashboardContentTable } from 'components';
|
||||
|
||||
/**
|
||||
* Project Sales Table.
|
||||
* @returns
|
||||
*/
|
||||
export default function ProjectSalesTableRoot() {
|
||||
return (
|
||||
<ProjectSalesContentTable>
|
||||
<ProjectSalesTable />
|
||||
</ProjectSalesContentTable>
|
||||
);
|
||||
}
|
||||
|
||||
const ProjectSalesContentTable = styled(DashboardContentTable)``;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { AvatarCell, TimesheetAccessor } from './components';
|
||||
|
||||
/**
|
||||
* Retrieve project timesheet list columns.
|
||||
*/
|
||||
export function useProjectTimesheetColumns() {
|
||||
return React.useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'avatar',
|
||||
Header: '',
|
||||
Cell: AvatarCell,
|
||||
className: 'avatar',
|
||||
width: 45,
|
||||
disableResizing: true,
|
||||
disableSortBy: true,
|
||||
clickable: true,
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
Header: 'Header',
|
||||
accessor: TimesheetAccessor,
|
||||
width: 100,
|
||||
className: 'name',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
Header: '',
|
||||
accessor: 'duration',
|
||||
width: 100,
|
||||
className: 'duration',
|
||||
align: 'right',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ProjectTimesheetsTable } from './ProjectTimesheetsTable';
|
||||
import { ProjectTimesheetsHeader } from './ProjectTimesheetsHeader';
|
||||
|
||||
/**
|
||||
* Project Timesheets.
|
||||
* @returns
|
||||
*/
|
||||
export default function ProjectTimeSheets() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ProjectTimesheetsHeader />
|
||||
<ProjectTimesheetTableCard>
|
||||
<ProjectTimesheetsTable />
|
||||
</ProjectTimesheetTableCard>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const ProjectTimesheetTableCard = styled.div`
|
||||
margin: 22px 32px;
|
||||
border: 1px solid #c8cad0;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
`;
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ProjectTransactionsSelect';
|
||||
export * from './FinancialSection';
|
||||
37
src/containers/Projects/containers/ProjectDetails/index.tsx
Normal file
37
src/containers/Projects/containers/ProjectDetails/index.tsx
Normal 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);
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as Yup from 'yup';
|
||||
import intl from 'react-intl-universal';
|
||||
import { DATATYPES_LENGTH } from 'common/dataTypes';
|
||||
|
||||
const Schema = Yup.object().shape({
|
||||
expenseName: Yup.string().label(
|
||||
intl.get('project_expense.schema.label.expense_name'),
|
||||
),
|
||||
estimatedExpense: Yup.number().label(
|
||||
intl.get('project_expense.schema.label.estimated_expense'),
|
||||
),
|
||||
expemseDate: Yup.date(),
|
||||
expenseQuantity: Yup.number().label(
|
||||
intl.get('project_expense.schema.label.quantity'),
|
||||
),
|
||||
expenseUnitPrice: Yup.number().label(
|
||||
intl.get('project_expense.schema.label.unitPrice'),
|
||||
),
|
||||
expenseTotal: Yup.number(),
|
||||
expenseCharge: Yup.string(),
|
||||
});
|
||||
|
||||
export const CreateProjectExpenseFormSchema = Schema;
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Formik } from 'formik';
|
||||
import { AppToaster } from 'components';
|
||||
import { CreateProjectExpenseFormSchema } from './ProjectExpenseForm.schema';
|
||||
import ProjectExpenseFormContent from './ProjectExpenseFormContent';
|
||||
import { useProjectExpenseFormContext } from './ProjectExpenseFormProvider';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
const defaultInitialValues = {
|
||||
expenseName: '',
|
||||
estimatedExpense: '',
|
||||
expemseDate: moment(new Date()).format('YYYY-MM-DD'),
|
||||
expenseUnitPrice: '',
|
||||
expenseQuantity: 1,
|
||||
expenseCharge: '% markup',
|
||||
percentage: '',
|
||||
expenseTotal: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Project expense form.
|
||||
* @returns
|
||||
*/
|
||||
function ProjectExpenseForm({
|
||||
//#withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
const initialValues = {
|
||||
...defaultInitialValues,
|
||||
};
|
||||
|
||||
// Handles the form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
const form = {};
|
||||
|
||||
// Handle request response success.
|
||||
const onSuccess = (response) => {
|
||||
AppToaster.show({});
|
||||
};
|
||||
|
||||
// Handle request response errors.
|
||||
const onError = ({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
setSubmitting(false);
|
||||
};
|
||||
};
|
||||
return (
|
||||
<Formik
|
||||
validationSchema={CreateProjectExpenseFormSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
component={ProjectExpenseFormContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogActions)(ProjectExpenseForm);
|
||||
@@ -0,0 +1,55 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Classes, ControlGroup } from '@blueprintjs/core';
|
||||
import { FFormGroup, FInputGroup, Choose } from 'components';
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
function PercentageFormField() {
|
||||
return (
|
||||
<FFormGroup
|
||||
label={intl.get('expenses.dialog.percentage')}
|
||||
name={'percentage'}
|
||||
>
|
||||
<FInputGroup name="percentage" />
|
||||
</FFormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomPirceField() {
|
||||
return (
|
||||
<ControlGroup className={Classes.FILL}>
|
||||
<FFormGroup
|
||||
name={'expenseUnitPrice'}
|
||||
label={intl.get('expenses.dialog.unit_price')}
|
||||
>
|
||||
<FInputGroup name="expenseUnitPrice" />
|
||||
</FFormGroup>
|
||||
<FFormGroup
|
||||
name={'expenseTotal'}
|
||||
label={intl.get('expenses.dialog.total')}
|
||||
>
|
||||
<FInputGroup name="expenseTotal" />
|
||||
</FFormGroup>
|
||||
</ControlGroup>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expense form charge fields.
|
||||
* @returns
|
||||
*/
|
||||
export default function ExpenseFormChargeFields() {
|
||||
const { values } = useFormikContext();
|
||||
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={values.expenseCharge === 'markup'}>
|
||||
<PercentageFormField />
|
||||
</Choose.When>
|
||||
<Choose.When condition={values.expenseCharge === 'custom_pirce'}>
|
||||
<CustomPirceField />
|
||||
</Choose.When>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Form } from 'formik';
|
||||
import ProjectExpenseFormFields from './ProjectExpenseFormFields';
|
||||
import ProjectExpneseFormFloatingActions from './ProjectExpneseFormFloatingActions';
|
||||
|
||||
/**
|
||||
* Expense form content.
|
||||
* @returns
|
||||
*/
|
||||
export default function ProjectExpenseFormContent() {
|
||||
return (
|
||||
<Form>
|
||||
<ProjectExpenseFormFields />
|
||||
<ProjectExpneseFormFloatingActions />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { ProjectExpenseFormProvider } from './ProjectExpenseFormProvider';
|
||||
import ProjectExpenseForm from './ProjectExpenseForm';
|
||||
|
||||
/**
|
||||
* Project expense form dialog content.
|
||||
* @returns
|
||||
*/
|
||||
export default function ProjectExpenseFormDialogContent({
|
||||
// #ownProps
|
||||
dialogName,
|
||||
expense,
|
||||
}) {
|
||||
return (
|
||||
<ProjectExpenseFormProvider dialogName={dialogName} expenseId={expense}>
|
||||
<ProjectExpenseForm />
|
||||
</ProjectExpenseFormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Classes, Position, ControlGroup } from '@blueprintjs/core';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
FFormGroup,
|
||||
FInputGroup,
|
||||
FDateInput,
|
||||
FormattedMessage as T,
|
||||
} from 'components';
|
||||
import { ExpenseSelect, FInputGroupComponent } from '../../components';
|
||||
import ExpenseFormChargeFields from './ProjectExpenseFormChargeFields';
|
||||
import { momentFormatter } from 'utils';
|
||||
import { useProjectExpenseFormContext } from './ProjectExpenseFormProvider';
|
||||
import { ChangeTypesSelect } from '../../components';
|
||||
import { expenseChargeOption } from 'containers/Projects/containers/common/modalChargeOptions';
|
||||
|
||||
/**
|
||||
* Project expense form fields.
|
||||
* @returns
|
||||
*/
|
||||
export default function ProjectExpenseFormFields() {
|
||||
return (
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
{/*------------ Expense Name -----------*/}
|
||||
<FFormGroup
|
||||
label={intl.get('project_expense.dialog.expense_name')}
|
||||
name={'expenseName'}
|
||||
>
|
||||
<FInputGroup name="expenseName" />
|
||||
</FFormGroup>
|
||||
{/*------------ Track to Expense -----------*/}
|
||||
<FFormGroup
|
||||
name={'estimatedExpense'}
|
||||
label={intl.get('project_expense.dialog.track_expense')}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
<ExpenseSelect
|
||||
name={'estimatedExpense'}
|
||||
popoverProps={{ minimal: true }}
|
||||
expenses={[{ id: 1, name: 'Expense 1' }]}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
{/*------------ Extimated Date -----------*/}
|
||||
<FFormGroup
|
||||
label={intl.get('project_expense.dialog.expense_date')}
|
||||
name={'expemseDate'}
|
||||
className={classNames(CLASSES.FILL, 'form-group--date')}
|
||||
>
|
||||
<FDateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
name="expemseDate"
|
||||
formatDate={(date) => date.toLocaleString()}
|
||||
popoverProps={{
|
||||
position: Position.BOTTOM,
|
||||
minimal: true,
|
||||
}}
|
||||
/>
|
||||
</FFormGroup>
|
||||
{/*------------ Quantity -----------*/}
|
||||
<FFormGroup
|
||||
label={intl.get('project_expense.dialog.quantity')}
|
||||
name={'expenseQuantity'}
|
||||
>
|
||||
<FInputGroupComponent name="expenseQuantity" />
|
||||
</FFormGroup>
|
||||
|
||||
<MetaLineLabel>
|
||||
<T id={'project_expense.dialog.cost_to_you'} />
|
||||
</MetaLineLabel>
|
||||
{/*------------ Unit Price -----------*/}
|
||||
<ControlGroup className={Classes.FILL}>
|
||||
<FFormGroup
|
||||
name={'unitPrice'}
|
||||
label={intl.get('project_expense.dialog.unit_price')}
|
||||
>
|
||||
<FInputGroupComponent name="expenseUnitPrice" />
|
||||
</FFormGroup>
|
||||
<FFormGroup
|
||||
name={'expenseTotal'}
|
||||
label={intl.get('project_expense.dialog.expense_total')}
|
||||
>
|
||||
<FInputGroup name="expenseTotal" />
|
||||
</FFormGroup>
|
||||
</ControlGroup>
|
||||
|
||||
<MetaLineLabel>
|
||||
<T id={'project_expense.dialog.what_you_ll_charge'} />
|
||||
</MetaLineLabel>
|
||||
{/*------------ Charge -----------*/}
|
||||
<FFormGroup
|
||||
name={'expenseCharge'}
|
||||
label={<T id={'project_expense.dialog.charge'} />}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
<ChangeTypesSelect
|
||||
name="expenseCharge"
|
||||
items={expenseChargeOption}
|
||||
popoverProps={{ minimal: true }}
|
||||
filterable={false}
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
{/*------------ Charge Fields -----------*/}
|
||||
<ExpenseFormChargeFields />
|
||||
|
||||
{/*------------ Total -----------*/}
|
||||
<ExpenseTotalBase>
|
||||
<ExpenseTotalLabel>
|
||||
<T id={'project_expense.dialog.total'} />
|
||||
</ExpenseTotalLabel>
|
||||
<ExpenseTotal>0.00</ExpenseTotal>
|
||||
</ExpenseTotalBase>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MetaLineLabel = styled.div`
|
||||
font-size: 14px;
|
||||
line-height: 1.5rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const ExpenseTotalBase = styled.div`
|
||||
display: block;
|
||||
text-align: right;
|
||||
`;
|
||||
|
||||
const ExpenseTotalLabel = styled.div`
|
||||
font-size: 14px;
|
||||
line-height: 1.5rem;
|
||||
opacity: 0.75;
|
||||
`;
|
||||
|
||||
const ExpenseTotal = styled.div`
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding-left: 14px;
|
||||
line-height: 2rem;
|
||||
`;
|
||||
@@ -0,0 +1,30 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import { DialogContent } from 'components';
|
||||
|
||||
const ProjectExpenseFormContext = React.createContext();
|
||||
|
||||
/**
|
||||
* Project expense form provider.
|
||||
* @returns
|
||||
*/
|
||||
function ProjectExpenseFormProvider({
|
||||
//#OwnProps
|
||||
dialogName,
|
||||
expenseId,
|
||||
...props
|
||||
}) {
|
||||
// state provider.
|
||||
const provider = {
|
||||
dialogName,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<ProjectExpenseFormContext.Provider value={provider} {...props} />
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
const useProjectExpenseFormContext = () => React.useContext(ProjectExpenseFormContext);
|
||||
export { ProjectExpenseFormProvider, useProjectExpenseFormContext };
|
||||
@@ -0,0 +1,44 @@
|
||||
//@ts-nocheck
|
||||
import React from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Intent, Button, Classes } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import { useProjectExpenseFormContext } from './ProjectExpenseFormProvider';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
function ProjectExpneseFormFloatingActions({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
// Formik context.
|
||||
const { isSubmitting } = useFormikContext();
|
||||
|
||||
// expense form dialog context.
|
||||
const { dialogName } = useProjectExpenseFormContext();
|
||||
|
||||
// Handle close button click.
|
||||
const handleCancelBtnClick = () => {
|
||||
closeDialog(dialogName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
|
||||
<T id={'cancel'} />
|
||||
</Button>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
loading={isSubmitting}
|
||||
style={{ minWidth: '75px' }}
|
||||
type="submit"
|
||||
>
|
||||
{<T id={'save'} />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogActions)(ProjectExpneseFormFloatingActions);
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
|
||||
import withDialogRedux from 'components/DialogReduxConnect';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const ProjectExpenseFormeDialogContent = React.lazy(
|
||||
() => import('./ProjectExpenseFormDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Project expense form dialog.
|
||||
* @returns
|
||||
*/
|
||||
function ProjectExpenseFormDialog({
|
||||
dialogName,
|
||||
payload: { projectId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<ProjectExpenseFormDialogRoot
|
||||
name={dialogName}
|
||||
title={<T id={'project_expense.dialog.label'} />}
|
||||
isOpen={isOpen}
|
||||
autoFocus={true}
|
||||
canEscapeKeyClose={true}
|
||||
style={{ width: '400px' }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<ProjectExpenseFormeDialogContent
|
||||
dialogName={dialogName}
|
||||
expense={projectId}
|
||||
/>
|
||||
</DialogSuspense>
|
||||
</ProjectExpenseFormDialogRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withDialogRedux())(ProjectExpenseFormDialog);
|
||||
|
||||
const ProjectExpenseFormDialogRoot = styled(Dialog)`
|
||||
.bp3-dialog-body {
|
||||
.bp3-form-group {
|
||||
margin-bottom: 15px;
|
||||
|
||||
label.bp3-label {
|
||||
margin-bottom: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bp3-dialog-footer {
|
||||
padding-top: 10px;
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Form } from 'formik';
|
||||
import ProjectTaskFormFields from './ProjectTaskFormFields';
|
||||
import ProjectTaskFormFloatingActions from './ProjectTaskFormFloatingActions';
|
||||
|
||||
/**
|
||||
* Task form content.
|
||||
* @returns
|
||||
*/
|
||||
export default function TaskFormContent() {
|
||||
return (
|
||||
<Form>
|
||||
<ProjectTaskFormFields />
|
||||
<ProjectTaskFormFloatingActions />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ProjectTaskFormProvider } from './ProjectTaskFormProvider';
|
||||
import ProjectTaskForm from './ProjectTaskForm';
|
||||
|
||||
/**
|
||||
* Project task form dialog content.
|
||||
*/
|
||||
export default function ProjectTaskFormDialogContent({
|
||||
// #ownProps
|
||||
dialogName,
|
||||
task,
|
||||
}) {
|
||||
return (
|
||||
<ProjectTaskFormProvider taskId={task} dialogName={dialogName}>
|
||||
<ProjectTaskForm />
|
||||
</ProjectTaskFormProvider>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
|
||||
import withDialogRedux from 'components/DialogReduxConnect';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const ProjectTaskFormDialogContent = React.lazy(
|
||||
() => import('./ProjectTaskFormDialogContent'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Project task form dialog.
|
||||
* @returns
|
||||
*/
|
||||
function ProjectTaskFormDialog({
|
||||
dialogName,
|
||||
payload: { taskId = null },
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
title={intl.get('project_task.dialog.new_task')}
|
||||
isOpen={isOpen}
|
||||
autoFocus={true}
|
||||
canEscapeKeyClose={true}
|
||||
style={{ width: '500px' }}
|
||||
>
|
||||
<DialogSuspense>
|
||||
<ProjectTaskFormDialogContent dialogName={dialogName} task={taskId} />
|
||||
</DialogSuspense>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default compose(withDialogRedux())(ProjectTaskFormDialog);
|
||||
@@ -0,0 +1,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;
|
||||
@@ -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);
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Form } from 'formik';
|
||||
import ProjectTimeEntryFormFields from './ProjectTimeEntryFormFields';
|
||||
import ProjectTimeEntryFormFloatingActions from './ProjectTimeEntryFormFloatingActions';
|
||||
|
||||
/**
|
||||
* Time entry form content.
|
||||
* @returns
|
||||
*/
|
||||
export default function TimeEntryFormContent() {
|
||||
return (
|
||||
<Form>
|
||||
<ProjectTimeEntryFormFields />
|
||||
<ProjectTimeEntryFormFloatingActions />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { ProjectTimeEntryFormProvider } from './ProjectTimeEntryFormProvider';
|
||||
import ProjectTimeEntryForm from './ProjectTimeEntryForm';
|
||||
|
||||
/**
|
||||
* Project time entry form dialog content.
|
||||
* @returns {ReactNode}
|
||||
*/
|
||||
export default function ProjectTimeEntryFormDialogContent({
|
||||
// #ownProps
|
||||
dialogName,
|
||||
project,
|
||||
timeEntry,
|
||||
}) {
|
||||
return (
|
||||
<ProjectTimeEntryFormProvider
|
||||
projectId={project}
|
||||
timeEntryId={timeEntry}
|
||||
dialogName={dialogName}
|
||||
>
|
||||
<ProjectTimeEntryForm />
|
||||
</ProjectTimeEntryFormProvider>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -0,0 +1,17 @@
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export const taskChargeOptions = [
|
||||
{ name: intl.get('task.dialog.hourly_rate'), value: 'hourly_rate' },
|
||||
{ name: intl.get('task.dialog.fixed_price'), value: 'fixed_price' },
|
||||
{ name: intl.get('task.dialog.non_chargeable'), value: 'non_chargeable' },
|
||||
];
|
||||
|
||||
export const expenseChargeOption = [
|
||||
{
|
||||
name: intl.get('expenses.dialog.markup'),
|
||||
value: 'markup',
|
||||
},
|
||||
{ name: intl.get('expenses.dialog.pass_cost_on'), value: 'pass_cost_on' },
|
||||
{ name: intl.get('expenses.dialog.custom_pirce'), value: 'custom_pirce' },
|
||||
{ name: intl.get('expenses.dialog.non_chargeable'), value: 'non_chargeable' },
|
||||
];
|
||||
125
src/containers/Projects/hooks/index.ts
Normal file
125
src/containers/Projects/hooks/index.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
//@ts-nocheck
|
||||
import { useQueryClient, useMutation } from 'react-query';
|
||||
import { useRequestQuery } from 'hooks/useQueryRequest';
|
||||
import { transformPagination } from 'utils';
|
||||
import useApiRequest from 'hooks/useRequest';
|
||||
import t from './type';
|
||||
|
||||
// Common invalidate queries.
|
||||
const commonInvalidateQueries = (queryClient) => {
|
||||
// Invalidate projects.
|
||||
queryClient.invalidateQueries(t.PROJECT);
|
||||
queryClient.invalidateQueries(t.PROJECTS);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
* @param props
|
||||
*/
|
||||
export function useCreateProject(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation((values) => apiRequest.post('projects', values), {
|
||||
onSuccess: (res, values) => {
|
||||
// Common invalidate queries.
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the given project
|
||||
* @param props
|
||||
* @returns
|
||||
*/
|
||||
export function useEditProject(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation(
|
||||
([id, values]) => apiRequest.post(`/projects/${id}`, values),
|
||||
{
|
||||
onSuccess: (res, [id, values]) => {
|
||||
// Invalidate specific project.
|
||||
queryClient.invalidateQueries([t.PROJECT, id]);
|
||||
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given project
|
||||
* @param props
|
||||
*/
|
||||
export function useDeleteProject(props) {
|
||||
const queryClient = useQueryClient();
|
||||
const apiRequest = useApiRequest();
|
||||
|
||||
return useMutation((id) => apiRequest.delete(`projects/${id}`), {
|
||||
onSuccess: (res, id) => {
|
||||
// Invalidate specific project.
|
||||
queryClient.invalidateQueries([t.PROJECT, id]);
|
||||
|
||||
// Common invalidate queries.
|
||||
commonInvalidateQueries(queryClient);
|
||||
},
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the projects details.
|
||||
* @param projectId The project id
|
||||
* @param props
|
||||
* @param requestProps
|
||||
* @returns
|
||||
*/
|
||||
export function useProject(projectId, props, requestProps) {
|
||||
return useRequestQuery(
|
||||
[t.PROJECT, projectId],
|
||||
{ method: 'get', url: `projects/${projectId}`, ...requestProps },
|
||||
{
|
||||
select: (res) => res.data.project,
|
||||
defaultData: {},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const transformProjects = (res) => ({
|
||||
projects: res.data.projects,
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieve projects list with pagination meta.
|
||||
* @param query
|
||||
* @param props
|
||||
*/
|
||||
export function useProjects(query, props) {
|
||||
return useRequestQuery(
|
||||
[t.PROJECTS, query],
|
||||
{ method: 'get', url: 'projects', params: query },
|
||||
{
|
||||
select: transformProjects,
|
||||
defaultData: {
|
||||
projects: [],
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function useRefreshInvoices() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return {
|
||||
refresh: () => {
|
||||
queryClient.invalidateQueries(t.PROJECTS);
|
||||
},
|
||||
};
|
||||
}
|
||||
11
src/containers/Projects/hooks/type.ts
Normal file
11
src/containers/Projects/hooks/type.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const CUSTOMERS = {
|
||||
CUSTOMERS: 'CUSTOMERS',
|
||||
CUSTOMER: 'CUSTOMER',
|
||||
};
|
||||
|
||||
const PROJECTS = {
|
||||
PROJECT: 'PROJECT',
|
||||
PROJECTS: 'PROJECTS',
|
||||
};
|
||||
|
||||
export default { ...PROJECTS, ...CUSTOMERS };
|
||||
0
src/containers/Projects/index.ts
Normal file
0
src/containers/Projects/index.ts
Normal 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;
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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: `/`,
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
14
src/store/Project/projects.actions.ts
Normal file
14
src/store/Project/projects.actions.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
33
src/store/Project/projects.reducer.ts
Normal file
33
src/store/Project/projects.reducer.ts
Normal 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);
|
||||
24
src/store/Project/projects.selectors.ts
Normal file
24
src/store/Project/projects.selectors.ts
Normal 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
Reference in New Issue
Block a user