feat(reporting): add billable and non-billable time tracking to member reports
- Implemented SQL logic to calculate billable and non-billable time for team members in the reporting module. - Enhanced the reporting members table to display new time tracking metrics with appropriate headers and tooltips. - Created a new TimeLogsCell component to visually represent billable vs non-billable time with percentage breakdowns. - Updated localization files for English, Spanish, and Portuguese to include new terms related to time tracking.
This commit is contained in:
@@ -31,5 +31,10 @@
|
||||
|
||||
"todoText": "To Do",
|
||||
"doingText": "Doing",
|
||||
"doneText": "Done"
|
||||
"doneText": "Done",
|
||||
|
||||
"timeLogsColumn": "Time Logs",
|
||||
"timeLogsColumnTooltip": "Shows the proportion of billable vs non-billable time",
|
||||
"billable": "Billable",
|
||||
"nonBillable": "Non-Billable"
|
||||
}
|
||||
|
||||
@@ -31,5 +31,10 @@
|
||||
|
||||
"todoText": "Por Hacer",
|
||||
"doingText": "Haciendo",
|
||||
"doneText": "Hecho"
|
||||
"doneText": "Hecho",
|
||||
|
||||
"timeLogsColumn": "Registros de Tiempo",
|
||||
"timeLogsColumnTooltip": "Muestra la proporción de tiempo facturable vs no facturable",
|
||||
"billable": "Facturable",
|
||||
"nonBillable": "No Facturable"
|
||||
}
|
||||
|
||||
@@ -31,5 +31,10 @@
|
||||
|
||||
"todoText": "To Do",
|
||||
"doingText": "Doing",
|
||||
"doneText": "Done"
|
||||
"doneText": "Done",
|
||||
|
||||
"timeLogsColumn": "Registros de Tempo",
|
||||
"timeLogsColumnTooltip": "Mostra a proporção de tempo faturável vs não faturável",
|
||||
"billable": "Faturável",
|
||||
"nonBillable": "Não Faturável"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import CustomTableTitle from '@/components/CustomTableTitle';
|
||||
import TasksProgressCell from './tablesCells/tasksProgressCell/TasksProgressCell';
|
||||
import MemberCell from './tablesCells/memberCell/MemberCell';
|
||||
import TimeLogsCell from './tablesCells/timeLogsCell/TimeLogsCell';
|
||||
import {
|
||||
fetchMembersData,
|
||||
setPagination,
|
||||
@@ -54,6 +55,15 @@ const MembersReportsTable = () => {
|
||||
return todo || doing || done ? <TasksProgressCell tasksStat={record.tasks_stat} /> : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'timeLogs',
|
||||
title: <CustomTableTitle title={t('timeLogsColumn')} tooltip={t('timeLogsColumnTooltip')} />,
|
||||
render: record => {
|
||||
const billableTime = record.billable_time || 0;
|
||||
const nonBillableTime = record.non_billable_time || 0;
|
||||
return <TimeLogsCell billableTime={billableTime} nonBillableTime={nonBillableTime} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'tasksAssigned',
|
||||
title: (
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface TimeLogsCellProps {
|
||||
billableTime: number;
|
||||
nonBillableTime: number;
|
||||
}
|
||||
|
||||
const TimeLogsCell = ({ billableTime, nonBillableTime }: TimeLogsCellProps) => {
|
||||
const { t } = useTranslation('reporting-members');
|
||||
const totalTime = billableTime + nonBillableTime;
|
||||
|
||||
if (totalTime === 0) return '-';
|
||||
|
||||
const billablePercentage = Math.round((billableTime / totalTime) * 100);
|
||||
const nonBillablePercentage = 100 - billablePercentage;
|
||||
|
||||
// Ensure minimum visibility for very small percentages
|
||||
const minWidth = 2; // minimum 2% width for visibility
|
||||
const billableWidth = Math.max(billablePercentage, billablePercentage > 0 ? minWidth : 0);
|
||||
const nonBillableWidth = Math.max(nonBillablePercentage, nonBillablePercentage > 0 ? minWidth : 0);
|
||||
|
||||
// Format time in hours and minutes
|
||||
const formatTime = (seconds: number) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
const tooltipContent = (
|
||||
<div className="text-sm">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-sm"></div>
|
||||
<span>{t('billable')}: {formatTime(billableTime)} ({billablePercentage}%)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-gray-400 rounded-sm"></div>
|
||||
<span>{t('nonBillable')}: {formatTime(nonBillableTime)} ({nonBillablePercentage}%)</span>
|
||||
</div>
|
||||
<div className="mt-2 pt-2 border-t border-gray-200">
|
||||
<span className="font-medium">Total: {formatTime(totalTime)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltipContent} placement="top">
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<div className="relative w-24 h-4 rounded-full overflow-hidden flex border border-gray-300">
|
||||
{/* Billable time section (green) */}
|
||||
{billableTime > 0 && (
|
||||
<div
|
||||
className="bg-green-500 transition-all duration-300 h-full"
|
||||
style={{ width: `${billableWidth}%` }}
|
||||
/>
|
||||
)}
|
||||
{/* Non-billable time section (gray) */}
|
||||
{nonBillableTime > 0 && (
|
||||
<div
|
||||
className="bg-gray-400 transition-all duration-300 h-full"
|
||||
style={{ width: `${nonBillableWidth}%` }}
|
||||
/>
|
||||
)}
|
||||
{/* Percentage text overlay */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-xs font-medium text-white drop-shadow-sm z-10">
|
||||
{billablePercentage}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeLogsCell;
|
||||
@@ -136,6 +136,8 @@ export interface IRPTMember {
|
||||
ongoing: number;
|
||||
todo: number;
|
||||
member_teams: any;
|
||||
billable_time?: number;
|
||||
non_billable_time?: number;
|
||||
}
|
||||
|
||||
export interface ISingleMemberLogs {
|
||||
|
||||
Reference in New Issue
Block a user