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/Projects/components/index.tsx b/src/containers/Projects/components/index.tsx
new file mode 100644
index 000000000..14e71d607
--- /dev/null
+++ b/src/containers/Projects/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/ProjectsActionsBar.tsx b/src/containers/Projects/containers/ProjectsActionsBar.tsx
new file mode 100644
index 000000000..536e67c0c
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsActionsBar.tsx
@@ -0,0 +1,126 @@
+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,
+
+ // #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,
+ })),
+)(ProjectsActionsBar);
diff --git a/src/containers/Projects/containers/ProjectsDataTable.tsx b/src/containers/Projects/containers/ProjectsDataTable.tsx
new file mode 100644
index 000000000..d2f954a14
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsDataTable.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+
+import { DataTable } from 'components';
+
+import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
+import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
+
+import { useProjectsListContext } from './ProjectsListProvider';
+import { useProjectsListColumns, ActionsMenu } from '../components';
+
+import withDialogActions from 'containers/Dialog/withDialogActions';
+
+import { compose } from 'utils';
+
+const projects = [
+ {
+ id: 1,
+ name: 'Project 1',
+ description: 'Project 1 description',
+ status: 'Active',
+ },
+];
+
+/**
+ * Projects list datatable.
+ * @returns
+ */
+function ProjectsDataTable({
+ // #withDial
+ openDialog,
+}) {
+ // Retrieve projects table columns.
+ const columns = useProjectsListColumns();
+
+ // Handle cell click.
+ const handleCellClick = (cell, event) => {};
+
+ // Handle new task button click.
+ const handleNewTaskButtonClick = () => {
+ openDialog('task-form');
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withDialogActions)(ProjectsDataTable);
diff --git a/src/containers/Projects/containers/ProjectsList.tsx b/src/containers/Projects/containers/ProjectsList.tsx
new file mode 100644
index 000000000..9de165c9d
--- /dev/null
+++ b/src/containers/Projects/containers/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 {React.TSX}
+ */
+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/ProjectsListProvider.tsx b/src/containers/Projects/containers/ProjectsListProvider.tsx
new file mode 100644
index 000000000..6ec170f45
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsListProvider.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import DashboardInsider from '../../../components/Dashboard/DashboardInsider';
+
+
+const ProjectsListContext = React.createContext({});
+
+/**
+ * Projects list data provider.
+ * @returns
+ */
+function ProjectsListProvider({ query, tableStateChanged, ...props }) {
+ // provider payload.
+
+ const provider = {};
+
+ return (
+
+
+
+ );
+}
+
+const useProjectsListContext = () => React.useContext(ProjectsListContext);
+
+export { ProjectsListProvider, useProjectsListContext };
diff --git a/src/containers/Projects/containers/ProjectsViewTabs.tsx b/src/containers/Projects/containers/ProjectsViewTabs.tsx
new file mode 100644
index 000000000..f13cf4606
--- /dev/null
+++ b/src/containers/Projects/containers/ProjectsViewTabs.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
+
+import { DashboardViewsTabs } from 'components';
+
+import withProjects from './withProjects';
+import withProjectsActions from './withProjectsActions';
+
+import { compose, transfromViewsToTabs } from 'utils';
+
+/**
+ * Projects views tabs.
+ * @returns {React.TSX}
+ */
+function ProjectsViewTabs({
+ // #withProjects
+ projectsCurrentView,
+
+ // #withProjectsActions
+ setProjectsTableState,
+}) {
+ // Projects views.
+ const tabs = transfromViewsToTabs([]);
+
+ // Handle tab change.
+ const handleTabsChange = (viewSlug) => {
+ setProjectsTableState({ viewSlug: viewSlug || null });
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withProjects(({ projectsTableState }) => ({
+ projectsCurrentView: projectsTableState?.viewSlug,
+ })),
+ withProjectsActions,
+)();
diff --git a/src/containers/Projects/containers/withProjects.tsx b/src/containers/Projects/containers/withProjects.tsx
new file mode 100644
index 000000000..cf230357a
--- /dev/null
+++ b/src/containers/Projects/containers/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/withProjectsActions.tsx b/src/containers/Projects/containers/withProjectsActions.tsx
new file mode 100644
index 000000000..de0a47f46
--- /dev/null
+++ b/src/containers/Projects/containers/withProjectsActions.tsx
@@ -0,0 +1,14 @@
+import { connect } from 'react-redux';
+// import type { Dispatch } from '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/index.ts b/src/containers/Projects/index.ts
new file mode 100644
index 000000000..e69de29bb