diff --git a/src/common/tables.js b/src/common/tables.js
index 59c50dff7..419b0e9bb 100644
--- a/src/common/tables.js
+++ b/src/common/tables.js
@@ -16,7 +16,8 @@ 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',
};
export const TABLE_SIZE = {
diff --git a/src/components/DialogsContainer.js b/src/components/DialogsContainer.js
index 8ddff3496..8d1a0c56c 100644
--- a/src/components/DialogsContainer.js
+++ b/src/components/DialogsContainer.js
@@ -40,6 +40,8 @@ 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 TaskFormDialog from '../containers/Projects/containers/TaskFormDialog';
/**
* Dialogs container.
@@ -90,6 +92,8 @@ export default function DialogsContainer() {
+
+
);
}
diff --git a/src/components/Forms/BlueprintFormik.js b/src/components/Forms/BlueprintFormik.js
index a82afbf3c..497049ad4 100644
--- a/src/components/Forms/BlueprintFormik.js
+++ b/src/components/Forms/BlueprintFormik.js
@@ -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,
};
diff --git a/src/config/sidebarMenu.js b/src/config/sidebarMenu.js
index b11315670..9f4155005 100644
--- a/src/config/sidebarMenu.js
+++ b/src/config/sidebarMenu.js
@@ -538,6 +538,27 @@ 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,
+ },
+ ],
+ },
+ ],
+ },
// ---------------
// # Reports
// ---------------
diff --git a/src/containers/Dashboard/Sidebar/interfaces.ts b/src/containers/Dashboard/Sidebar/interfaces.ts
index b3f7eaa3a..76e6a75b1 100644
--- a/src/containers/Dashboard/Sidebar/interfaces.ts
+++ b/src/containers/Dashboard/Sidebar/interfaces.ts
@@ -69,6 +69,7 @@ export enum ISidebarMenuOverlayIds {
Contacts = 'Contacts',
Cashflow = 'Cashflow',
Expenses = 'Expenses',
+ Projects = 'Projects',
}
export enum ISidebarSubscriptionAbility {
diff --git a/src/containers/Dialogs/TaskDialog/components/index.ts b/src/containers/Dialogs/TaskDialog/components/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/containers/Projects/containers/ProjectFormDialog/ProjectForm.schema.tsx b/src/containers/Projects/containers/ProjectFormDialog/ProjectForm.schema.tsx
new file mode 100644
index 000000000..530fec56b
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectFormDialog/ProjectForm.schema.tsx
@@ -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({
+ contact: Yup.string().label(intl.get('project.schema.label.contact')),
+ projectName: Yup.string()
+ .label(intl.get('project.schema.label.project_name'))
+ .required(),
+ projectDeadline: Yup.date()
+ .label(intl.get('project.schema.label.deadline'))
+ .required(),
+ projectState: Yup.boolean().label(
+ intl.get('project.schema.label.project_state'),
+ ),
+ projectCost: Yup.number().label(
+ intl.get('project.schema.label.project_cost'),
+ ),
+});
+
+export const CreateProjectFormSchema = Schema;
diff --git a/src/containers/Projects/containers/ProjectFormDialog/ProjectForm.tsx b/src/containers/Projects/containers/ProjectFormDialog/ProjectForm.tsx
new file mode 100644
index 000000000..115e8a0ff
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectFormDialog/ProjectForm.tsx
@@ -0,0 +1,68 @@
+// @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 ProjectFormContent from './ProjectFormContent';
+import { CreateProjectFormSchema } from './ProjectForm.schema';
+import { useProjectFormContext } from './ProjectFormProvider';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+
+import { compose } from 'utils';
+
+const defaultInitialValues = {
+ contact: '',
+ projectName: '',
+ projectDeadline: moment(new Date()).format('YYYY-MM-DD'),
+ projectState: true,
+ projectCost: '',
+};
+
+/**
+ * Project form
+ * @returns
+ */
+function ProjectForm({
+ // #withDialogActions
+ closeDialog,
+}) {
+ // project form dialog context.
+ const { dialogName } = useProjectFormContext();
+
+ // 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 (
+
+ );
+}
+
+export default compose(withDialogActions)(ProjectForm);
diff --git a/src/containers/Projects/containers/ProjectFormDialog/ProjectFormContent.tsx b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormContent.tsx
new file mode 100644
index 000000000..0bcbed687
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormContent.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/containers/Projects/containers/ProjectFormDialog/ProjectFormDialogContent.tsx b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormDialogContent.tsx
new file mode 100644
index 000000000..63921866e
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormDialogContent.tsx
@@ -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 (
+
+
+
+ );
+}
diff --git a/src/containers/Projects/containers/ProjectFormDialog/ProjectFormFields.tsx b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormFields.tsx
new file mode 100644
index 000000000..7047ae048
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormFields.tsx
@@ -0,0 +1,107 @@
+// @ts-nocheck
+import React from 'react';
+import intl from 'react-intl-universal';
+
+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,
+ CustomerSelectField,
+} from 'components';
+import {
+ inputIntent,
+ momentFormatter,
+ tansformDateValue,
+ handleDateChange,
+} from 'utils';
+import { useProjectFormContext } from './ProjectFormProvider';
+
+/**
+ * Project form fields.
+ * @returns
+ */
+function ProjectFormFields() {
+ const { customers } = useProjectFormContext();
+
+ return (
+
+ {/*------------ Contact -----------*/}
+
+ {({ form, field: { value }, meta: { error, touched } }) => (
+
+ {
+ form.setFieldValue('contact', customer.id);
+ }}
+ allowCreate={true}
+ popoverFill={true}
+ />
+
+ )}
+
+ {/*------------ Project Name -----------*/}
+
+
+
+ {/*------------ DeadLine -----------*/}
+
+ date.toLocaleString()}
+ popoverProps={{
+ position: Position.BOTTOM,
+ minimal: true,
+ }}
+ />
+
+
+ {/*------------ CheckBox -----------*/}
+
+
+
+ {/*------------ Cost Estimate -----------*/}
+
+
+
+
+
+
+
+ );
+}
+
+export default ProjectFormFields;
diff --git a/src/containers/Projects/containers/ProjectFormDialog/ProjectFormFloatingActions.tsx b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormFloatingActions.tsx
new file mode 100644
index 000000000..57b798d35
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormFloatingActions.tsx
@@ -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,
+}) {
+ // project form dialog context.
+ const { dialogName } = useProjectFormContext();
+
+ // Formik context.
+ const { isSubmitting } = useFormikContext();
+
+ // Handle close button click.
+ const handleCancelBtnClick = () => {
+ closeDialog(dialogName);
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withDialogActions)(ProjectFormFloatingActions);
diff --git a/src/containers/Projects/containers/ProjectFormDialog/ProjectFormProvider.tsx b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormProvider.tsx
new file mode 100644
index 000000000..3c68c865a
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectFormDialog/ProjectFormProvider.tsx
@@ -0,0 +1,39 @@
+// @ts-nocheck
+import React from 'react';
+import { useCustomers } from 'hooks/query';
+import { DialogContent } from 'components';
+
+const ProjectFormContext = React.createContext();
+
+/**
+ * Project form provider.
+ * @returns
+ */
+function ProjectFormProvider({
+ // #ownProps
+ dialogName,
+ projectId,
+ ...props
+}) {
+ // Handle fetch customers data table or list
+ const {
+ data: { customers },
+ isLoading: isCustomersLoading,
+ } = useCustomers({ page_size: 10000 });
+
+ // State provider.
+ const provider = {
+ customers,
+ dialogName,
+ };
+
+ return (
+
+
+
+ );
+}
+
+const useProjectFormContext = () => React.useContext(ProjectFormContext);
+
+export { ProjectFormProvider, useProjectFormContext };
diff --git a/src/containers/Projects/containers/ProjectFormDialog/index.tsx b/src/containers/Projects/containers/ProjectFormDialog/index.tsx
new file mode 100644
index 000000000..c9d035d86
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectFormDialog/index.tsx
@@ -0,0 +1,50 @@
+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 }, isOpen }) {
+ return (
+ }
+ isOpen={isOpen}
+ autoFocus={true}
+ canEscapeKeyClose={true}
+ style={{ width: '400px' }}
+ >
+
+
+
+
+ );
+}
+
+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;
+ }
+ }
+`;
diff --git a/src/containers/Projects/containers/ProjectsLanding/ProjectsActionsBar.tsx b/src/containers/Projects/containers/ProjectsLanding/ProjectsActionsBar.tsx
new file mode 100644
index 000000000..c1a9d2c12
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsLanding/ProjectsActionsBar.tsx
@@ -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 (
+
+
+ }
+ views={[]}
+ onChange={handleTabChange}
+ />
+
+ }
+ text={'New Project'}
+ onClick={handleNewProjectBtnClick}
+ />
+ {/* AdvancedFilterPopover */}
+
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+
+
+
+
+
+ }
+ onClick={handleRefreshBtnClick}
+ />
+
+
+ );
+}
+
+export default compose(
+ withDialogActions,
+ withProjectsActions,
+ withSettingsActions,
+ withProjects(({ projectsTableState }) => ({
+ projectsFilterRoles: projectsTableState?.filterRoles,
+ })),
+ withSettings(({ projectSettings }) => ({
+ projectsTableSize: projectSettings?.tableSize,
+ })),
+)(ProjectsActionsBar);
diff --git a/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx b/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx
new file mode 100644
index 000000000..ea14c783e
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsLanding/ProjectsDataTable.tsx
@@ -0,0 +1,101 @@
+import React from 'react';
+import { DataTable } from 'components';
+import { TABLES } from 'common/tables';
+import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
+import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
+import { useProjectsListContext } from './ProjectsListProvider';
+import { useMemorizedColumnsWidths } from 'hooks';
+import { useProjectsListColumns, ActionsMenu } from './components';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import withProjectsActions from './withProjectsActions';
+import withSettings from '../../../Settings/withSettings';
+
+import { compose } from 'utils';
+
+const projects = [
+ {
+ id: 1,
+ name: 'Project 1',
+ description: 'Project 1 description',
+ status: 'Active',
+ },
+ {
+ id: 2,
+ name: 'Project 2',
+ description: 'Project 2 description',
+ status: 'Active',
+ },
+ {
+ id: 3,
+ name: 'Project 3',
+ description: 'Project 3 description',
+ status: 'Active',
+ },
+];
+
+/**
+ * Projects list datatable.
+ * @returns
+ */
+function ProjectsDataTable({
+ // #withDial
+ openDialog,
+
+ // #withSettings
+ projectsTableSize,
+}) {
+ // Retrieve projects table columns.
+ const columns = useProjectsListColumns();
+
+ // Handle cell click.
+ const handleCellClick = (cell, event) => {};
+
+ // Handle edit project.
+ const handleEditProject = (project) => {
+ openDialog('project-form', {
+ projectId: project.id,
+ });
+ };
+
+ // Handle new task button click.
+ const handleNewTaskButtonClick = () => {
+ openDialog('task-form');
+ };
+
+ // Local storage memorizing columns widths.
+ const [initialColumnsWidths, , handleColumnResizing] =
+ useMemorizedColumnsWidths(TABLES.PROJECTS);
+
+ return (
+
+ );
+}
+
+export default compose(
+ withDialogActions,
+ withProjectsActions,
+ withSettings(({ projectSettings }) => ({
+ projectsTableSize: projectSettings?.tableSize,
+ })),
+)(ProjectsDataTable);
diff --git a/src/containers/Projects/containers/ProjectsLanding/ProjectsList.tsx b/src/containers/Projects/containers/ProjectsLanding/ProjectsList.tsx
new file mode 100644
index 000000000..77ad204b0
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsLanding/ProjectsList.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withProjects(({ projectsTableState, projectsTableStateChanged }) => ({
+ projectsTableState,
+ projectsTableStateChanged,
+ })),
+ withProjectsActions,
+)(ProjectsList);
diff --git a/src/containers/Projects/containers/ProjectsLanding/ProjectsListProvider.tsx b/src/containers/Projects/containers/ProjectsLanding/ProjectsListProvider.tsx
new file mode 100644
index 000000000..a87f2d1f4
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsLanding/ProjectsListProvider.tsx
@@ -0,0 +1,34 @@
+//@ts-nocheck
+import React from 'react';
+import { useResourceViews, useResourceMeta } from 'hooks/query';
+import DashboardInsider from '../../../../components/Dashboard/DashboardInsider';
+
+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');
+
+ // provider payload.
+ const provider = {
+ projectsViews,
+ };
+
+ return (
+
+
+
+ );
+}
+
+const useProjectsListContext = () => React.useContext(ProjectsListContext);
+
+export { ProjectsListProvider, useProjectsListContext };
diff --git a/src/containers/Projects/containers/ProjectsLanding/ProjectsViewTabs.tsx b/src/containers/Projects/containers/ProjectsLanding/ProjectsViewTabs.tsx
new file mode 100644
index 000000000..30b6e70dd
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsLanding/ProjectsViewTabs.tsx
@@ -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 (
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withProjects(({ projectsTableState }) => ({
+ projectsCurrentView: projectsTableState?.viewSlug,
+ })),
+ withProjectsActions,
+)(ProjectsViewTabs);
diff --git a/src/containers/Projects/containers/ProjectsLanding/components/index.tsx b/src/containers/Projects/containers/ProjectsLanding/components/index.tsx
new file mode 100644
index 000000000..1a630b8b4
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsLanding/components/index.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+
+import { Menu, MenuDivider, MenuItem, Intent } from '@blueprintjs/core';
+
+import { Icon } from 'components';
+import { safeCallback } from 'utils';
+
+/**
+ * Table actions cell.
+ */
+export const ActionsMenu = ({
+ row: { original },
+ payload: { onEdit, onDelete, onViewDetails, onNewTask },
+}) => (
+
+);
+
+/**
+ * Retrieve projects list columns columns.
+ */
+export const useProjectsListColumns = () => {
+ return React.useMemo(
+ () => [
+ {
+ id: 'name',
+ Header: 'Project Name',
+ accessor: 'name',
+ width: 100,
+ className: 'name',
+ clickable: true,
+ },
+ ],
+ [],
+ );
+};
diff --git a/src/containers/Projects/containers/ProjectsLanding/withProjects.tsx b/src/containers/Projects/containers/ProjectsLanding/withProjects.tsx
new file mode 100644
index 000000000..35b73c42f
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsLanding/withProjects.tsx
@@ -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);
+};
diff --git a/src/containers/Projects/containers/ProjectsLanding/withProjectsActions.tsx b/src/containers/Projects/containers/ProjectsLanding/withProjectsActions.tsx
new file mode 100644
index 000000000..d74590f07
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsLanding/withProjectsActions.tsx
@@ -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);
diff --git a/src/containers/Projects/containers/TaskFormDialog/TaskForm.schema.tsx b/src/containers/Projects/containers/TaskFormDialog/TaskForm.schema.tsx
new file mode 100644
index 000000000..85b5954ab
--- /dev/null
+++ b/src/containers/Projects/containers/TaskFormDialog/TaskForm.schema.tsx
@@ -0,0 +1,14 @@
+import * as Yup from 'yup';
+import intl from 'react-intl-universal';
+import { DATATYPES_LENGTH } from 'common/dataTypes';
+
+const Schema = Yup.object().shape({
+ taksName: Yup.string()
+ .label(intl.get('task.schema.label.task_name'))
+ .required(),
+ taskHouse: Yup.string().label(intl.get('task.schema.label.task_house')),
+ change: Yup.string().label(intl.get('task.schema.label.charge')).required(),
+ amount: Yup.number().label(intl.get('task.schema.label.amount')),
+});
+
+export const CreateTaskFormSchema = Schema;
diff --git a/src/containers/Projects/containers/TaskFormDialog/TaskForm.tsx b/src/containers/Projects/containers/TaskFormDialog/TaskForm.tsx
new file mode 100644
index 000000000..2cc6fbd59
--- /dev/null
+++ b/src/containers/Projects/containers/TaskFormDialog/TaskForm.tsx
@@ -0,0 +1,63 @@
+//@ts-nocheck
+import React from 'react';
+import { Formik } from 'formik';
+import { CreateTaskFormSchema } from './TaskForm.schema';
+import { useTaskFormContext } from './TaskFormProvider';
+import { AppToaster } from 'components';
+import TaskFormContent from './TaskFormContent';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+
+import { compose } from 'utils';
+
+const defaultInitialValues = {
+ taksName: '',
+ taskHouse: '00:00',
+ change: 'Hourly Rate',
+ changeAmount: '100000000',
+ amount: '',
+};
+
+/**
+ * Task form.
+ * @returns
+ */
+function TaskForm({
+ // #withDialogActions
+ closeDialog,
+}) {
+ // task form dialog context.
+ const { dialogName } = useTaskFormContext();
+
+ // 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 (
+
+ );
+}
+
+export default compose(withDialogActions)(TaskForm);
diff --git a/src/containers/Projects/containers/TaskFormDialog/TaskFormContent.tsx b/src/containers/Projects/containers/TaskFormDialog/TaskFormContent.tsx
new file mode 100644
index 000000000..1d1f371f5
--- /dev/null
+++ b/src/containers/Projects/containers/TaskFormDialog/TaskFormContent.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Form } from 'formik';
+import TaskFormFields from './TaskFormFields';
+import TaskFormFloatingActions from './TaskFormFloatingActions';
+
+/**
+ * Task form content.
+ * @returns
+ */
+export default function TaskFormContent() {
+ return (
+
+ );
+}
diff --git a/src/containers/Projects/containers/TaskFormDialog/TaskFormDialogContent.tsx b/src/containers/Projects/containers/TaskFormDialog/TaskFormDialogContent.tsx
new file mode 100644
index 000000000..441356e65
--- /dev/null
+++ b/src/containers/Projects/containers/TaskFormDialog/TaskFormDialogContent.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { TaskFormProvider } from './TaskFormProvider';
+import TaskForm from './TaskForm';
+
+/**
+ * Task form dialog content.
+ */
+export default function TaskFormDialogContent({
+ // #ownProps
+ dialogName,
+ task,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Projects/containers/TaskFormDialog/TaskFormFields.tsx b/src/containers/Projects/containers/TaskFormDialog/TaskFormFields.tsx
new file mode 100644
index 000000000..5a053ac6a
--- /dev/null
+++ b/src/containers/Projects/containers/TaskFormDialog/TaskFormFields.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import styled from 'styled-components';
+import { Classes, ControlGroup } from '@blueprintjs/core';
+import {
+ FFormGroup,
+ FInputGroup,
+ Col,
+ Row,
+ FormattedMessage as T,
+} from 'components';
+
+/**
+ * Task form fields.
+ * @returns
+ */
+function TaskFormFields() {
+ return (
+
+ {/*------------ Task Name -----------*/}
+ } name={'task_name'}>
+
+
+ {/*------------ Estimated Hours -----------*/}
+
+
+ }
+ name={'taskHouse'}
+ >
+
+
+
+ {/*------------ Charge -----------*/}
+
+ } name={'Charge'}>
+
+
+
+
+
+
+
+ {/*------------ Estimated Amount -----------*/}
+
+
+
+ $100000
+
+
+
+ );
+}
+
+export default TaskFormFields;
+
+const EstimatedAmountBase = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ font-size: 12px;
+ /* opacity: 0.7; */
+`;
+
+const EstimatedAmountContent = styled.span`
+ background-color: #fffdf5;
+ padding: 0.1rem 0;
+`;
+
+const EstimateAmount = styled.span`
+ font-size: 13px;
+ font-weight: 700;
+ margin-left: 10px;
+`;
diff --git a/src/containers/Projects/containers/TaskFormDialog/TaskFormFloatingActions.tsx b/src/containers/Projects/containers/TaskFormDialog/TaskFormFloatingActions.tsx
new file mode 100644
index 000000000..c9c30a6a9
--- /dev/null
+++ b/src/containers/Projects/containers/TaskFormDialog/TaskFormFloatingActions.tsx
@@ -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 { useTaskFormContext } from './TaskFormProvider';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import { compose } from 'utils';
+
+/**
+ * Task form floating actions.
+ * @returns
+ */
+function TaskFormFloatingActions({
+ // #withDialogActions
+ closeDialog,
+}) {
+ // Formik context.
+ const { isSubmitting } = useFormikContext();
+
+ // Task form dialog context.
+ const { dialogName } = useTaskFormContext();
+
+ // Handle close button click.
+ const handleCancelBtnClick = () => {
+ closeDialog(dialogName);
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withDialogActions)(TaskFormFloatingActions);
diff --git a/src/containers/Projects/containers/TaskFormDialog/TaskFormProvider.tsx b/src/containers/Projects/containers/TaskFormDialog/TaskFormProvider.tsx
new file mode 100644
index 000000000..ea9bf5be3
--- /dev/null
+++ b/src/containers/Projects/containers/TaskFormDialog/TaskFormProvider.tsx
@@ -0,0 +1,31 @@
+//@ts-nocheck
+import React from 'react';
+import { DialogContent } from 'components';
+
+const TaskFormContext = React.createContext();
+
+/**
+ * Task form provider.
+ * @returns
+ */
+function TaskFormProvider({
+ // #ownProps
+ dialogName,
+ taskId,
+ ...props
+}) {
+ // State provider.
+ const provider = {
+ dialogName,
+ };
+
+ return (
+
+
+
+ );
+}
+
+const useTaskFormContext = () => React.useContext(TaskFormContext);
+
+export { TaskFormProvider, useTaskFormContext };
diff --git a/src/containers/Projects/containers/TaskFormDialog/index.tsx b/src/containers/Projects/containers/TaskFormDialog/index.tsx
new file mode 100644
index 000000000..d57287c46
--- /dev/null
+++ b/src/containers/Projects/containers/TaskFormDialog/index.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import styled from 'styled-components';
+import intl from 'react-intl-universal';
+import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
+import withDialogRedux from 'components/DialogReduxConnect';
+import { compose } from 'utils';
+
+const TaskFormDialogContent = React.lazy(
+ () => import('./TaskFormDialogContent'),
+);
+
+/**
+ * Task form dialog.
+ * @returns
+ */
+function TaskFormDialog({ dialogName, payload: { taskId = null }, isOpen }) {
+ return (
+
+ );
+}
+export default compose(withDialogRedux())(TaskFormDialog);
+
+const TaskFormDialogRoot = styled(Dialog)``;
diff --git a/src/containers/Projects/index.ts b/src/containers/Projects/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/containers/Settings/withSettings.js b/src/containers/Settings/withSettings.js
index 08e7fd6dc..e1c1aa7db 100644
--- a/src/containers/Settings/withSettings.js
+++ b/src/containers/Settings/withSettings.js
@@ -22,6 +22,7 @@ export default (mapState) => {
creditNoteSettings: state.settings.data.creditNote,
vendorsCreditNoteSetting: state.settings.data.vendorCredit,
warehouseTransferSettings: state.settings.data.warehouseTransfers,
+ projectSettings:state.settings.data.projects
};
return mapState ? mapState(mapped, state, props) : mapped;
};
diff --git a/src/lang/en/index.json b/src/lang/en/index.json
index 6f35aaa18..fea33284a 100644
--- a/src/lang/en/index.json
+++ b/src/lang/en/index.json
@@ -2044,5 +2044,30 @@
"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.label.contact": "Contact",
+ "projects.label.project_name": "Project Name",
+ "projects.label.deadline": "Deadline",
+ "projects.label.calculator_expenses": "Calculator from tasks & estimated expenses",
+ "projects.label.cost_estimate": "Cost Estimate",
+ "projects.label.create": "Create",
+ "task.label.new_task": "New Task",
+ "task.label.task_name": "Task Name",
+ "task.label.estimated_hours": "Task Name",
+ "task.label.charge": "Charge",
+ "task.label.estimated_amount": "Estimated Amount",
+ "project.schema.label.contact": "Contact",
+ "project.schema.label.project_name": "Project name",
+ "project.schema.label.deadline": "Deadline",
+ "project.schema.label.project_state": "Project state",
+ "project.schema.label.project_cost": "Project cost",
+ "task.schema.label.task_name": "Task name",
+ "task.schema.label.task_house": "Task house",
+ "task.schema.label.charge": "Charge",
+ "task.schema.label.amount": "Amount"
}
\ No newline at end of file
diff --git a/src/routes/dashboard.js b/src/routes/dashboard.js
index 9283b492f..f5b5c2c12 100644
--- a/src/routes/dashboard.js
+++ b/src/routes/dashboard.js
@@ -969,6 +969,13 @@ export const getDashboardRoutes = () => [
),
pageTitle: intl.get('sidebar.transactions_locaking'),
},
+ {
+ path: '/projects',
+ component: lazy(() =>
+ import('../containers/Projects/containers/ProjectsLanding/ProjectsList'),
+ ),
+ pageTitle: intl.get('sidebar.projects'),
+ },
// Homepage
{
path: `/`,
diff --git a/src/store/Project/projects.actions.ts b/src/store/Project/projects.actions.ts
new file mode 100644
index 000000000..f39531d81
--- /dev/null
+++ b/src/store/Project/projects.actions.ts
@@ -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,
+ };
+};
diff --git a/src/store/Project/projects.reducer.ts b/src/store/Project/projects.reducer.ts
new file mode 100644
index 000000000..cd219035b
--- /dev/null
+++ b/src/store/Project/projects.reducer.ts
@@ -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);
diff --git a/src/store/Project/projects.selectors.ts b/src/store/Project/projects.selectors.ts
new file mode 100644
index 000000000..10ab19fab
--- /dev/null
+++ b/src/store/Project/projects.selectors.ts
@@ -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);
+ });
diff --git a/src/store/Project/projects.type.ts b/src/store/Project/projects.type.ts
new file mode 100644
index 000000000..8026b0cf6
--- /dev/null
+++ b/src/store/Project/projects.type.ts
@@ -0,0 +1,4 @@
+export default {
+ PROJECTS_TABLE_STATE_SET: 'PROJECTS/TABLE_STATE_SET',
+ PROJECTS_TABLE_STATE_RESET: 'PROJECTS/TABLE_STATE_RESET',
+};
diff --git a/src/store/reducers.js b/src/store/reducers.js
index 0223270ad..b89241911 100644
--- a/src/store/reducers.js
+++ b/src/store/reducers.js
@@ -35,6 +35,7 @@ import plans from './plans/plans.reducer';
import creditNotes from './CreditNote/creditNote.reducer';
import vendorCredit from './VendorCredit/VendorCredit.reducer';
import warehouseTransfers from './WarehouseTransfer/warehouseTransfer.reducer';
+import projects from './Project/projects.reducer';
const appReducer = combineReducers({
authentication,
@@ -70,6 +71,7 @@ const appReducer = combineReducers({
creditNotes,
vendorCredit,
warehouseTransfers,
+ projects,
});
// Reset the state of a redux store
diff --git a/src/store/settings/settings.reducer.js b/src/store/settings/settings.reducer.js
index ee140bf98..fb28d894f 100644
--- a/src/store/settings/settings.reducer.js
+++ b/src/store/settings/settings.reducer.js
@@ -61,6 +61,9 @@ const initialState = {
warehouseTransfer: {
tableSize: 'medium',
},
+ projects: {
+ tableSize: 'medium',
+ },
},
};
diff --git a/src/store/types.js b/src/store/types.js
index 079344984..602953034 100644
--- a/src/store/types.js
+++ b/src/store/types.js
@@ -31,6 +31,7 @@ import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.typ
import creditNote from './CreditNote/creditNote.type';
import vendorCredit from './VendorCredit/vendorCredit.type';
import WarehouseTransfer from './WarehouseTransfer/warehouseTransfer.type';
+import projects from './Project/projects.type'
import plans from './plans/plans.types';
export default {
@@ -68,4 +69,5 @@ export default {
...creditNote,
...vendorCredit,
...WarehouseTransfer,
+ ...projects
};