Merge pull request #139 from shancds/feature/project-finance
Feature/project finance
This commit is contained in:
@@ -32,7 +32,10 @@
|
|||||||
"labourHoursColumn": "Labour Hours",
|
"labourHoursColumn": "Labour Hours",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"selectJobTitle": "Select Job Title",
|
"selectJobTitle": "Select Job Title",
|
||||||
"ratecardsPluralText": "Rate Card Templates"
|
"ratecardsPluralText": "Rate Card Templates",
|
||||||
|
"deleteConfirm": "Are you sure ?",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,9 +166,11 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
|||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
|
title: 'Name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'priority',
|
key: 'priority',
|
||||||
|
title: 'Priority',
|
||||||
render: (record: IProjectTask) => (
|
render: (record: IProjectTask) => (
|
||||||
<Tag
|
<Tag
|
||||||
color={themeMode === 'dark' ? record.priority_color_dark : record.priority_color}
|
color={themeMode === 'dark' ? record.priority_color_dark : record.priority_color}
|
||||||
@@ -180,6 +182,7 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
|
title: 'Status',
|
||||||
render: (record: IProjectTask) => (
|
render: (record: IProjectTask) => (
|
||||||
<Tag
|
<Tag
|
||||||
color={themeMode === 'dark' ? record.status_color_dark : record.status_color}
|
color={themeMode === 'dark' ? record.status_color_dark : record.status_color}
|
||||||
@@ -191,10 +194,12 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'assignee',
|
key: 'assignee',
|
||||||
|
title: 'Assignees',
|
||||||
render: (record: ISubTask) => <Avatars members={record.names || []} />,
|
render: (record: ISubTask) => <Avatars members={record.names || []} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'actionBtns',
|
key: 'actionBtns',
|
||||||
|
title: 'Actions',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (record: IProjectTask) => (
|
render: (record: IProjectTask) => (
|
||||||
<Flex gap={8} align="center" className="action-buttons">
|
<Flex gap={8} align="center" className="action-buttons">
|
||||||
@@ -230,7 +235,6 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
|||||||
{subTasks.length > 0 && (
|
{subTasks.length > 0 && (
|
||||||
<Table
|
<Table
|
||||||
className="custom-two-colors-row-table subtask-table"
|
className="custom-two-colors-row-table subtask-table"
|
||||||
showHeader={false}
|
|
||||||
dataSource={subTasks}
|
dataSource={subTasks}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey={record => record?.id || nanoid()}
|
rowKey={record => record?.id || nanoid()}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Drawer, Select, Typography, Flex, Button, Input, Table } from 'antd';
|
import { Drawer, Select, Typography, Flex, Button, Input, Table, Popconfirm, Tooltip } from 'antd';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAppSelector } from '../../../hooks/useAppSelector';
|
import { useAppSelector } from '../../../hooks/useAppSelector';
|
||||||
@@ -7,7 +7,8 @@ import { deleteRateCard, fetchRateCardById, fetchRateCards, toggleRatecardDrawer
|
|||||||
import { RatecardType, IJobType } from '@/types/project/ratecard.types';
|
import { RatecardType, IJobType } from '@/types/project/ratecard.types';
|
||||||
import { IJobTitlesViewModel } from '@/types/job.types';
|
import { IJobTitlesViewModel } from '@/types/job.types';
|
||||||
import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service';
|
import { jobTitlesApiService } from '@/api/settings/job-titles/job-titles.api.service';
|
||||||
import { DeleteOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
|
import { colors } from '@/styles/colors';
|
||||||
|
|
||||||
interface PaginationType {
|
interface PaginationType {
|
||||||
current: number;
|
current: number;
|
||||||
@@ -220,7 +221,7 @@ const RatecardDrawer = ({
|
|||||||
setAddingRowIndex(null);
|
setAddingRowIndex(null);
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (roles[index].job_title_id === ""){
|
if (roles[index].job_title_id === "") {
|
||||||
handleDeleteRole(index);
|
handleDeleteRole(index);
|
||||||
}
|
}
|
||||||
setEditingRowIndex(null);
|
setEditingRowIndex(null);
|
||||||
@@ -244,7 +245,7 @@ const RatecardDrawer = ({
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
// onClick={() => setEditingRowIndex(index)}
|
// onClick={() => setEditingRowIndex(index)}
|
||||||
>
|
>
|
||||||
{record.jobtitle}
|
{record.jobtitle}
|
||||||
</span>
|
</span>
|
||||||
@@ -259,6 +260,7 @@ const RatecardDrawer = ({
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={roles[index]?.rate ?? 0}
|
value={roles[index]?.rate ?? 0}
|
||||||
|
min={0}
|
||||||
style={{
|
style={{
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -279,11 +281,25 @@ const RatecardDrawer = ({
|
|||||||
title: t('actionsColumn') || 'Actions',
|
title: t('actionsColumn') || 'Actions',
|
||||||
dataIndex: 'actions',
|
dataIndex: 'actions',
|
||||||
render: (_: any, __: any, index: number) => (
|
render: (_: any, __: any, index: number) => (
|
||||||
<Button
|
<Popconfirm
|
||||||
size="small"
|
title={t('deleteConfirmationTitle')}
|
||||||
icon={<DeleteOutlined />}
|
icon={<ExclamationCircleFilled style={{ color: colors.vibrantOrange }} />}
|
||||||
onClick={() => handleDeleteRole(index)}
|
okText={t('deleteConfirmationOk')}
|
||||||
/>
|
cancelText={t('deleteConfirmationCancel')}
|
||||||
|
onConfirm={async () => {
|
||||||
|
if (index) {
|
||||||
|
handleDeleteRole(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title="Delete">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -291,7 +307,7 @@ const RatecardDrawer = ({
|
|||||||
if (
|
if (
|
||||||
drawerRatecard &&
|
drawerRatecard &&
|
||||||
(drawerRatecard.jobRolesList?.length === 0 || !drawerRatecard.jobRolesList) &&
|
(drawerRatecard.jobRolesList?.length === 0 || !drawerRatecard.jobRolesList) &&
|
||||||
name === 'Untitled Rate Card'
|
(name === 'Untitled Rate Card' || name === '' || name === undefined)
|
||||||
) {
|
) {
|
||||||
await dispatch(deleteRateCard(drawerRatecard.id as string));
|
await dispatch(deleteRateCard(drawerRatecard.id as string));
|
||||||
}
|
}
|
||||||
@@ -343,7 +359,7 @@ const RatecardDrawer = ({
|
|||||||
width={700}
|
width={700}
|
||||||
footer={
|
footer={
|
||||||
<Flex justify="end" gap={16} style={{ marginTop: 16 }}>
|
<Flex justify="end" gap={16} style={{ marginTop: 16 }}>
|
||||||
<Button style={{ marginBottom: 24 }} onClick={handleSave} type="primary">{t('saveButton')}</Button>
|
<Button style={{ marginBottom: 24 }} onClick={handleSave} type="primary" disabled={name === '' || name === 'Untitled Rate Card' && roles.length === 0}>{t('saveButton')}</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -355,13 +371,13 @@ const RatecardDrawer = ({
|
|||||||
pagination={false}
|
pagination={false}
|
||||||
footer={() => (
|
footer={() => (
|
||||||
<Button
|
<Button
|
||||||
type="dashed"
|
type="dashed"
|
||||||
onClick={handleAddRole}
|
onClick={handleAddRole}
|
||||||
block
|
block
|
||||||
style={{ margin: 0, padding: 0 }}
|
style={{ margin: 0, padding: 0 }}
|
||||||
>
|
>
|
||||||
{t('addRoleButton')}
|
{t('addRoleButton')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ const RatecardTable: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={roles[index]?.rate ?? 0}
|
value={roles[index]?.rate ?? 0}
|
||||||
|
min={0}
|
||||||
style={{
|
style={{
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|||||||
@@ -92,17 +92,38 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: function(context: any) {
|
label: function (context: any) {
|
||||||
const idx = context.dataIndex;
|
const idx = context.dataIndex;
|
||||||
const member = jsonData[idx];
|
const member = jsonData[idx];
|
||||||
const hours = member?.utilized_hours || '0.00';
|
const hours = member?.utilized_hours || '0.00';
|
||||||
const percent = member?.utilization_percent || '0.00';
|
const percent = parseFloat(member?.utilization_percent || '0.00');
|
||||||
const overUnder = member?.over_under_utilized_hours || '0.00';
|
const overUnder = member?.over_under_utilized_hours || '0.00';
|
||||||
|
let status = '';
|
||||||
|
let color = '';
|
||||||
|
if (percent < 90) {
|
||||||
|
status = 'Under';
|
||||||
|
} else if (percent <= 110) {
|
||||||
|
status = 'Optimal';
|
||||||
|
} else {
|
||||||
|
status = 'Over';
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
`${context.dataset.label}: ${hours} h`,
|
`${context.dataset.label}: ${hours} h`,
|
||||||
`Utilization: ${percent}%`,
|
`Utilization: ${percent}%`,
|
||||||
`Over/Under Utilized: ${overUnder} h`
|
`${status} Utilized: ${overUnder} h`
|
||||||
];
|
];
|
||||||
|
},
|
||||||
|
labelTextColor: function (context: any) {
|
||||||
|
const idx = context.dataIndex;
|
||||||
|
const member = jsonData[idx];
|
||||||
|
const utilization = parseFloat(member?.utilization_percent || '0');
|
||||||
|
if (utilization < 90) {
|
||||||
|
return '#FFB546';
|
||||||
|
} else if (utilization >= 90 && utilization <= 110) {
|
||||||
|
return '#B2EF9A';
|
||||||
|
} else {
|
||||||
|
return '#FE7173';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const RatecardSettings: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRateCards();
|
fetchRateCards();
|
||||||
}, [toggleRatecardDrawer]);
|
}, [toggleRatecardDrawer, dispatch]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user