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 ( + + + + + + + + + ); +} + +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', };