feat: add project timesheet.

This commit is contained in:
elforjani13
2022-06-23 00:10:06 +02:00
parent 5128c021b0
commit 4ba64cc4ff
11 changed files with 426 additions and 188 deletions

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
import React from 'react';
import {
Button,
@@ -15,7 +16,7 @@ import {
import withSettings from '../../../Settings/withSettings';
import withSettingsActions from '../../../Settings/withSettingsActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { useProjectDetailContext } from './ProjectDetailProvider';
import { compose } from 'utils';
/**
@@ -32,16 +33,25 @@ function ProjectDetailActionsBar({
// #withSettingsActions
addSetting,
}) {
const { projectId } = useProjectDetailContext();
// Handle new transaction button click.
const handleNewTransactionBtnClick = () => {};
const handleEditProjectBtnClick = () => {
openDialog('project-form', {
projectId,
});
};
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
addSetting('timesheets', 'tableSize', size);
};
const handleTimeEntryBtnClick = () => {
openDialog('time-entry-form');
openDialog('time-entry-form', {
projectId,
});
};
// Handle the refresh button click.
@@ -58,14 +68,15 @@ function ProjectDetailActionsBar({
/>
<Button
className={Classes.MINIMAL}
text={<T id={'projcet_details.action.log_time'} />}
icon={<Icon icon={'time-24'} iconSize={16} />}
text={<T id={'projcet_details.action.time_entry'} />}
onClick={handleTimeEntryBtnClick}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="pen-18" />}
text={<T id={'projcet_details.action.edit_project'} />}
// onClick={}
onClick={handleEditProjectBtnClick}
/>
<NavbarDivider />
<Button

View File

@@ -9,11 +9,14 @@ const ProjectDetailContext = React.createContext();
* @returns
*/
function ProjectDetailProvider({
projectId,
// #ownProps
...props
}) {
// State provider.
const provider = {};
const provider = {
projectId,
};
return (
<DashboardInsider class="timesheets">
<ProjectDetailContext.Provider value={provider} {...props} />

View File

@@ -2,7 +2,8 @@ import React from 'react';
import styled from 'styled-components';
import intl from 'react-intl-universal';
import { Tabs, Tab } from '@blueprintjs/core';
import TimesheetDataTable from './TimesheetDataTable';
import ProjectTimesheet from './ProjectTimesheet';
/**
* Project detail tabs.
@@ -21,7 +22,7 @@ export default function ProjectDetailTabs() {
<Tab
id="timesheet"
title={intl.get('project_details.label.timesheet')}
panel={<TimesheetDataTable />}
panel={<ProjectTimesheet />}
/>
<Tab
id="purchases"
@@ -58,11 +59,9 @@ const ProjectTabsContent = styled.div`
}
}
.bp3-tab-panel {
border: 2px solid #f0f0f0;
border-radius: 10px;
padding: 30px 18px;
margin: 30px 15px;
background: #fff;
margin-top: 20px;
margin-bottom: 20px;
padding: 0 25px;
}
}
`;

View File

@@ -0,0 +1,125 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Intent, ProgressBar } from '@blueprintjs/core';
import { FormatDate } from 'components';
import { calculateStatus } from 'utils';
/**
* Project Financial Section.
* @returns
*/
export default function ProjectFinancialSection() {
return (
<ProjectFinancialSectionRoot>
<FinancialSectionCard>
<FinancialSectionContent>
<FinancialSectionTitle>Project estimate</FinancialSectionTitle>
<FinancialSectionValue>3.14</FinancialSectionValue>
</FinancialSectionContent>
</FinancialSectionCard>
<FinancialSectionCard>
<FinancialSectionContent>
<FinancialSectionTitle>Invoiced</FinancialSectionTitle>
<FinancialSectionValue>0.00</FinancialSectionValue>
<FinancialSectionStatus>
<FinancialSectionText>0% of project estimate</FinancialSectionText>
<FinancialSectionProgressBar
animate={false}
intent={Intent.NONE}
value={0}
/>
</FinancialSectionStatus>
</FinancialSectionContent>
</FinancialSectionCard>
<FinancialSectionCard>
<FinancialSectionContent>
<FinancialSectionTitle>Time & Expenses</FinancialSectionTitle>
<FinancialSectionValue>0.00</FinancialSectionValue>
<FinancialSectionStatus>
<FinancialSectionText>0% of project estimate</FinancialSectionText>
<FinancialSectionProgressBar
animate={false}
intent={Intent.NONE}
value={0}
/>
</FinancialSectionStatus>
</FinancialSectionContent>
</FinancialSectionCard>
<FinancialSectionCard>
<FinancialSectionContent>
<FinancialSectionTitle>To be invoiced</FinancialSectionTitle>
<FinancialSectionValue>3.14</FinancialSectionValue>
</FinancialSectionContent>
</FinancialSectionCard>
<FinancialSectionCard>
<FinancialSectionContent>
<FinancialSectionTitle>Deadline</FinancialSectionTitle>
<FinancialSectionValue>
<FormatDate value={'2022-06-08T22:00:00.000Z'} />
</FinancialSectionValue>
<FinancialSectionText>4 days to go</FinancialSectionText>
</FinancialSectionContent>
</FinancialSectionCard>
</ProjectFinancialSectionRoot>
);
}
export const ProjectFinancialSectionRoot = styled.div`
display: flex;
flex-wrap: wrap;
margin: 20px 20px 20px;
gap: 10px;
`;
export const FinancialSectionCard = styled.div`
display: flex;
flex-direction: column;
flex-shrink: 0;
border-radius: 3px;
width: 220px;
height: 116px;
background-color: #fff;
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
`;
export const FinancialSectionContent = styled.div`
margin: 16px;
/* flex-direction: column; */
`;
export const FinancialSectionTitle = styled.div`
font-size: 15px;
color: #000;
white-space: nowrap;
font-weight: 400;
line-height: 1.5rem;
`;
export const FinancialSectionValue = styled.div`
font-size: 21px;
line-height: 2rem;
font-weight: 700;
`;
export const FinancialSectionStatus = styled.div``;
export const FinancialSectionText = styled.div`
font-size: 13px;
line-height: 1.5rem;
`;
export const FinancialSectionProgressBar = styled(ProgressBar)`
&.bp3-progress-bar {
height: 3px;
&,
.bp3-progress-meter {
border-radius: 0;
}
}
`;

View File

@@ -0,0 +1,124 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { DataTable } from 'components';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { useTimesheetColumns, ActionsMenu } from './components';
import { TABLES } from 'common/tables';
import { useMemorizedColumnsWidths } from 'hooks';
import withSettings from '../../../../Settings/withSettings';
import { compose } from 'utils';
const Timesheet = [
{
id: 1,
date: '2022-06-08T22:00:00.000Z',
name: 'Lighting',
display_name: 'Kyrie Rearden',
description: 'Laid paving stones',
duration: '12:00',
},
{
id: 2,
date: '2022-06-08T22:00:00.000Z',
name: 'Interior Decoration',
display_name: 'Project Sherwood',
description: 'Laid paving stones',
duration: '11:00',
},
];
/**
* Timesheet DataTable.
* @returns
*/
function TimesheetsTable({
// #withSettings
timesheetsTableSize,
}) {
// Retrieve timesheet table columns.
const columns = useTimesheetColumns();
// Handle delete timesheet.
const handleDeleteTimesheet = () => {};
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.TIMESHEETS);
return (
<TimesheetDataTable
columns={columns}
data={Timesheet}
// loading={}
// headerLoading={}
// progressBarLoading={}
manualSortBy={true}
noInitialFetch={true}
sticky={true}
hideTableHeader={true}
ContextMenu={ActionsMenu}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={timesheetsTableSize}
payload={{
onDelete: handleDeleteTimesheet,
}}
/>
);
}
export default compose(
withSettings(({ timesheetsSettings }) => ({
timesheetsTableSize: timesheetsSettings?.tableSize,
})),
)(TimesheetsTable);
const TimesheetDataTable = styled(DataTable)`
.table {
.thead .tr .th {
.resizer {
display: none;
}
}
.tbody {
.tr .td {
padding: 0.5rem 0.8rem;
}
.avatar.td {
.avatar {
display: inline-block;
background: #adbcc9;
border-radius: 50%;
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;
}
}
}
}
.table-size--small {
.tbody .tr {
height: 45px;
}
}
}
`;

View File

@@ -0,0 +1,120 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { FormatDate, Icon, FormattedMessage as T } from 'components';
import { Menu, MenuItem, Intent } from '@blueprintjs/core';
import { safeCallback, firstLettersArgs } from 'utils';
import { chain } from 'lodash';
/**
* Table actions cell.
*/
export function ActionsMenu({
payload: { onDelete, onViewDetails },
row: { original },
}) {
return (
<Menu>
<MenuItem
text={intl.get('timesheets.actions.delete_timesheet')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Avatar cell.
*/
export const AvatarCell = ({ row: { original }, size }) => (
<span className="avatar" data-size={size}>
{firstLettersArgs(original?.display_name, original?.name)}
</span>
);
/**
* Timesheet accessor.
*/
export const TimesheetAccessor = (timesheet) => (
<React.Fragment>
<TimesheetHeader>
<TimesheetTitle>{timesheet.display_name}</TimesheetTitle>
<TimesheetSubTitle>{timesheet.name}</TimesheetSubTitle>
</TimesheetHeader>
<TimesheetContent>
<FormatDate value={timesheet.date} />
<TimesheetDescription>{timesheet.description}</TimesheetDescription>
</TimesheetContent>
</React.Fragment>
);
const TimesheetHeader = styled.div`
display: flex;
align-items: baseline;
flex-flow: wrap;
`;
const TimesheetTitle = styled.span`
font-weight: 500;
margin-right: 12px;
line-height: 1.5rem;
`;
const TimesheetSubTitle = styled.span``;
const TimesheetContent = styled.div`
display: block;
white-space: nowrap;
font-size: 13px;
opacity: 0.75;
margin-bottom: 0.1rem;
line-height: 1.2rem;
`;
const TimesheetDescription = styled.span`
&::before {
content: '•';
margin: 0.3rem;
}
`;
/**
* Retrieve timesheet list columns columns.
*/
export function useTimesheetColumns() {
return React.useMemo(
() => [
{
id: 'avatar',
Header: '',
Cell: AvatarCell,
className: 'avatar',
width: 45,
disableResizing: true,
disableSortBy: true,
clickable: true,
},
{
id: 'name',
Header: 'Header',
accessor: TimesheetAccessor,
width: 100,
className: 'name',
clickable: true,
textOverview: true,
},
{
id: 'duration',
Header: '',
accessor: 'duration',
width: 100,
className: 'duration',
align: 'right',
clickable: true,
textOverview: true,
},
],
[],
);
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import styled from 'styled-components';
import TimesheetsTable from './TimesheetsTable';
import ProjectFinancialSection from '../ProjectFinancialSection';
/**
* Project Timesheet.
* @returns
*/
export default function ProjectTimesheet() {
return (
<React.Fragment>
<ProjectFinancialSection />
<ProjectTimesheetTableCard>
<TimesheetsTable />
</ProjectTimesheetTableCard>
</React.Fragment>
);
}
const ProjectTimesheetTableCard = styled.div`
margin: 20px;
border: 1px solid #c8cad0; // #000a1e33 #f0f0f0
border-radius: 3px;
background: #fff;
`;

View File

@@ -1,83 +0,0 @@
import React from 'react';
import intl from 'react-intl-universal';
import { FormatDateCell, Icon } from 'components';
import { Menu, MenuDivider, MenuItem, Intent } from '@blueprintjs/core';
import { safeCallback } from 'utils';
/**
* Table actions cell.
*/
export function ActionsMenu({
payload: { onDelete, onViewDetails },
row: { original },
}) {
return (
<Menu>
<MenuItem
text={'Delete'}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Retrieve timesheet list columns columns.
*/
export function useTimesheetColumns() {
return React.useMemo(
() => [
{
id: 'date',
Header: intl.get('timesheets.column.date'),
accessor: 'date',
Cell: FormatDateCell,
width: 100,
className: 'date',
clickable: true,
textOverview: true,
},
{
id: 'task',
Header: intl.get('timesheets.column.task'),
accessor: 'task',
width: 100,
className: 'task',
clickable: true,
textOverview: true,
},
{
id: 'user',
Header: intl.get('timesheets.column.user'),
accessor: 'user',
width: 100,
className: 'user',
clickable: true,
textOverview: true,
},
{
id: 'time',
Header: intl.get('timesheets.column.time'),
accessor: 'time',
width: 100,
className: 'user',
align: 'right',
clickable: true,
textOverview: true,
},
{
id: 'billingStatus',
Header: intl.get('timesheets.column.billing_status'),
accessor: 'billing_status',
width: 140,
className: 'billingStatus',
clickable: true,
textOverview: true,
},
],
[],
);
}

View File

@@ -1,89 +0,0 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { DataTable, TableFastCell } from 'components';
import TableVirtualizedListRows from 'components/Datatable/TableVirtualizedRows';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import { useTimesheetColumns, ActionsMenu } from './components';
import { TableStyle } from '../../../../../common';
import withSettings from '../../../../Settings/withSettings';
import { compose } from 'utils';
const Timesheet = [
{
id: 1,
data: '2020-01-01',
task: 'Task 1',
user: 'User 1',
time: '12:00Am',
billingStatus: '',
},
];
/**
* Timesheet DataTable.
* @returns
*/
function TimesheetDataTable({
// #withSettings
timesheetsTableSize,
}) {
// Retrieve timesheet table columns.
const columns = useTimesheetColumns();
// Handle delete timesheet.
const handleDeleteTimesheet = () => {};
return (
<TimesheetsTable
columns={columns}
data={Timesheet}
// loading={}
// headerLoading={}
noInitialFetch={true}
sticky={true}
expandColumnSpace={1}
expandToggleColumn={2}
selectionColumnWidth={45}
ContextMenu={ActionsMenu}
TableCellRenderer={TableFastCell}
TableLoadingRenderer={TableSkeletonRows}
TableRowsRenderer={TableVirtualizedListRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
vListrowHeight={timesheetsTableSize === 'small' ? 32 : 40}
vListOverscanRowCount={0}
styleName={TableStyle.Constrant}
payload={{
onDelete: handleDeleteTimesheet,
}}
/>
);
}
export default compose(
withSettings(({ timesheetsSettings }) => ({
timesheetsTableSize: timesheetsSettings?.tableSize,
})),
)(TimesheetDataTable);
const TimesheetsTable = styled(DataTable)`
.table .tbody {
.tbody-inner .tr.no-results {
.td {
padding: 2rem 0;
font-size: 14px;
color: #888;
font-weight: 400;
border-bottom: 0;
}
}
.tbody-inner {
.tr .td:not(:first-child) {
border-left: 1px solid #e6e6e6;
}
}
}
`;

View File

@@ -17,15 +17,15 @@ function ProjectTabs({
changePageTitle,
}) {
const {
state: { name },
state: { projectName, projectId },
} = useLocation();
React.useEffect(() => {
changePageTitle(name);
}, [changePageTitle, name]);
changePageTitle(projectName);
}, [changePageTitle, projectName]);
return (
<ProjectDetailProvider>
<ProjectDetailProvider projectId={projectId}>
<ProjectDetailActionsBar />
<DashboardPageContent>
<ProjectDetailTabs />