From 5c3a7effc135dacd9c95d43a1fa1f5d615b171d5 Mon Sep 17 00:00:00 2001
From: elforjani13 <39470382+elforjani13@users.noreply.github.com>
Date: Mon, 22 Aug 2022 19:56:27 +0200
Subject: [PATCH] feat: add project profitability summary.
---
src/constants/sidebarMenu.tsx | 5 +
.../ProjectProfitabilitySummary.tsx | 77 ++++++++++
.../ProjectProfitabilitySummaryActionsBar.tsx | 131 ++++++++++++++++++
.../ProjectProfitabilitySummaryBody.tsx | 38 +++++
.../ProjectProfitabilitySummaryHeader.tsx | 114 +++++++++++++++
...ProfitabilitySummaryHeaderGeneralPanal.tsx | 48 +++++++
.../ProjectProfitabilitySummaryProvider.tsx | 54 ++++++++
.../ProjectProfitabilitySummaryTable.tsx | 67 +++++++++
.../components.tsx | 55 ++++++++
.../ProjectProfitabilitySummary/constants.ts | 31 +++++
.../dynamicColumns.tsx | 69 +++++++++
.../ProjectProfitabilitySummary/hooks.ts | 30 ++++
.../ProjectProfitabilitySummary/utils.tsx | 70 ++++++++++
.../withProjectProfitabilitySummary.tsx | 14 ++
...withProjectProfitabilitySummaryActions.tsx | 9 ++
.../components/ProjectMultiSelect.tsx | 75 ++++++++++
src/containers/Projects/components/index.ts | 1 +
src/hooks/query/types.tsx | 1 +
src/lang/en/index.json | 26 ++--
src/routes/dashboard.tsx | 12 ++
src/static/json/icons.tsx | 6 +
.../financialStatements.actions.tsx | 13 ++
.../financialStatements.reducer.tsx | 18 ++-
.../financialStatements.selectors.tsx | 11 ++
.../financialStatements.types.tsx | 1 +
25 files changed, 965 insertions(+), 11 deletions(-)
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummary.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryActionsBar.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryBody.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryHeader.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryHeaderGeneralPanal.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryProvider.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryTable.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/components.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/constants.ts
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/dynamicColumns.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/hooks.ts
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/utils.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/withProjectProfitabilitySummary.tsx
create mode 100644 src/containers/FinancialStatements/ProjectProfitabilitySummary/withProjectProfitabilitySummaryActions.tsx
create mode 100644 src/containers/Projects/components/ProjectMultiSelect.tsx
diff --git a/src/constants/sidebarMenu.tsx b/src/constants/sidebarMenu.tsx
index e41ad8375..60a618110 100644
--- a/src/constants/sidebarMenu.tsx
+++ b/src/constants/sidebarMenu.tsx
@@ -659,6 +659,11 @@ export const SidebarMenu = [
ability: ReportsAction.READ_AP_AGING_SUMMARY,
},
},
+ {
+ text: ,
+ href: '/financial-reports/project-profitability-summary',
+ type: ISidebarMenuItemType.Link,
+ },
],
},
{
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummary.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummary.tsx
new file mode 100644
index 000000000..0d7cf963f
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummary.tsx
@@ -0,0 +1,77 @@
+import React, { useEffect } from 'react';
+import moment from 'moment';
+
+import {
+ ProjectProfitabilitySummaryAlerts,
+ ProjectProfitabilitySummaryLoadingBar,
+} from './components';
+import { FinancialStatement, DashboardPageContent } from '@/components';
+
+import ProjectProfitabilitySummaryHeader from './ProjectProfitabilitySummaryHeader';
+import ProjectProfitabilitySummaryActionsBar from './ProjectProfitabilitySummaryActionsBar';
+import { ProjectProfitabilitySummaryBody } from './ProjectProfitabilitySummaryBody';
+import { ProjectProfitabilitySummaryProvider } from './ProjectProfitabilitySummaryProvider';
+import { useProjectProfitabilitySummaryQuery } from './utils';
+import withProjectProfitabilitySummaryActions from './withProjectProfitabilitySummaryActions';
+import { compose } from '@/utils';
+
+/**
+ * Project profitability summary.
+ * @returns {React.JSX}
+ */
+function ProjectProfitabilitySummary({
+ // #withProjectProfitabilitySummaryActions
+ toggleProjectProfitabilitySummaryFilterDrawer,
+}) {
+ // Project profitability summary query.
+ const { query, setLocationQuery } = useProjectProfitabilitySummaryQuery();
+
+ // Handle refetch project profitability summary filter changer.
+ const handleFilterSubmit = (filter) => {
+ const newFilter = {
+ ...filter,
+ fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
+ toDate: moment(filter.toDate).format('YYYY-MM-DD'),
+ };
+ setLocationQuery({ ...newFilter });
+ };
+ // Handle number format submit.
+ const handleNumberFormatSubmit = (values) => {
+ setLocationQuery({
+ ...query,
+ numberFormat: values,
+ });
+ };
+
+ useEffect(
+ () => () => {
+ toggleProjectProfitabilitySummaryFilterDrawer(false);
+ },
+ [toggleProjectProfitabilitySummaryFilterDrawer],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default compose(withProjectProfitabilitySummaryActions)(
+ ProjectProfitabilitySummary,
+);
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryActionsBar.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryActionsBar.tsx
new file mode 100644
index 000000000..185126e30
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryActionsBar.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import {
+ NavbarGroup,
+ NavbarDivider,
+ Button,
+ Classes,
+ Popover,
+ PopoverInteractionKind,
+ Position,
+} from '@blueprintjs/core';
+import { DashboardActionsBar, FormattedMessage as T, Icon } from '@/components';
+import classNames from 'classnames';
+
+import NumberFormatDropdown from '@/components/NumberFormatDropdown';
+
+import { compose, saveInvoke } from '@/utils';
+import { useProjectProfitabilitySummaryContext } from './ProjectProfitabilitySummaryProvider';
+import withProjectProfitabilitySummary from './withProjectProfitabilitySummary';
+import withProjectProfitabilitySummaryActions from './withProjectProfitabilitySummaryActions';
+
+/**
+ * Project profitability summary actions bar.
+ */
+function ProjectProfitabilitySummaryActionsBar({
+ // #withProjectProfitabilitySummary
+ isFilterDrawerOpen,
+
+ // #withProjectProfitabilitySummaryActions
+ toggleProjectProfitabilitySummaryFilterDrawer: toggleFilterDrawer,
+
+ // #ownProps
+ numberFormat,
+ onNumberFormatSubmit,
+}) {
+ const { isLoading, refetchProjectProfitabilitySummary } =
+ useProjectProfitabilitySummaryContext();
+
+ // Handle filter toggle click.
+ const handleFilterToggleClick = () => {
+ toggleFilterDrawer();
+ };
+
+ // Handle recalculate the report button.
+ const handleRecalcReport = () => {
+ refetchProjectProfitabilitySummary();
+ };
+
+ // Handle number format form submit.
+ const handleNumberFormatSubmit = (values) => {
+ saveInvoke(onNumberFormatSubmit, values);
+ };
+
+ return (
+
+
+ }
+ onClick={handleRecalcReport}
+ icon={}
+ />
+
+ }
+ text={
+ !isFilterDrawerOpen ? (
+
+ ) : (
+
+ )
+ }
+ onClick={handleFilterToggleClick}
+ active={isFilterDrawerOpen}
+ />
+
+
+ }
+ minimal={true}
+ interactionKind={PopoverInteractionKind.CLICK}
+ position={Position.BOTTOM_LEFT}
+ >
+ }
+ icon={}
+ />
+
+
+ }
+ icon={}
+ />
+
+
+
+
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+
+
+ );
+}
+
+export default compose(
+ withProjectProfitabilitySummary(
+ ({ projectProfitabilitySummaryDrawerFilter }) => ({
+ isFilterDrawerOpen: projectProfitabilitySummaryDrawerFilter,
+ }),
+ ),
+ withProjectProfitabilitySummaryActions,
+)(ProjectProfitabilitySummaryActionsBar);
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryBody.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryBody.tsx
new file mode 100644
index 000000000..fe966949b
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryBody.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+
+import ProjectProfitabilitySummaryTable from './ProjectProfitabilitySummaryTable';
+import { FinancialReportBody } from '../FinancialReportPage';
+import { FinancialSheetSkeleton } from '@/components/FinancialSheet';
+import { useProjectProfitabilitySummaryContext } from './ProjectProfitabilitySummaryProvider';
+
+import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
+
+import { compose } from '@/utils';
+
+/**
+ * Project profitability summary body JSX.
+ * @returns {JSX.Element}
+ */
+function ProjectProfitabilitySummaryBodyJSX({
+ // #withCurrentOrganization
+ organizationName,
+}) {
+ const { isProjectProfitabilitySummaryLoading } =
+ useProjectProfitabilitySummaryContext();
+
+ return (
+
+ {isProjectProfitabilitySummaryLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
+export const ProjectProfitabilitySummaryBody = compose(
+ withCurrentOrganization(({ organization }) => ({
+ organizationName: organization?.name,
+ })),
+)(ProjectProfitabilitySummaryBodyJSX);
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryHeader.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryHeader.tsx
new file mode 100644
index 000000000..867316819
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryHeader.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import styled from 'styled-components';
+import moment from 'moment';
+import { Formik, Form } from 'formik';
+import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
+import { FormattedMessage as T } from '@/components';
+
+import withProjectProfitabilitySummary from './withProjectProfitabilitySummary';
+import withProjectProfitabilitySummaryActions from './withProjectProfitabilitySummaryActions';
+
+import ProjectProfitabilitySummaryHeaderGeneralPanal from './ProjectProfitabilitySummaryHeaderGeneralPanal';
+import FinancialStatementHeader from '../FinancialStatementHeader';
+
+import {
+ getProjectProfitabilitySummaryValidationSchema,
+ getDefaultProjectProfitabilitySummaryQuery,
+} from './utils';
+import { compose, transformToForm } from '@/utils';
+
+/**
+ * Project profitability summary header.
+ */
+function ProjectProfitabilitySummaryHeader({
+ // #ownProps
+ onSubmitFilter,
+ pageFilter,
+
+ // #withProjectProfitabilitySummary
+ isFilterDrawerOpen,
+
+ // #withProjectProfitabilitySummaryActions
+ toggleProjectProfitabilitySummaryFilterDrawer: toggleFilterDrawer,
+}) {
+ // Filter form default values.
+ const defaultValues = getDefaultProjectProfitabilitySummaryQuery();
+
+ // Filter form initial values.
+ const initialValues = transformToForm(
+ {
+ ...pageFilter,
+ fromDate: moment(pageFilter.fromDate).toDate(),
+ toDate: moment(pageFilter.toDate).toDate(),
+ },
+ defaultValues,
+ );
+
+ // Validation schema.
+ const validationSchema = getProjectProfitabilitySummaryValidationSchema();
+
+ // Handle form submit.
+ const handleSubmit = (values, { setSubmitting }) => {
+ onSubmitFilter(values);
+ toggleFilterDrawer(false);
+ setSubmitting(false);
+ };
+
+ // Handle cancel button click.
+ const handleCancelClick = () => {
+ toggleFilterDrawer(false);
+ };
+
+ // Handle drawer close action.
+ const handleDrawerClose = () => {
+ toggleFilterDrawer(false);
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withProjectProfitabilitySummary(
+ ({ projectProfitabilitySummaryDrawerFilter }) => ({
+ isFilterDrawerOpen: projectProfitabilitySummaryDrawerFilter,
+ }),
+ ),
+ withProjectProfitabilitySummaryActions,
+)(ProjectProfitabilitySummaryHeader);
+
+const ProjectProfitabilityDrawerHeader = styled(FinancialStatementHeader)`
+ .bp3-drawer {
+ max-height: 520px;
+ }
+`;
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryHeaderGeneralPanal.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryHeaderGeneralPanal.tsx
new file mode 100644
index 000000000..5c43083a2
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryHeaderGeneralPanal.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+
+import { filterProjectProfitabilityOptions } from './constants';
+import { Classes } from '@blueprintjs/core';
+import { ProjectMultiSelect } from '@/containers/Projects/components';
+import { Row, Col, FFormGroup, FormattedMessage as T } from '@/components';
+
+import FinancialStatementDateRange from '../FinancialStatementDateRange';
+import FinancialStatementsFilter from '../FinancialStatementsFilter';
+import RadiosAccountingBasis from '../RadiosAccountingBasis';
+
+import { useProjectProfitabilitySummaryContext } from './ProjectProfitabilitySummaryProvider';
+
+/**
+ * Project profitability summary header - General panal.
+ */
+export default function ProjectProfitabilitySummaryHeaderGeneralPanal() {
+ const { projects } = useProjectProfitabilitySummaryContext();
+
+ return (
+
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ className={Classes.FILL}
+ >
+
+
+
+
+
+
+ );
+}
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryProvider.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryProvider.tsx
new file mode 100644
index 000000000..7c466a9ee
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryProvider.tsx
@@ -0,0 +1,54 @@
+import React, { createContext, useMemo, useContext } from 'react';
+
+import FinancialReportPage from '../FinancialReportPage';
+import { useProjectProfitabilitySummary } from './hooks';
+import { useProjects } from '@/containers/Projects/hooks';
+import { transformFilterFormToQuery } from '../common';
+
+const ProjectProfitabilitySummaryContext = createContext();
+
+function ProjectProfitabilitySummaryProvider({ filter, ...props }) {
+ // Transformes the given filter to query.
+ const query = useMemo(() => transformFilterFormToQuery(filter), [filter]);
+
+ // Handle fetching the items table based on the given query.
+ const {
+ data: projectProfitabilitySummary,
+ isFetching: isProjectProfitabilitySummaryFetching,
+ isLoading: isProjectProfitabilitySummaryLoading,
+ refetch: refetchProjectProfitabilitySummary,
+ } = useProjectProfitabilitySummary(query, { keepPreviousData: true });
+
+ // Fetch project list.
+ const {
+ data: { projects },
+ isLoading: isProjectsLoading,
+ } = useProjects();
+
+ const provider = {
+ projectProfitabilitySummary,
+ isProjectProfitabilitySummaryFetching,
+ isProjectProfitabilitySummaryLoading,
+ refetchProjectProfitabilitySummary,
+ projects,
+
+ query,
+ filter,
+ };
+ return (
+
+
+
+ );
+}
+
+const useProjectProfitabilitySummaryContext = () =>
+ useContext(ProjectProfitabilitySummaryContext);
+
+export {
+ ProjectProfitabilitySummaryProvider,
+ useProjectProfitabilitySummaryContext,
+};
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryTable.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryTable.tsx
new file mode 100644
index 000000000..1028612e8
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryTable.tsx
@@ -0,0 +1,67 @@
+import React, { useMemo } from 'react';
+import styled from 'styled-components';
+import intl from 'react-intl-universal';
+
+import { TableStyle } from '@/constants';
+import { ReportDataTable, FinancialSheet } from '@/components';
+import { useProjectProfitabilitySummaryContext } from './ProjectProfitabilitySummaryProvider';
+import { useProjectProfitabilitySummaryColumns } from './components';
+import { defaultExpanderReducer, tableRowTypesToClassnames } from '@/utils';
+
+/**
+ * Project profitability summary table.
+ */
+export default function ProjectProfitabilitySummaryTable({
+ // #ownProps
+ companyName,
+}) {
+ // Project profitability summary context.
+ const {
+ projectProfitabilitySummary: { tableRows },
+ query,
+ } = useProjectProfitabilitySummaryContext();
+
+ // Retrieve the database columns.
+ const tableColumns = useProjectProfitabilitySummaryColumns();
+
+ return (
+
+
+
+ );
+}
+
+const ProjectProfitabilitySummaryDataTable = styled(ReportDataTable)`
+ .table {
+ .tbody .tr {
+ .td {
+ border-bottom: 0;
+ padding-top: 0.32rem;
+ padding-bottom: 0.32rem;
+ }
+ .tr.row_type--total .td {
+ border-top: 1px solid #bbb;
+ font-weight: 500;
+ border-bottom: 3px double #000;
+ }
+
+ &:last-of-type .td {
+ border-bottom: 1px solid #bbb;
+ }
+ }
+ }
+`;
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/components.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/components.tsx
new file mode 100644
index 000000000..35c040204
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/components.tsx
@@ -0,0 +1,55 @@
+import React, { useMemo } from 'react';
+import { Button } from '@blueprintjs/core';
+
+import FinancialLoadingBar from '../FinancialLoadingBar';
+
+import { dynamicColumns } from './dynamicColumns';
+import { FinancialComputeAlert } from '../FinancialReportPage';
+import { FormattedMessage as T, Icon, If } from '@/components';
+import { useProjectProfitabilitySummaryContext } from './ProjectProfitabilitySummaryProvider';
+
+/**
+ * Project profitability summary alerts.
+ */
+export function ProjectProfitabilitySummaryAlerts() {
+ // Handle refetch the report sheet.
+ const handleRecalcReport = () => {};
+
+ // Can't display any error if the report is loading.
+ // if (isLoading) return null;
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+/**
+ * Project profitability summary loading bar.
+ */
+export function ProjectProfitabilitySummaryLoadingBar() {
+ return (
+
+
+
+ );
+}
+
+/**
+ * Retrieve Project profitability summary columns.
+ */
+export function useProjectProfitabilitySummaryColumns() {
+ // Balance sheet context.
+ const {
+ projectProfitabilitySummary: { columns, tableRows },
+ } = useProjectProfitabilitySummaryContext();
+
+ return useMemo(
+ () => dynamicColumns(columns, tableRows),
+ [tableRows, columns],
+ );
+}
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/constants.ts b/src/containers/FinancialStatements/ProjectProfitabilitySummary/constants.ts
new file mode 100644
index 000000000..a21975ff2
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/constants.ts
@@ -0,0 +1,31 @@
+import intl from 'react-intl-universal';
+
+export const filterProjectProfitabilityOptions = [
+ {
+ key: 'all-projects',
+ name: intl.get(
+ 'project_profitability_summary.filter_projects.all_projects',
+ ),
+ hint: intl.get(
+ 'project_profitability_summary.filter_projects.all_projects.hint',
+ ),
+ },
+ {
+ key: 'without-zero-balance',
+ name: intl.get(
+ 'project_profitability_summary.filter_projects.without_zero_balance',
+ ),
+ hint: intl.get(
+ 'project_profitability_summary.filter_projects.without_zero_balance.hint',
+ ),
+ },
+ {
+ key: 'with-transactions',
+ name: intl.get(
+ 'project_profitability_summary.filter_projects.with_transactions',
+ ),
+ hint: intl.get(
+ 'project_profitability_summary.filter_projects.with_transactions.hint',
+ ),
+ },
+];
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/dynamicColumns.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/dynamicColumns.tsx
new file mode 100644
index 000000000..0658b4b5c
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/dynamicColumns.tsx
@@ -0,0 +1,69 @@
+import * as R from 'ramda';
+
+import { getColumnWidth } from '@/utils';
+import { Align } from '@/constants';
+
+const characterColumn = R.curry((data, index, column) => ({
+ id: column.key,
+ key: column.key,
+ Header: column.label,
+ accessor: `cells[${index}].value`,
+ className: column.key,
+ width: getColumnWidth(data, `cells.${index}.key`, {
+ minWidth: 200,
+ magicSpacing: 10,
+ }),
+ disableSortBy: true,
+ textOverview: true,
+ sticky: Align.Left,
+}));
+
+const numericColumn = R.curry((data, index, column) => ({
+ id: column.key,
+ key: column.key,
+ Header: column.label,
+ accessor: `cells[${index}].value`,
+ className: column.key,
+ width: getColumnWidth(data, `cells.${index}.key`, {
+ minWidth: 130,
+ magicSpacing: 10,
+ }),
+ disableSortBy: true,
+ align: Align.Right,
+}));
+
+/**
+ * columns mapper.
+ */
+const columnsMapper = R.curry((data, index, column) => ({
+ id: column.key,
+ key: column.key,
+ Header: column.label,
+ accessor: `cells[${index}].value`,
+ className: column.key,
+ width: getColumnWidth(data, `cells.${index}.key`, {
+ minWidth: 130,
+ magicSpacing: 10,
+ }),
+ disableSortBy: true,
+ textOverview: true,
+}));
+
+/**
+ * project profitability summary columns mapper.
+ */
+export const dynamicColumns = (columns, data) => {
+ const mapper = (column, index) => {
+ return R.compose(
+ R.cond([
+ [R.pathEq(['key'], 'name'), characterColumn(data, index)],
+ [R.pathEq(['key'], 'customer_name'), characterColumn(data, index)],
+ [R.pathEq(['key'], 'income'), numericColumn(data, index)],
+ [R.pathEq(['key'], 'expenses'), numericColumn(data, index)],
+ [R.pathEq(['key'], 'profit'), numericColumn(data, index)],
+ [R.T, columnsMapper(data, index)],
+ ]),
+ )(column);
+ };
+ return columns.map(mapper);
+};
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/hooks.ts b/src/containers/FinancialStatements/ProjectProfitabilitySummary/hooks.ts
new file mode 100644
index 000000000..fcc37d0c3
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/hooks.ts
@@ -0,0 +1,30 @@
+import { useRequestQuery } from '@/hooks/useQueryRequest';
+import t from '@/hooks/query/types';
+
+/**
+ * Retrieve the profitability summary for the project
+ */
+export function useProjectProfitabilitySummary(query, props) {
+ return useRequestQuery(
+ [t.FINANCIAL_REPORT, t.PROJECT_PROFITABILITY_SUMMARY, query],
+ {
+ method: 'get',
+ url: '/financial_statements/project-profitability-summary',
+ params: query,
+ headers: {
+ Accept: 'application/json+table',
+ },
+ },
+ {
+ select: (res) => ({
+ columns: res.data.table.columns,
+ tableRows: res.data.table.data,
+ }),
+ defaultData: {
+ tableRows: [],
+ columns: [],
+ },
+ ...props,
+ },
+ );
+}
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/utils.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/utils.tsx
new file mode 100644
index 000000000..9ef23f633
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/utils.tsx
@@ -0,0 +1,70 @@
+import React, { useMemo } from 'react';
+import moment from 'moment';
+import { castArray } from 'lodash';
+
+import intl from 'react-intl-universal';
+import * as R from 'ramda';
+import * as Yup from 'yup';
+
+import { transformToForm } from '@/utils';
+import { useAppQueryString } from '@/hooks';
+
+/**
+ * Retrieves the project profitability validation schema.
+ */
+export const getProjectProfitabilitySummaryValidationSchema = () =>
+ Yup.object().shape({
+ dateRange: Yup.string().optional(),
+ fromDate: Yup.date().required().label(intl.get('fromDate')),
+ toDate: Yup.date()
+ .min(Yup.ref('fromDate'))
+ .required()
+ .label(intl.get('toDate')),
+ filterByOption: Yup.string(),
+ });
+
+/**
+ * Retrieves the project profitability summary default values.
+ */
+export const getDefaultProjectProfitabilitySummaryQuery = () => ({
+ fromDate: moment().startOf('year').format('YYYY-MM-DD'),
+ toDate: moment().endOf('year').format('YYYY-MM-DD'),
+ basis: 'cash',
+ filterByOption: 'without-zero-balance',
+ projectsIds: [],
+});
+
+/**
+ * Parses project profitability summary query.
+ */
+const parseProjectProfitabilityQuery = (locationQuery) => {
+ const defaultQuery = getDefaultProjectProfitabilitySummaryQuery();
+
+ const transformed = {
+ ...defaultQuery,
+ ...transformToForm(locationQuery, defaultQuery),
+ };
+ return {
+ ...transformed,
+ projectsIds: castArray(transformed.projectsIds),
+ };
+};
+
+/**
+ * Retrieves the project profitability summary query.
+ */
+export const useProjectProfitabilitySummaryQuery = () => {
+ // Retrieves location query.
+ const [locationQuery, setLocationQuery] = useAppQueryString();
+
+ // Merges the default filter query with location URL query.
+ const query = useMemo(
+ () => parseProjectProfitabilityQuery(locationQuery),
+ [locationQuery],
+ );
+ return {
+ query,
+ locationQuery,
+ setLocationQuery,
+ };
+};
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/withProjectProfitabilitySummary.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/withProjectProfitabilitySummary.tsx
new file mode 100644
index 000000000..fad00d438
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/withProjectProfitabilitySummary.tsx
@@ -0,0 +1,14 @@
+import { connect } from 'react-redux';
+import { getProjectProfitabilitySummaryFilterDrawer } from '@/store/financialStatement/financialStatements.selectors';
+
+export default (mapState) => {
+ const mapStateToProps = (state, props) => {
+ const mapped = {
+ projectProfitabilitySummaryDrawerFilter:
+ getProjectProfitabilitySummaryFilterDrawer(state),
+ };
+ return mapState ? mapState(mapped, state, props) : mapped;
+ };
+
+ return connect(mapStateToProps);
+};
diff --git a/src/containers/FinancialStatements/ProjectProfitabilitySummary/withProjectProfitabilitySummaryActions.tsx b/src/containers/FinancialStatements/ProjectProfitabilitySummary/withProjectProfitabilitySummaryActions.tsx
new file mode 100644
index 000000000..9818e25ff
--- /dev/null
+++ b/src/containers/FinancialStatements/ProjectProfitabilitySummary/withProjectProfitabilitySummaryActions.tsx
@@ -0,0 +1,9 @@
+import { connect } from 'react-redux';
+import { toggleProjectProfitabilitySummaryFilterDrawer } from '@/store/financialStatement/financialStatements.actions';
+
+const mapDispatchToProps = (dispatch) => ({
+ toggleProjectProfitabilitySummaryFilterDrawer: (toggle) =>
+ dispatch(toggleProjectProfitabilitySummaryFilterDrawer(toggle)),
+});
+
+export default connect(null, mapDispatchToProps);
diff --git a/src/containers/Projects/components/ProjectMultiSelect.tsx b/src/containers/Projects/components/ProjectMultiSelect.tsx
new file mode 100644
index 000000000..6ce0de269
--- /dev/null
+++ b/src/containers/Projects/components/ProjectMultiSelect.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+import { MenuItem } from '@blueprintjs/core';
+import { FMultiSelect } from '@/components';
+
+/**
+ *
+ * @param query
+ * @param project
+ * @param _index
+ * @param exactMatch
+ */
+const projectItemPredicate = (query, project, _index, exactMatch) => {
+ const normalizedTitle = project.name.toLowerCase();
+ const normalizedQuery = query.toLowerCase();
+
+ if (exactMatch) {
+ return normalizedTitle === normalizedQuery;
+ } else {
+ return `${project.name}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
+ }
+};
+
+/**
+ *
+ * @param project
+ * @param param1
+ * @param param2
+ * @returns
+ */
+const projectItemRenderer = (
+ project,
+ { handleClick, modifiers, query },
+ { isSelected },
+) => {
+ return (
+
+ );
+};
+
+const projectSelectProps = {
+ itemPredicate: projectItemPredicate,
+ itemRenderer: projectItemRenderer,
+ valueAccessor: (item) => item.id,
+ labelAccessor: (item) => item.name,
+ tagRenderer: (item) => item.name,
+};
+
+/**
+ * projects mulit select.
+ * @param param0
+ * @returns {JSX.Element}
+ */
+export function ProjectMultiSelect({
+ projects,
+
+ ...rest
+}) {
+ return (
+
+ );
+}
diff --git a/src/containers/Projects/components/index.ts b/src/containers/Projects/components/index.ts
index 3e29cc72e..93edd1b79 100644
--- a/src/containers/Projects/components/index.ts
+++ b/src/containers/Projects/components/index.ts
@@ -2,4 +2,5 @@ export * from './ExpenseSelect';
export * from './ChangeTypesSelect';
export * from './TaskSelect';
export * from './ProjectsSelect';
+export * from './ProjectMultiSelect'
export * from './FInputGroupComponent';
diff --git a/src/hooks/query/types.tsx b/src/hooks/query/types.tsx
index fad87425a..65eab819c 100644
--- a/src/hooks/query/types.tsx
+++ b/src/hooks/query/types.tsx
@@ -26,6 +26,7 @@ const FINANCIAL_REPORTS = {
TRANSACTIONS_BY_REFERENCE: 'TRANSACTIONS_BY_REFERENCE',
REALIZED_GAIN_OR_LOSS: 'REALIZED_GAIN_OR_LOSS',
UNREALIZED_GAIN_OR_LOSS: 'UNREALIZED_GAIN_OR_LOSS',
+ PROJECT_PROFITABILITY_SUMMARY: 'PROJECT_PROFITABILITY_SUMMARY',
};
const BILLS = {
diff --git a/src/lang/en/index.json b/src/lang/en/index.json
index badfae2a5..1a15872fc 100644
--- a/src/lang/en/index.json
+++ b/src/lang/en/index.json
@@ -2050,7 +2050,7 @@
"projects.action.new_task": "New Task",
"projects.action.delete_project": "Delete Project",
"projects.label.new_project": "New Project",
- "projects.label.new_time_entry":"New Time Entry",
+ "projects.label.new_time_entry": "New Time Entry",
"projects.dialog.contact": "Contact",
"projects.dialog.project_name": "Project Name",
"projects.dialog.deadline": "Deadline",
@@ -2065,7 +2065,7 @@
"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.alert.status_message": "The project has been edited successfully.",
- "projects.alert.are_you_sure_you_want":"Are you sure you want to edit this project?",
+ "projects.alert.are_you_sure_you_want": "Are you sure you want to edit this project?",
"projects.empty_status.title": "",
"projects.empty_status.description": "",
"projects.empty_status.action": "New Project",
@@ -2185,10 +2185,20 @@
"sales.column.total": "Total",
"sales.column.status": "Status",
"sales.action.delete": "Delete",
- "invoice.project_name.label":"Project Name",
- "estimate.project_name.label":"Project Name",
- "receipt.project_name.label":"Project Name",
- "bill.project_name.label":"Project Name",
- "payment_receive.project_name.label":"Project Name",
- "select_project": "Select Project"
+ "invoice.project_name.label": "Project Name",
+ "estimate.project_name.label": "Project Name",
+ "receipt.project_name.label": "Project Name",
+ "bill.project_name.label": "Project Name",
+ "payment_receive.project_name.label": "Project Name",
+ "select_project": "Select Project",
+ "projects_multi_select.label":"Projects",
+ "projects_multi_select.placeholder": "Filter by projects…",
+ "project_profitability_summary": "Project Profitability Summary",
+ "project_profitability_summary.filter_projects.all_projects": "All Projects",
+ "project_profitability_summary.filter_projects.all_projects.hint": "All project, include that onces have zero-balance.",
+ "project_profitability_summary.filter_projects.without_zero_balance": "Projects without zero balance",
+ "project_profitability_summary.filter_projects.without_zero_balance.hint": "Include projects that onces have transactions on the given date period only.",
+ "project_profitability_summary.filter_projects.with_transactions": "Projects with transactions",
+ "project_profitability_summary.filter_projects.with_transactions.hint": "Include projects that onces have transactions on the given date period only.",
+ "project_profitability_summary.filter_options.label": "Filter projects"
}
\ No newline at end of file
diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx
index e3047912f..2bb2e2049 100644
--- a/src/routes/dashboard.tsx
+++ b/src/routes/dashboard.tsx
@@ -435,6 +435,18 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
+ {
+ path: `/financial-reports/project-profitability-summary`,
+ component: lazy(
+ () =>
+ import('@/containers/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummary'),
+ ),
+ breadcrumb: intl.get('project_profitability_summary'),
+ pageTitle: intl.get('project_profitability_summary'),
+ backLink: true,
+ sidebarExpand: false,
+ subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
+ },
{
path: '/financial-reports',
component: lazy(
diff --git a/src/static/json/icons.tsx b/src/static/json/icons.tsx
index c9aaee2ed..45f774300 100644
--- a/src/static/json/icons.tsx
+++ b/src/static/json/icons.tsx
@@ -534,6 +534,12 @@ export default {
],
viewBox: '0 0 24 24',
},
+ 'case-16': {
+ path: [
+ 'M8 6V4q0-.825.588-1.413Q9.175 2 10 2h4q.825 0 1.413.587Q16 3.175 16 4v2h4q.825 0 1.413.588Q22 7.175 22 8v11q0 .825-.587 1.413Q20.825 21 20 21H4q-.825 0-1.412-.587Q2 19.825 2 19V8q0-.825.588-1.412Q3.175 6 4 6Zm2 0h4V4h-4Zm10 9h-5v2H9v-2H4v4h16Zm-9 0h2v-2h-2Zm-7-2h5v-2h6v2h5V8H4Zm8 1Z',
+ ],
+ viewBox: '0 0 24 24',
+ },
'more-13': {
path: [
'M1.5 3a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 10a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0-5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z',
diff --git a/src/store/financialStatement/financialStatements.actions.tsx b/src/store/financialStatement/financialStatements.actions.tsx
index 4e40aa802..92a6d7fd0 100644
--- a/src/store/financialStatement/financialStatements.actions.tsx
+++ b/src/store/financialStatement/financialStatements.actions.tsx
@@ -230,3 +230,16 @@ export function toggleUnrealizedGainOrLossFilterDrawer(toggle) {
},
};
}
+
+/**
+ * Toggle display of the project Profitability summary filter drawer.
+ * @param {boolean} toggle
+ */
+export function toggleProjectProfitabilitySummaryFilterDrawer(toggle) {
+ return {
+ type: `${t.PROJECT_PROFITABILITY_SUMMARY}/${t.DISPLAY_FILTER_DRAWER_TOGGLE}`,
+ payload: {
+ toggle,
+ },
+ };
+}
diff --git a/src/store/financialStatement/financialStatements.reducer.tsx b/src/store/financialStatement/financialStatements.reducer.tsx
index 789310180..097317c22 100644
--- a/src/store/financialStatement/financialStatements.reducer.tsx
+++ b/src/store/financialStatement/financialStatements.reducer.tsx
@@ -57,6 +57,9 @@ const initialState = {
unrealizedGainOrLoss: {
displayFilterDrawer: false,
},
+ projectProfitabilitySummary: {
+ dispalyFilterDrawer: false,
+ },
};
/**
@@ -108,7 +111,16 @@ export default createReducer(initialState, {
t.INVENTORY_ITEM_DETAILS,
'inventoryItemDetails',
),
- ...financialStatementFilterToggle(t.REALIZED_GAIN_OR_LOSS, 'realizedGainOrLoss'),
- ...financialStatementFilterToggle(t.UNREALIZED_GAIN_OR_LOSS, 'unrealizedGainOrLoss'),
-
+ ...financialStatementFilterToggle(
+ t.REALIZED_GAIN_OR_LOSS,
+ 'realizedGainOrLoss',
+ ),
+ ...financialStatementFilterToggle(
+ t.UNREALIZED_GAIN_OR_LOSS,
+ 'unrealizedGainOrLoss',
+ ),
+ ...financialStatementFilterToggle(
+ t.PROJECT_PROFITABILITY_SUMMARY,
+ 'projectProfitabilitySummary',
+ ),
});
diff --git a/src/store/financialStatement/financialStatements.selectors.tsx b/src/store/financialStatement/financialStatements.selectors.tsx
index a554ebb37..46ba3c0be 100644
--- a/src/store/financialStatement/financialStatements.selectors.tsx
+++ b/src/store/financialStatement/financialStatements.selectors.tsx
@@ -81,6 +81,10 @@ export const unrealizedGainOrLossFilterDrawerSelector = (state) => {
return filterDrawerByTypeSelector('unrealizedGainOrLoss')(state);
};
+export const projectProfitabilitySummaryFilterDrawerSelector = (state) => {
+ return filterDrawerByTypeSelector('projectProfitabilitySummary')(state);
+};
+
/**
* Retrieve balance sheet filter drawer.
*/
@@ -266,3 +270,10 @@ export const getUnrealizedGainOrLossFilterDrawer = createSelector(
return isOpen;
},
);
+
+export const getProjectProfitabilitySummaryFilterDrawer = createSelector(
+ projectProfitabilitySummaryFilterDrawerSelector,
+ (isOpen) => {
+ return isOpen;
+ },
+);
diff --git a/src/store/financialStatement/financialStatements.types.tsx b/src/store/financialStatement/financialStatements.types.tsx
index 98d37a1d5..628ec2893 100644
--- a/src/store/financialStatement/financialStatements.types.tsx
+++ b/src/store/financialStatement/financialStatements.types.tsx
@@ -16,6 +16,7 @@ export default {
VENDORS_TRANSACTIONS: 'VENDORS TRANSACTIONS',
CASH_FLOW_STATEMENT: 'CASH FLOW STATEMENT',
INVENTORY_ITEM_DETAILS: 'INVENTORY ITEM DETAILS',
+ PROJECT_PROFITABILITY_SUMMARY: 'PROJECT PROFITABILITY SUMMARY',
REALIZED_GAIN_OR_LOSS: 'REALIZED GAIN OR LOSS',
UNREALIZED_GAIN_OR_LOSS: 'UNREALIZED GAIN OR LOSS',
};