Merge pull request #139 from shancds/feature/project-finance

Feature/project finance
This commit is contained in:
Chamika J
2025-05-27 09:07:17 +05:30
committed by GitHub
6 changed files with 72 additions and 27 deletions

View File

@@ -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"
} }

View File

@@ -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()}

View File

@@ -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>

View File

@@ -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',

View File

@@ -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';
}
} }
} }
} }
@@ -145,7 +166,7 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
const fetchChartData = async () => { const fetchChartData = async () => {
try { try {
setLoading(true); setLoading(true);
const selectedTeams = teams.filter(team => team.selected); const selectedTeams = teams.filter(team => team.selected);
const selectedProjects = filterProjects.filter(project => project.selected); const selectedProjects = filterProjects.filter(project => project.selected);
const selectedCategories = categories.filter(category => category.selected); const selectedCategories = categories.filter(category => category.selected);
@@ -178,7 +199,7 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
if (chartRef.current) { if (chartRef.current) {
// Get the canvas element // Get the canvas element
const canvas = chartRef.current.canvas; const canvas = chartRef.current.canvas;
// Create a temporary canvas to draw with background // Create a temporary canvas to draw with background
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d'); const tempCtx = tempCanvas.getContext('2d');
@@ -225,4 +246,4 @@ const MembersTimeSheet = forwardRef<MembersTimeSheetRef>((_, ref) => {
MembersTimeSheet.displayName = 'MembersTimeSheet'; MembersTimeSheet.displayName = 'MembersTimeSheet';
export default MembersTimeSheet; export default MembersTimeSheet;

View File

@@ -87,7 +87,7 @@ const RatecardSettings: React.FC = () => {
useEffect(() => { useEffect(() => {
fetchRateCards(); fetchRateCards();
}, [toggleRatecardDrawer]); }, [toggleRatecardDrawer, dispatch]);