feat(projects): WIP projects service.

This commit is contained in:
a.bouhuolia
2022-10-02 21:33:23 +02:00
parent 900a237a52
commit 41db96d958
21 changed files with 1279 additions and 1252 deletions

2076
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -107,7 +107,11 @@
},
"proxy": "http://localhost:3000/",
"devDependencies": {
"@types/react-dom": "^16.9.16"
"@types/react-dom": "^16.9.16",
"react-error-overlay": "^6.0.9"
},
"resolutions": {
"react-error-overlay": "6.0.9"
},
"browserslist": {
"production": [

View File

@@ -583,6 +583,17 @@ export const SidebarMenu = [
},
],
},
{
text: <T id={'Reports'} />,
type: ISidebarMenuItemType.Group,
children: [
{
text: <T id={'project_profitability_summary'} />,
href: '/financial-reports/project-profitability-summary',
type: ISidebarMenuItemType.Link,
},
],
},
],
},
// ---------------
@@ -669,11 +680,6 @@ export const SidebarMenu = [
ability: ReportsAction.READ_AP_AGING_SUMMARY,
},
},
{
text: <T id={'project_profitability_summary'} />,
href: '/financial-reports/project-profitability-summary',
type: ISidebarMenuItemType.Link,
},
],
},
{

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
//@ts-nocheck
import React from 'react';
import { Overlay, OverlayProps } from '@blueprintjs/core';
import { Link } from 'react-router-dom';
@@ -19,7 +18,7 @@ export interface ISidebarOverlayProps {
export interface ISidebarOverlayItemProps {
text: string | JSX.Element;
href?: string;
href: string;
onClick?: any;
}
@@ -85,7 +84,7 @@ function SidebarOverlayItem({ item }: SidebarOverlayItemProps) {
) : //
item.type === ISidebarMenuItemType.Link ||
item.type === ISidebarMenuItemType.Dialog ? (
<SidebarOverlayItemLink text={item.text} onClick={item.onClick} />
<SidebarOverlayItemLink text={item.text} href={item.href} onClick={item.onClick} />
) : null;
}

View File

@@ -54,12 +54,11 @@ const ProjectProfitabilitySummaryDataTable = styled(ReportDataTable)`
padding-top: 0.32rem;
padding-bottom: 0.32rem;
}
.tr.row_type--total .td {
&.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;
}

View File

@@ -84,6 +84,7 @@ function ProjectDetailActionsBar({
transactions={projectTranslations}
onItemSelect={handleNewTransactionBtnClick}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'time-24'} iconSize={16} />}

View File

@@ -21,7 +21,6 @@ export default function ProjectDetailTabs() {
renderActiveTabPanelOnly={true}
defaultSelectedTabId={'tasks'}
>
<Tab id="overview" title={intl.get('project_details.label.overview')} />
<Tab
id="tasks"
title={intl.get('project_details.label.tasks')}

View File

@@ -1,15 +1,10 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import moment from 'moment';
import styled from 'styled-components';
import { Intent } from '@blueprintjs/core';
import { FormatDate } from '@/components';
import {
DetailFinancialCard,
DetailFinancialSection,
FinancialProgressBar,
FinancialCardText,
} from './components';
import { calculateStatus } from '@/utils';
import { useCalculateProject } from './utils';
@@ -18,20 +13,11 @@ import { useProjectDetailContext } from './ProjectDetailProvider';
/**
* Project details header.
* @returns
*/
export function ProjectDetailHeader() {
const { project } = useProjectDetailContext();
const { percentageOfInvoice, percentageOfExpense } = useCalculateProject();
// function getDiff() {
let start = moment(new Date());
let end = moment(project.deadline);
let duration = moment.duration(start.diff(end, 'days'));
console.log(duration, 'XX');
return (
<DetailFinancialSection>
<DetailFinancialCard
@@ -41,32 +27,25 @@ export function ProjectDetailHeader() {
<DetailFinancialCard
label={intl.get('project_details.label.invoiced')}
value={project.total_invoiced_formatted}
>
<FinancialCardText>
{intl.get('project_details.label.of_project_estimate', {
value: percentageOfInvoice,
})}
</FinancialCardText>
<FinancialProgressBar
intent={Intent.NONE}
value={calculateStatus(project.total_invoiced, project.cost_estimate)}
/>
</DetailFinancialCard>
description={intl.get('project_details.label.of_project_estimate', {
value: percentageOfInvoice,
})}
progressValue={calculateStatus(
project.total_invoiced,
project.cost_estimate,
)}
/>
<DetailFinancialCard
label={intl.get('project_details.label.time_expenses')}
value={project.total_expenses_formatted}
>
<FinancialCardText>
{intl.get('project_details.label.of_project_estimate', {
value: percentageOfExpense,
})}
</FinancialCardText>
<FinancialProgressBar
intent={Intent.NONE}
value={calculateStatus(project.total_expenses, project.cost_estimate)}
/>
</DetailFinancialCard>
description={intl.get('project_details.label.of_project_estimate', {
value: percentageOfExpense,
})}
progressValue={calculateStatus(
project.total_expenses,
project.cost_estimate,
)}
/>
<DetailFinancialCard
label={intl.get('project_details.label.to_be_invoiced')}
value={project.total_billable_formatted}
@@ -74,9 +53,8 @@ export function ProjectDetailHeader() {
<DetailFinancialCard
label={'Deadline'}
value={<FormatDate value={project.deadline_formatted} />}
>
<FinancialCardText>4 days to go</FinancialCardText>
</DetailFinancialCard>
description={'4 days to go'}
/>
</DetailFinancialSection>
);
}

View File

@@ -84,5 +84,21 @@ const ProjectTaksDataTable = styled(DataTable)`
display: none;
}
}
.tbody .tr .td{
padding-top: 0.7rem;
padding-bottom: 0.7rem;
&:first-of-type{
padding-left: 1rem;
}
&.td-actions{
padding-right: 1rem;
}
}
.tbody .tr:last-of-type .td {
border-bottom: 0;
}
}
`;

View File

@@ -3,7 +3,7 @@ import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Icon, If, Choose, FormattedMessage as T } from '@/components';
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
import { Menu, MenuItem, Intent, ProgressBar } from '@blueprintjs/core';
import { safeCallback } from '@/utils';
/**
@@ -70,25 +70,72 @@ export function TaskAccessor(task) {
);
}
const TaskRoot = styled.div`
margin-left: 12px;
export function TaskTimeAccessor(task) {
return (
<TaskTimeRoot>
<TaskTimeMinutesRoot>
<TaskTimeMinutes>00:00</TaskTimeMinutes>
<TaskTimeFull>17h 30m</TaskTimeFull>
</TaskTimeMinutesRoot>
<TaskProgressBar
animate={false}
stripes={false}
intent={Intent.NONE}
value={100}
/>
</TaskTimeRoot>
);
}
const TaskTimeRoot = styled.div`
display: flex;
align-items: center;
flex-direction: row-reverse;
`;
const TaskTimeMinutesRoot = styled.div`
margin-left: 20px;
display: flex;
flex-direction: column;
font-size: 14px;
text-align: right;
`;
const TaskTimeMinutes = styled.div``;
const TaskTimeFull = styled.div`
font-size: 12px;
color: #5b5c62;
`;
const TaskProgressBar = styled(ProgressBar)`
&.bp3-progress-bar {
display: block;
flex-shrink: 0;
height: 4px;
max-width: 150px;
&,
.bp3-progress-meter {
border-radius: 4px;
}
}
`;
const TaskRoot = styled.div``;
const TaskHeader = styled.div`
display: flex;
align-items: baseline;
flex-flow: wrap;
`;
const TaskTitle = styled.span`
font-weight: 500;
line-height: 1.5rem;
font-weight: 600;
`;
const TaskContent = styled.div`
display: block;
white-space: nowrap;
font-size: 13px;
opacity: 0.75;
margin-bottom: 0.1rem;
line-height: 1.2rem;
margin-top: 0.25rem;
`;
const TaskDescription = styled.span`
margin: 0.3rem;

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
import React from 'react';
import { TaskAccessor } from './components';
import { TaskAccessor, TaskTimeAccessor } from './components';
/**
* Retrieve project tasks list columns.
@@ -17,6 +17,15 @@ export function useProjectTaskColumns() {
clickable: true,
textOverview: true,
},
{
id: 'actions',
Header: 'Header',
accessor: TaskTimeAccessor,
width: 100,
className: 'name',
clickable: true,
textOverview: true,
}
],
[],
);

View File

@@ -1,28 +1,37 @@
// @ts-nocheck
import React from 'react';
import { isUndefined } from 'lodash';
import styled from 'styled-components';
import { ProgressBar } from '@blueprintjs/core';
import { Intent, ProgressBar } from '@blueprintjs/core';
export function DetailFinancialSection({ children }) {
return <FinancialSectionWrap>{children}</FinancialSectionWrap>;
}
export function DetailFinancialCard({ label, value, children }) {
interface DetailFinancialCardProps {
label: string;
value: number;
description: string | JSX.Element;
progressValue: number;
}
export function DetailFinancialCard({
label,
value,
description,
progressValue,
}: DetailFinancialCardProps) {
return (
<React.Fragment>
<FinancialSectionCard>
<FinancialSectionCardContent>
<FinancialCardTitle>{label}</FinancialCardTitle>
<FinancialCardValue>{value}</FinancialCardValue>
{children}
</FinancialSectionCardContent>
</FinancialSectionCard>
</React.Fragment>
<FinancialSectionCard>
<FinancialCardTitle>{label}</FinancialCardTitle>
<FinancialCardValue>{value}</FinancialCardValue>
{description && <FinancialCartDesc>{description}</FinancialCartDesc>}
{!isUndefined(progressValue) && (
<FinancialProgressBar intent={Intent.NONE} value={progressValue} />
)}
</FinancialSectionCard>
);
}
export const FinancialDescription = ({ childern }) => {
return <FinancialCardText>{childern}</FinancialCardText>;
};
export const FinancialProgressBar = ({ ...rest }) => {
return <FinancialCardProgressBar animate={false} stripes={false} {...rest} />;
@@ -41,34 +50,30 @@ const FinancialSectionCard = styled.div`
border-radius: 3px;
width: 230px;
height: 116px;
padding: 16px;
background-color: #fff;
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
border: 1px solid #c8cad0;
gap: 6px;
`;
const FinancialSectionCardContent = styled.div`
margin: 16px;
`;
const FinancialCardWrap = styled.div``;
const FinancialCardTitle = styled.div`
font-size: 15px;
color: #000;
font-size: 14px;
color: #203252;
white-space: nowrap;
font-weight: 400;
line-height: 1.5rem;
`;
const FinancialCardValue = styled.div`
font-size: 21px;
line-height: 2rem;
font-weight: 700;
font-size: 20px;
font-weight: 600;
`;
const FinancialCardStatus = styled.div``;
const FinancialCartDesc = styled.div`
font-size: 12px;
`;
export const FinancialCardText = styled.div`
font-size: 13px;
line-height: 1.5rem;
color: #7b8195;
`;
export const FinancialCardProgressBar = styled(ProgressBar)`
&.bp3-progress-bar {

View File

@@ -6,9 +6,6 @@ const Schema = Yup.object().shape({
name: Yup.string()
.label(intl.get('project_task.schema.label.task_name'))
.required(),
charge_type: Yup.string()
.label(intl.get('project_task.schema.label.charge_type'))
.required(),
rate: Yup.number()
.label(intl.get('project_task.schema.label.rate'))
.required(),

View File

@@ -12,7 +12,6 @@ import withDialogActions from '@/containers/Dialog/withDialogActions';
const defaultInitialValues = {
name: '',
charge_type: 'TIME',
estimate_hours: '',
rate: '0.00',
};

View File

@@ -8,10 +8,9 @@ import {
Col,
Row,
FormattedMessage as T,
InputPrependText,
} from '@/components';
import { EstimateAmount } from './utils';
import { taskChargeOptions } from '../common/modalChargeOptions';
import { ProjectTaskChargeTypeSelect } from '../../components';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import { compose } from '@/utils';
@@ -48,17 +47,12 @@ function ProjectTaskFormFields({
{/*------------ Charge -----------*/}
<Col xs={8}>
<FFormGroup
name={'charge_type'}
name={'rate'}
className={'form-group--select-list'}
label={<T id={'project_task.dialog.charge'} />}
>
<ControlGroup>
<ProjectTaskChargeTypeSelect
name="charge_type"
items={taskChargeOptions}
popoverProps={{ minimal: true }}
filterable={false}
/>
<InputPrependText text={'Hourly Price'} />
<FInputGroup
name="rate"
disabled={values?.charge_type === 'non_chargable'}

View File

@@ -35,7 +35,7 @@ function ProjectTaskFormFloatingActions({
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '75px' }}
style={{ minWidth: '85px' }}
type="submit"
>
{<T id={'save'} />}

View File

@@ -14,26 +14,12 @@ export function EstimateAmount({ baseCurrency }) {
return (
<EstimatedAmountBase>
<EstimatedAmountContent>
<Choose>
<Choose.When condition={values?.charge_type === 'TIME'}>
<T id={'project_task.dialog.estimated_amount'} />
<EstimatedAmount>
<Money amount={estimatedAmount} currency={baseCurrency} />
</EstimatedAmount>
</Choose.When>
<Choose.When condition={values?.charge_type === 'FIXED'}>
<T id={'project_task.dialog.total'} />
<EstimatedAmount>
<Money amount={values.rate} currency={baseCurrency} />
</EstimatedAmount>
</Choose.When>
<Choose.Otherwise>
<T id={'project_task.dialog.total'} />
<EstimatedAmount>
<Money amount={0.0} currency={baseCurrency} />
</EstimatedAmount>
</Choose.Otherwise>
</Choose>
<EstimatedText>
<T id={'project_task.dialog.estimated_amount'} />
</EstimatedText>
<EstimatedAmount>
<Money amount={estimatedAmount} currency={baseCurrency} />
</EstimatedAmount>
</EstimatedAmountContent>
</EstimatedAmountBase>
);
@@ -42,9 +28,7 @@ export function EstimateAmount({ baseCurrency }) {
const EstimatedAmountBase = styled.div`
display: flex;
justify-content: flex-end;
font-size: 14px;
line-height: 1.5rem;
opacity: 0.75;
margin-bottom: 2rem;
`;
const EstimatedAmountContent = styled.span`
@@ -53,7 +37,11 @@ const EstimatedAmountContent = styled.span`
`;
const EstimatedAmount = styled.span`
font-size: 15px;
font-weight: 700;
font-size: 18px;
font-weight: 600;
margin-left: 10px;
`;
const EstimatedText = styled.span`
color: #607090;
`;

View File

@@ -10,9 +10,7 @@ import {
import {
Icon,
Can,
AdvancedFilterPopover,
DashboardActionViewsList,
DashboardFilterButton,
DashboardRowsHeightButton,
FormattedMessage as T,
DashboardActionsBar,
@@ -86,8 +84,6 @@ function ProjectsActionsBar({
onClick={handleNewProjectBtnClick}
/>
</Can>
{/* AdvancedFilterPopover */}
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />}

View File

@@ -66,14 +66,12 @@ function ProjectsDataTable({
action: 'edit',
});
};
// Handle new task button click.
const handleNewTaskButtonClick = (project) => {
openDialog('project-task-form', {
projectId: project.id,
});
};
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.PROJECTS);
@@ -132,32 +130,7 @@ export default compose(
const ProjectsTable = styled(DataTable)`
.tbody {
.tr .td {
padding: 0.5rem 0.8rem;
}
.avatar.td {
.cell-inner {
.avatar {
display: inline-block;
background: #adbcc9;
border-radius: 8%;
text-align: center;
font-weight: 400;
color: #fff;
&[data-size='medium'] {
height: 30px;
width: 30px;
line-height: 30px;
font-size: 14px;
}
&[data-size='small'] {
height: 25px;
width: 25px;
line-height: 25px;
font-size: 12px;
}
}
}
padding: 0.75rem 0.8rem;
}
}
.table-size--small {

View File

@@ -40,6 +40,7 @@ function ProjectsList({
<ProjectsActionsBar />
<DashboardPageContent>
<ProjectsViewTabs />
<DashboardContentTable>
<ProjectsDataTable />
</DashboardContentTable>

View File

@@ -28,12 +28,12 @@ import { safeCallback, firstLettersArgs, calculateStatus } from '@/utils';
export function ProjectStatus({ row }) {
return (
<ProjectStatusRoot>
<ProjectStatusTaskAmount>{row.cost_estimate}</ProjectStatusTaskAmount>
<ProjectStatusTaskAmount>{row.total_expenses_formatted}</ProjectStatusTaskAmount>
<ProjectProgressBar
animate={false}
stripes={false}
intent={Intent.PRIMARY}
value={calculateStatus(100, row.cost_estimate)}
value={calculateStatus(row.total_expenses, row.cost_estimate)}
/>
</ProjectStatusRoot>
);
@@ -44,37 +44,10 @@ export function ProjectStatus({ row }) {
*/
export const StatusAccessor = (row) => {
return (
<Choose>
<Choose.When condition={row.status_formatted === 'InProgress'}>
<ProjectStatus row={row} />
</Choose.When>
<Choose.When condition={row.status_formatted === 'Closed'}>
<StatusTagWrap>
<Tag minimal={true} intent={Intent.SUCCESS} round={true}>
{row.status_formatted}
</Tag>
</StatusTagWrap>
</Choose.When>
<Choose.Otherwise>
<StatusTagWrap>
<Tag minimal={true} round={true}>
<T id={'draft'} />
</Tag>
</StatusTagWrap>
</Choose.Otherwise>
</Choose>
<ProjectStatus row={row} />
);
};
/**
* Avatar cell.
*/
export const AvatarCell = ({ row: { original }, size }) => (
<span className="avatar" data-size={size}>
{firstLettersArgs(original?.contact_display_name, original?.name)}
</span>
);
/**
* Table actions cell.
*/
@@ -135,20 +108,27 @@ export const ActionsMenu = ({
* Projects accessor.
*/
export const ProjectsAccessor = (row) => (
<ProjectItemsWrap>
<ProjectItemsHeader>
<ProjectItemContactName>
{row.contact_display_name}
</ProjectItemContactName>
<ProjectItemProjectName>{row.name}</ProjectItemProjectName>
</ProjectItemsHeader>
<ProjectItemDescription>
<FormatDate value={row.deadline_formatted} />
{intl.get('projects.label.cost_estimate', {
value: row.cost_estimate_formatted,
})}
</ProjectItemDescription>
</ProjectItemsWrap>
<ProjectName>
<ProjectAvatar data-size="medium">
{firstLettersArgs(row?.contact_display_name, row?.name)}
</ProjectAvatar>
<ProjectItemsWrap>
<ProjectItemsHeader>
<ProjectItemContactName>
{row.contact_display_name}
</ProjectItemContactName>
<ProjectItemProjectName>{row.name}</ProjectItemProjectName>
</ProjectItemsHeader>
<ProjectItemDescription>
<FormatDate value={row.deadline_formatted} />
{intl.get('projects.label.cost_estimate', {
value: row.cost_estimate_formatted,
})}
</ProjectItemDescription>
</ProjectItemsWrap>
</ProjectName>
);
/**
@@ -157,16 +137,6 @@ export const ProjectsAccessor = (row) => (
export const useProjectsListColumns = () => {
return React.useMemo(
() => [
{
id: 'avatar',
Header: '',
Cell: AvatarCell,
className: 'avatar',
width: 45,
disableResizing: true,
disableSortBy: true,
clickable: true,
},
{
id: 'name',
Header: '',
@@ -196,16 +166,18 @@ const ProjectItemsHeader = styled.div`
`;
const ProjectItemContactName = styled.div`
font-weight: 500;
padding-right: 4px;
font-weight: 600;
padding-right: 10px;
`;
const ProjectItemProjectName = styled.div`
color: #595b66;
`;
const ProjectItemProjectName = styled.div``;
const ProjectItemDescription = styled.div`
display: inline-block;
font-size: 13px;
opacity: 0.75;
margin-top: 0.2rem;
margin-top: 0.3rem;
line-height: 1;
`;
@@ -218,8 +190,7 @@ const ProjectStatusRoot = styled.div`
const ProjectStatusTaskAmount = styled.div`
text-align: right;
font-weight: 400;
line-height: 1.5rem;
font-size: 15px;
margin-left: 20px;
`;
@@ -244,3 +215,36 @@ const StatusTagWrap = styled.div`
text-align: center;
}
`;
export const ProjectName = styled.div`
display: flex;
flex-direction: row;
gap: 15px;
`;
export const Avatar = styled.div`
display: inline-block;
background: #adbcc9;
border-radius: 8%;
text-align: center;
font-weight: 400;
color: #fff;
&[data-size='medium'] {
height: 32px;
width: 32px;
line-height: 32px;
font-size: 14px;
}
&[data-size='small'] {
height: 25px;
width: 25px;
line-height: 25px;
font-size: 12px;
}
`;
export const ProjectAvatar = styled(Avatar)`
margin-top: auto;
margin-bottom: auto;
`;