feat(time-report-localization): enhance English, Spanish, and Portuguese translations for time reporting

- Added new localization keys for total time logged, expected capacity, team utilization, variance, and related terms in English, Spanish, and Portuguese JSON files.
- Updated the Total Time Utilization component to utilize new translations and improve UI elements for better user experience.
- Enhanced theme support for card styles and progress indicators based on utilization status.
This commit is contained in:
chamikaJ
2025-06-02 16:51:29 +05:30
parent b6be411162
commit 3f7b969e44
4 changed files with 373 additions and 20 deletions

View File

@@ -43,6 +43,20 @@
"noData": "No data found",
"members": "Members",
"searchByMember": "Search by member",
"utilization": "Utilization"
"utilization": "Utilization",
"totalTimeLogged": "Total Time Logged",
"expectedCapacity": "Expected Capacity",
"teamUtilization": "Team Utilization",
"variance": "Variance",
"acrossAllTeamMembers": "Across all team members",
"basedOnWorkingSchedule": "Based on working schedule",
"optimal": "Optimal",
"underUtilized": "Under-utilized",
"overUtilized": "Over-utilized",
"overCapacity": "Over capacity",
"underCapacity": "Under capacity",
"considerWorkloadRedistribution": "Consider workload redistribution",
"capacityAvailableForNewProjects": "Capacity available for new projects",
"targetRange": "Target: 90-110%"
}

View File

@@ -40,5 +40,23 @@
"noCategory": "No Categoría",
"noProjects": "No se encontraron proyectos",
"noTeams": "No se encontraron equipos",
"noData": "No se encontraron datos"
"noData": "No se encontraron datos",
"members": "Miembros",
"searchByMember": "Buscar por miembro",
"utilization": "Utilización",
"totalTimeLogged": "Tiempo Total Registrado",
"expectedCapacity": "Capacidad Esperada",
"teamUtilization": "Utilización del Equipo",
"variance": "Varianza",
"acrossAllTeamMembers": "En todos los miembros del equipo",
"basedOnWorkingSchedule": "Basado en horario de trabajo",
"optimal": "Óptimo",
"underUtilized": "Sub-utilizado",
"overUtilized": "Sobre-utilizado",
"overCapacity": "Sobre capacidad",
"underCapacity": "Bajo capacidad",
"considerWorkloadRedistribution": "Considerar redistribución de carga de trabajo",
"capacityAvailableForNewProjects": "Capacidad disponible para nuevos proyectos",
"targetRange": "Objetivo: 90-110%"
}

View File

@@ -40,5 +40,23 @@
"noCategory": "Nenhuma Categoria",
"noProjects": "Nenhum projeto encontrado",
"noTeams": "Nenhum time encontrado",
"noData": "Nenhum dado encontrado"
"noData": "Nenhum dado encontrado",
"members": "Membros",
"searchByMember": "Pesquisar por membro",
"utilization": "Utilização",
"totalTimeLogged": "Tempo Total Registrado",
"expectedCapacity": "Capacidade Esperada",
"teamUtilization": "Utilização da Equipe",
"variance": "Variância",
"acrossAllTeamMembers": "Em todos os membros da equipe",
"basedOnWorkingSchedule": "Baseado no horário de trabalho",
"optimal": "Ótimo",
"underUtilized": "Sub-utilizado",
"overUtilized": "Super-utilizado",
"overCapacity": "Acima da capacidade",
"underCapacity": "Abaixo da capacidade",
"considerWorkloadRedistribution": "Considerar redistribuição da carga de trabalho",
"capacityAvailableForNewProjects": "Capacidade disponível para novos projetos",
"targetRange": "Meta: 90-110%"
}

View File

@@ -1,30 +1,333 @@
import { Card, Flex } from 'antd';
import React, { useEffect } from 'react';
import { Card, Flex, Progress, Tooltip } from 'antd';
import React, { useMemo } from 'react';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useTranslation } from 'react-i18next';
import { IRPTTimeTotals } from '@/types/reporting/reporting.types';
import { ClockCircleOutlined, CalendarOutlined, PercentageOutlined, ArrowUpOutlined, ArrowDownOutlined, CheckCircleOutlined } from '@ant-design/icons';
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' }}>
<Card style={{ borderRadius: '4px', flex: 1 }}>
<div>
<div style={{ fontSize: 14, color: '#888' }}>Total Time Logs</div>
<div style={{ fontSize: 24, fontWeight: 600 }}>{totals.total_time_logs}h</div>
</div>
{/* 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>
<Card style={{ borderRadius: '4px', flex: 1 }}>
<div>
<div style={{ fontSize: 14, color: '#888' }}>Estimated Hours</div>
<div style={{ fontSize: 24, fontWeight: 600 }}>{totals.total_estimated_hours}h</div>
</div>
{/* 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>
<Card style={{ borderRadius: '4px', flex: 1 }}>
<div>
<div style={{ fontSize: 14, color: '#888' }}>Utilization (%)</div>
<div style={{ fontSize: 24, fontWeight: 600 }}>{totals.total_utilization}%</div>
{/* 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>