feat(reporting): add total time utilization component and enhance localization
- Introduced a new TotalTimeUtilization component to display total time logged, expected capacity, and team utilization metrics. - Updated existing time-report localization files to include new keys for total time logged, expected capacity, and utilization states across multiple languages. - Refactored MembersTimeReports to integrate the new TotalTimeUtilization component, improving the reporting interface. - Enhanced the overall structure and organization of the reporting components for better maintainability.
This commit is contained in:
@@ -46,5 +46,20 @@
|
|||||||
"filterByBillableStatus": "按计费状态筛选",
|
"filterByBillableStatus": "按计费状态筛选",
|
||||||
"searchByMember": "按成员搜索",
|
"searchByMember": "按成员搜索",
|
||||||
"members": "成员",
|
"members": "成员",
|
||||||
"utilization": "利用率"
|
"utilization": "利用率",
|
||||||
|
|
||||||
|
"totalTimeLogged": "总记录时间",
|
||||||
|
"acrossAllTeamMembers": "跨所有团队成员",
|
||||||
|
"expectedCapacity": "预期容量",
|
||||||
|
"basedOnWorkingSchedule": "基于工作时间表",
|
||||||
|
"teamUtilization": "团队利用率",
|
||||||
|
"targetRange": "目标范围",
|
||||||
|
"variance": "差异",
|
||||||
|
"overCapacity": "超出容量",
|
||||||
|
"underCapacity": "容量不足",
|
||||||
|
"considerWorkloadRedistribution": "考虑工作负载重新分配",
|
||||||
|
"capacityAvailableForNewProjects": "可用于新项目的容量",
|
||||||
|
"optimal": "最佳",
|
||||||
|
"underUtilized": "利用率不足",
|
||||||
|
"overUtilized": "过度利用"
|
||||||
}
|
}
|
||||||
@@ -58,5 +58,20 @@
|
|||||||
"showSelected": "Shfaq Vetëm të Zgjedhurat",
|
"showSelected": "Shfaq Vetëm të Zgjedhurat",
|
||||||
"expandAll": "Zgjero të Gjitha",
|
"expandAll": "Zgjero të Gjitha",
|
||||||
"collapseAll": "Mbyll të Gjitha",
|
"collapseAll": "Mbyll të Gjitha",
|
||||||
"ungrouped": "Pa Grupuar"
|
"ungrouped": "Pa Grupuar",
|
||||||
|
|
||||||
|
"totalTimeLogged": "Koha Totale e Regjistruar",
|
||||||
|
"acrossAllTeamMembers": "Në të gjithë anëtarët e ekipit",
|
||||||
|
"expectedCapacity": "Kapaciteti i Pritur",
|
||||||
|
"basedOnWorkingSchedule": "Bazuar në orarin e punës",
|
||||||
|
"teamUtilization": "Përdorimi i Ekipit",
|
||||||
|
"targetRange": "Gama e Objektivit",
|
||||||
|
"variance": "Varianca",
|
||||||
|
"overCapacity": "Mbi Kapacitetin",
|
||||||
|
"underCapacity": "Nën Kapacitetin",
|
||||||
|
"considerWorkloadRedistribution": "Konsidero rishpërndarjen e ngarkesës së punës",
|
||||||
|
"capacityAvailableForNewProjects": "Kapaciteti i disponueshëm për projekte të reja",
|
||||||
|
"optimal": "Optimal",
|
||||||
|
"underUtilized": "I Përdorur Pak",
|
||||||
|
"overUtilized": "I Përdorur Shumë"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,5 +58,20 @@
|
|||||||
"showSelected": "Nur Ausgewählte anzeigen",
|
"showSelected": "Nur Ausgewählte anzeigen",
|
||||||
"expandAll": "Alle erweitern",
|
"expandAll": "Alle erweitern",
|
||||||
"collapseAll": "Alle einklappen",
|
"collapseAll": "Alle einklappen",
|
||||||
"ungrouped": "Nicht gruppiert"
|
"ungrouped": "Nicht gruppiert",
|
||||||
|
|
||||||
|
"totalTimeLogged": "Gesamte erfasste Zeit",
|
||||||
|
"acrossAllTeamMembers": "Über alle Teammitglieder",
|
||||||
|
"expectedCapacity": "Erwartete Kapazität",
|
||||||
|
"basedOnWorkingSchedule": "Basierend auf Arbeitsplan",
|
||||||
|
"teamUtilization": "Team-Auslastung",
|
||||||
|
"targetRange": "Zielbereich",
|
||||||
|
"variance": "Abweichung",
|
||||||
|
"overCapacity": "Überkapazität",
|
||||||
|
"underCapacity": "Unterkapazität",
|
||||||
|
"considerWorkloadRedistribution": "Arbeitslast-Umverteilung erwägen",
|
||||||
|
"capacityAvailableForNewProjects": "Kapazität für neue Projekte verfügbar",
|
||||||
|
"optimal": "Optimal",
|
||||||
|
"underUtilized": "Unterausgelastet",
|
||||||
|
"overUtilized": "Überausgelastet"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,5 +59,20 @@
|
|||||||
"showSelected": "Show Selected Only",
|
"showSelected": "Show Selected Only",
|
||||||
"expandAll": "Expand All",
|
"expandAll": "Expand All",
|
||||||
"collapseAll": "Collapse All",
|
"collapseAll": "Collapse All",
|
||||||
"ungrouped": "Ungrouped"
|
"ungrouped": "Ungrouped",
|
||||||
|
|
||||||
|
"totalTimeLogged": "Total Time Logged",
|
||||||
|
"acrossAllTeamMembers": "Across all team members",
|
||||||
|
"expectedCapacity": "Expected Capacity",
|
||||||
|
"basedOnWorkingSchedule": "Based on working schedule",
|
||||||
|
"teamUtilization": "Team Utilization",
|
||||||
|
"targetRange": "Target Range",
|
||||||
|
"variance": "Variance",
|
||||||
|
"overCapacity": "Over Capacity",
|
||||||
|
"underCapacity": "Under Capacity",
|
||||||
|
"considerWorkloadRedistribution": "Consider workload redistribution",
|
||||||
|
"capacityAvailableForNewProjects": "Capacity available for new projects",
|
||||||
|
"optimal": "Optimal",
|
||||||
|
"underUtilized": "Under Utilized",
|
||||||
|
"overUtilized": "Over Utilized"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,5 +59,20 @@
|
|||||||
"showSelected": "Mostrar Solo Seleccionados",
|
"showSelected": "Mostrar Solo Seleccionados",
|
||||||
"expandAll": "Expandir Todo",
|
"expandAll": "Expandir Todo",
|
||||||
"collapseAll": "Contraer Todo",
|
"collapseAll": "Contraer Todo",
|
||||||
"ungrouped": "Sin Agrupar"
|
"ungrouped": "Sin Agrupar",
|
||||||
|
|
||||||
|
"totalTimeLogged": "Tiempo Total Registrado",
|
||||||
|
"acrossAllTeamMembers": "En todos los miembros del equipo",
|
||||||
|
"expectedCapacity": "Capacidad Esperada",
|
||||||
|
"basedOnWorkingSchedule": "Basado en el horario de trabajo",
|
||||||
|
"teamUtilization": "Utilización del Equipo",
|
||||||
|
"targetRange": "Rango Objetivo",
|
||||||
|
"variance": "Varianza",
|
||||||
|
"overCapacity": "Sobre Capacidad",
|
||||||
|
"underCapacity": "Bajo Capacidad",
|
||||||
|
"considerWorkloadRedistribution": "Considerar redistribución de carga de trabajo",
|
||||||
|
"capacityAvailableForNewProjects": "Capacidad disponible para nuevos proyectos",
|
||||||
|
"optimal": "Óptimo",
|
||||||
|
"underUtilized": "Subutilizado",
|
||||||
|
"overUtilized": "Sobreutilizado"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,5 +59,20 @@
|
|||||||
"showSelected": "Mostrar Apenas Selecionados",
|
"showSelected": "Mostrar Apenas Selecionados",
|
||||||
"expandAll": "Expandir Tudo",
|
"expandAll": "Expandir Tudo",
|
||||||
"collapseAll": "Recolher Tudo",
|
"collapseAll": "Recolher Tudo",
|
||||||
"ungrouped": "Não Agrupado"
|
"ungrouped": "Não Agrupado",
|
||||||
|
|
||||||
|
"totalTimeLogged": "Tempo Total Registrado",
|
||||||
|
"acrossAllTeamMembers": "Em todos os membros da equipe",
|
||||||
|
"expectedCapacity": "Capacidade Esperada",
|
||||||
|
"basedOnWorkingSchedule": "Baseado no cronograma de trabalho",
|
||||||
|
"teamUtilization": "Utilização da Equipe",
|
||||||
|
"targetRange": "Faixa Alvo",
|
||||||
|
"variance": "Variância",
|
||||||
|
"overCapacity": "Sobre Capacidade",
|
||||||
|
"underCapacity": "Abaixo da Capacidade",
|
||||||
|
"considerWorkloadRedistribution": "Considerar redistribuição de carga de trabalho",
|
||||||
|
"capacityAvailableForNewProjects": "Capacidade disponível para novos projetos",
|
||||||
|
"optimal": "Ótimo",
|
||||||
|
"underUtilized": "Subutilizado",
|
||||||
|
"overUtilized": "Sobreutilizado"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,5 +47,20 @@
|
|||||||
"showSelected": "仅显示已选择",
|
"showSelected": "仅显示已选择",
|
||||||
"expandAll": "全部展开",
|
"expandAll": "全部展开",
|
||||||
"collapseAll": "全部折叠",
|
"collapseAll": "全部折叠",
|
||||||
"ungrouped": "未分组"
|
"ungrouped": "未分组",
|
||||||
|
|
||||||
|
"totalTimeLogged": "总记录时间",
|
||||||
|
"acrossAllTeamMembers": "跨所有团队成员",
|
||||||
|
"expectedCapacity": "预期容量",
|
||||||
|
"basedOnWorkingSchedule": "基于工作时间表",
|
||||||
|
"teamUtilization": "团队利用率",
|
||||||
|
"targetRange": "目标范围",
|
||||||
|
"variance": "差异",
|
||||||
|
"overCapacity": "超出容量",
|
||||||
|
"underCapacity": "容量不足",
|
||||||
|
"considerWorkloadRedistribution": "考虑工作负载重新分配",
|
||||||
|
"capacityAvailableForNewProjects": "可用于新项目的容量",
|
||||||
|
"optimal": "最佳",
|
||||||
|
"underUtilized": "利用率不足",
|
||||||
|
"overUtilized": "过度利用"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Flex,
|
||||||
|
Progress,
|
||||||
|
Tooltip,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
ArrowUpOutlined,
|
||||||
|
ArrowDownOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
} from '@/shared/antd-imports';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { IRPTTimeTotals } from '@/types/reporting/reporting.types';
|
||||||
|
|
||||||
|
interface TotalTimeUtilizationProps {
|
||||||
|
totals: IRPTTimeTotals;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TotalTimeUtilization: React.FC<TotalTimeUtilizationProps> = ({ totals }) => {
|
||||||
|
const { t } = useTranslation('time-report');
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
const isDark = themeMode === 'dark';
|
||||||
|
|
||||||
|
const utilizationData = useMemo(() => {
|
||||||
|
const timeLogged = parseFloat(totals.total_time_logs || '0');
|
||||||
|
const estimatedHours = parseFloat(totals.total_estimated_hours || '0');
|
||||||
|
const utilizationPercent = parseFloat(totals.total_utilization || '0');
|
||||||
|
|
||||||
|
// Determine utilization status and color
|
||||||
|
let status: 'under' | 'optimal' | 'over' = 'optimal';
|
||||||
|
let statusColor = '#52c41a'; // Green
|
||||||
|
let statusIcon = <CheckCircleOutlined />;
|
||||||
|
let statusText = t('optimal');
|
||||||
|
|
||||||
|
if (utilizationPercent < 90) {
|
||||||
|
status = 'under';
|
||||||
|
statusColor = '#faad14'; // Orange
|
||||||
|
statusIcon = <ArrowDownOutlined />;
|
||||||
|
statusText = t('underUtilized');
|
||||||
|
} else if (utilizationPercent > 110) {
|
||||||
|
status = 'over';
|
||||||
|
statusColor = '#ff4d4f'; // Red
|
||||||
|
statusIcon = <ArrowUpOutlined />;
|
||||||
|
statusText = t('overUtilized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timeLogged,
|
||||||
|
estimatedHours,
|
||||||
|
utilizationPercent,
|
||||||
|
status,
|
||||||
|
statusColor,
|
||||||
|
statusIcon,
|
||||||
|
statusText,
|
||||||
|
};
|
||||||
|
}, [totals, t]);
|
||||||
|
|
||||||
|
const getThemeColors = useMemo(
|
||||||
|
() => ({
|
||||||
|
cardBackground: isDark ? '#1f1f1f' : '#ffffff',
|
||||||
|
cardBorder: isDark ? '#303030' : '#f0f0f0',
|
||||||
|
cardShadow: isDark ? '0 2px 8px rgba(0, 0, 0, 0.3)' : '0 2px 8px rgba(0, 0, 0, 0.06)',
|
||||||
|
cardHoverShadow: isDark ? '0 4px 16px rgba(0, 0, 0, 0.5)' : '0 4px 16px rgba(0, 0, 0, 0.12)',
|
||||||
|
primaryText: isDark ? '#ffffff' : '#262626',
|
||||||
|
secondaryText: isDark ? '#bfbfbf' : '#8c8c8c',
|
||||||
|
tertiaryText: isDark ? '#8c8c8c' : '#595959',
|
||||||
|
iconBackgrounds: {
|
||||||
|
blue: isDark ? '#0f1419' : '#e6f7ff',
|
||||||
|
green: isDark ? '#0f1b0f' : '#f6ffed',
|
||||||
|
},
|
||||||
|
iconColors: {
|
||||||
|
blue: isDark ? '#40a9ff' : '#1890ff',
|
||||||
|
green: isDark ? '#73d13d' : '#52c41a',
|
||||||
|
},
|
||||||
|
progressTrail: isDark ? '#262626' : '#f5f5f5',
|
||||||
|
varianceBackgrounds: {
|
||||||
|
positive: isDark ? '#0f1b0f' : '#f6ffed',
|
||||||
|
negative: isDark ? '#1f0f0f' : '#fff2f0',
|
||||||
|
},
|
||||||
|
varianceColors: {
|
||||||
|
positive: isDark ? '#73d13d' : '#389e0d',
|
||||||
|
negative: isDark ? '#ff7875' : '#a8071a',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[isDark]
|
||||||
|
);
|
||||||
|
|
||||||
|
const cardStyle = {
|
||||||
|
borderRadius: '8px',
|
||||||
|
flex: 1,
|
||||||
|
boxShadow: getThemeColors.cardShadow,
|
||||||
|
border: `1px solid ${getThemeColors.cardBorder}`,
|
||||||
|
backgroundColor: getThemeColors.cardBackground,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap={16} style={{ marginBottom: '16px' }}>
|
||||||
|
{/* Total Time Logs Card */}
|
||||||
|
<Card
|
||||||
|
style={cardStyle}
|
||||||
|
styles={{
|
||||||
|
body: { padding: '20px' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex align="center" gap={12}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: '12px',
|
||||||
|
backgroundColor: getThemeColors.iconBackgrounds.blue,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '20px',
|
||||||
|
color: getThemeColors.iconColors.blue,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ClockCircleOutlined />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: getThemeColors.secondaryText,
|
||||||
|
fontWeight: 500,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('totalTimeLogged')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: getThemeColors.primaryText,
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{totals.total_time_logs}h
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: getThemeColors.tertiaryText,
|
||||||
|
marginTop: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('acrossAllTeamMembers')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Estimated Hours Card */}
|
||||||
|
<Card
|
||||||
|
style={cardStyle}
|
||||||
|
styles={{
|
||||||
|
body: { padding: '20px' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex align="center" gap={12}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: '12px',
|
||||||
|
backgroundColor: getThemeColors.iconBackgrounds.green,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '20px',
|
||||||
|
color: getThemeColors.iconColors.green,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CalendarOutlined />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: getThemeColors.secondaryText,
|
||||||
|
fontWeight: 500,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('expectedCapacity')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: getThemeColors.primaryText,
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{totals.total_estimated_hours}h
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: getThemeColors.tertiaryText,
|
||||||
|
marginTop: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('basedOnWorkingSchedule')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Utilization Card with Progress */}
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
...cardStyle,
|
||||||
|
borderColor: utilizationData.statusColor,
|
||||||
|
borderWidth: '2px',
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
body: { padding: '20px' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex align="center" gap={12}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: '12px',
|
||||||
|
backgroundColor: `${utilizationData.statusColor}${isDark ? '20' : '15'}`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '20px',
|
||||||
|
color: utilizationData.statusColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{utilizationData.statusIcon}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<Flex justify="space-between" align="center" style={{ marginBottom: '4px' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: getThemeColors.secondaryText,
|
||||||
|
fontWeight: 500,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('teamUtilization')}
|
||||||
|
</div>
|
||||||
|
<Tooltip title={`${utilizationData.statusText} (${t('targetRange')})`}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
color: utilizationData.statusColor,
|
||||||
|
fontWeight: 600,
|
||||||
|
backgroundColor: `${utilizationData.statusColor}${isDark ? '20' : '15'}`,
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{utilizationData.statusText}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: utilizationData.statusColor,
|
||||||
|
lineHeight: 1,
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{totals.total_utilization}%
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
percent={Math.min(utilizationData.utilizationPercent, 150)} // Cap at 150% for display
|
||||||
|
strokeColor={{
|
||||||
|
'0%': utilizationData.statusColor,
|
||||||
|
'100%': utilizationData.statusColor,
|
||||||
|
}}
|
||||||
|
trailColor={getThemeColors.progressTrail}
|
||||||
|
strokeWidth={6}
|
||||||
|
showInfo={false}
|
||||||
|
style={{ marginBottom: '4px' }}
|
||||||
|
/>
|
||||||
|
<Flex
|
||||||
|
justify="space-between"
|
||||||
|
style={{ fontSize: 10, color: getThemeColors.secondaryText }}
|
||||||
|
>
|
||||||
|
<span>0%</span>
|
||||||
|
<span style={{ color: '#52c41a' }}>90% - 110%</span>
|
||||||
|
<span>150%+</span>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Additional Insights Card */}
|
||||||
|
<Card
|
||||||
|
style={cardStyle}
|
||||||
|
styles={{
|
||||||
|
body: { padding: '20px' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: getThemeColors.secondaryText,
|
||||||
|
fontWeight: 500,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
marginBottom: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('variance')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 700,
|
||||||
|
color:
|
||||||
|
utilizationData.timeLogged > utilizationData.estimatedHours
|
||||||
|
? getThemeColors.varianceColors.negative
|
||||||
|
: getThemeColors.varianceColors.positive,
|
||||||
|
lineHeight: 1,
|
||||||
|
marginBottom: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{utilizationData.timeLogged > utilizationData.estimatedHours ? '+' : ''}
|
||||||
|
{(utilizationData.timeLogged - utilizationData.estimatedHours).toFixed(1)}h
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: getThemeColors.tertiaryText,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{utilizationData.timeLogged > utilizationData.estimatedHours
|
||||||
|
? t('overCapacity')
|
||||||
|
: t('underCapacity')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '8px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor:
|
||||||
|
utilizationData.timeLogged > utilizationData.estimatedHours
|
||||||
|
? getThemeColors.varianceBackgrounds.negative
|
||||||
|
: getThemeColors.varianceBackgrounds.positive,
|
||||||
|
fontSize: 10,
|
||||||
|
color:
|
||||||
|
utilizationData.timeLogged > utilizationData.estimatedHours
|
||||||
|
? getThemeColors.varianceColors.negative
|
||||||
|
: getThemeColors.varianceColors.positive,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{utilizationData.timeLogged > utilizationData.estimatedHours
|
||||||
|
? t('considerWorkloadRedistribution')
|
||||||
|
: t('capacityAvailableForNewProjects')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TotalTimeUtilization;
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
import { Card, Flex } from '@/shared/antd-imports';
|
import { Card, Flex } from 'antd';
|
||||||
import TimeReportPageHeader from '@/components/reporting/time-reports/page-header/TimeReportPageHeader';
|
import MembersTimeSheet, { MembersTimeSheetRef } from '@/pages/reporting/time-reports/members-time-sheet/members-time-sheet';
|
||||||
import MembersTimeSheet, {
|
|
||||||
MembersTimeSheetRef,
|
|
||||||
} from '@/pages/reporting/time-reports/members-time-sheet/members-time-sheet';
|
|
||||||
import TimeReportingRightHeader from '@/components/reporting/time-reports/right-header/TimeReportingRightHeader';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
import { useDocumentTitle } from '@/hooks/useDoumentTItle';
|
||||||
import { useRef } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
import { IRPTTimeTotals } from '@/types/reporting/reporting.types';
|
||||||
|
import TimeReportingRightHeader from '@/components/reporting/time-reports/right-header/TimeReportingRightHeader';
|
||||||
|
import TimeReportPageHeader from '@/components/reporting/time-reports/page-header/TimeReportPageHeader';
|
||||||
|
import TotalTimeUtilization from '@/components/reporting/time-reports/total-time-utilization/total-time-utilization';
|
||||||
|
|
||||||
const MembersTimeReports = () => {
|
const MembersTimeReports = () => {
|
||||||
const { t } = useTranslation('time-report');
|
const { t } = useTranslation('time-report');
|
||||||
const chartRef = useRef<MembersTimeSheetRef>(null);
|
const chartRef = useRef<MembersTimeSheetRef>(null);
|
||||||
|
const [totals, setTotals] = useState<IRPTTimeTotals>({
|
||||||
|
total_time_logs: "0",
|
||||||
|
total_estimated_hours: "0",
|
||||||
|
total_utilization: "0",
|
||||||
|
});
|
||||||
useDocumentTitle('Reporting - Allocation');
|
useDocumentTitle('Reporting - Allocation');
|
||||||
|
|
||||||
const handleExport = (type: string) => {
|
const handleExport = (type: string) => {
|
||||||
@@ -20,24 +24,18 @@ const MembersTimeReports = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTotalsUpdate = (totals: {
|
const handleTotalsUpdate = (newTotals: IRPTTimeTotals) => {
|
||||||
total_time_logs: string;
|
setTotals(newTotals);
|
||||||
total_estimated_hours: string;
|
|
||||||
total_utilization: string;
|
|
||||||
}) => {
|
|
||||||
// Handle totals update if needed
|
|
||||||
// This could be used to display totals in the UI or pass to parent components
|
|
||||||
console.log('Totals updated:', totals);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
<TimeReportingRightHeader
|
<TimeReportingRightHeader
|
||||||
title={t('membersTimeSheet')}
|
title={t('Members Time Sheet')}
|
||||||
exportType={[{ key: 'png', label: 'PNG' }]}
|
exportType={[{ key: 'png', label: 'PNG' }]}
|
||||||
export={handleExport}
|
export={handleExport}
|
||||||
/>
|
/>
|
||||||
|
<TotalTimeUtilization totals={totals} />
|
||||||
<Card
|
<Card
|
||||||
style={{ borderRadius: '4px' }}
|
style={{ borderRadius: '4px' }}
|
||||||
title={
|
title={
|
||||||
@@ -53,7 +51,7 @@ const MembersTimeReports = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MembersTimeSheet ref={chartRef} onTotalsUpdate={handleTotalsUpdate} />
|
<MembersTimeSheet onTotalsUpdate={handleTotalsUpdate} ref={chartRef} />
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -181,7 +181,9 @@ export {
|
|||||||
InfoCircleOutlined,
|
InfoCircleOutlined,
|
||||||
WarningTwoTone,
|
WarningTwoTone,
|
||||||
ShareAltOutlined,
|
ShareAltOutlined,
|
||||||
CloudDownloadOutlined
|
CloudDownloadOutlined,
|
||||||
|
ArrowUpOutlined,
|
||||||
|
ArrowDownOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
// Re-export all components with React
|
// Re-export all components with React
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ export interface IRPTMember {
|
|||||||
ongoing: number;
|
ongoing: number;
|
||||||
todo: number;
|
todo: number;
|
||||||
member_teams: any;
|
member_teams: any;
|
||||||
|
billable_time?: number;
|
||||||
|
non_billable_time?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISingleMemberLogs {
|
export interface ISingleMemberLogs {
|
||||||
@@ -409,6 +411,16 @@ export interface IRPTTimeMember {
|
|||||||
utilized_hours?: string;
|
utilized_hours?: string;
|
||||||
utilization_percent?: string;
|
utilization_percent?: string;
|
||||||
over_under_utilized_hours?: string;
|
over_under_utilized_hours?: string;
|
||||||
|
utilization_state?: string;
|
||||||
|
}
|
||||||
|
export interface IRPTTimeTotals {
|
||||||
|
total_estimated_hours?: string;
|
||||||
|
total_time_logs?: string;
|
||||||
|
total_utilization?: string;
|
||||||
|
}
|
||||||
|
export interface IRPTTimeMemberViewModel {
|
||||||
|
filteredRows?: IRPTTimeMember[];
|
||||||
|
totals?: IRPTTimeTotals;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMemberTaskStatGroupResonse {
|
export interface IMemberTaskStatGroupResonse {
|
||||||
|
|||||||
Reference in New Issue
Block a user