diff --git a/worklenz-backend/src/public/locales/zh/time-report.json b/worklenz-backend/src/public/locales/zh/time-report.json index 0fd2104a..693dfa79 100644 --- a/worklenz-backend/src/public/locales/zh/time-report.json +++ b/worklenz-backend/src/public/locales/zh/time-report.json @@ -46,5 +46,20 @@ "filterByBillableStatus": "按计费状态筛选", "searchByMember": "按成员搜索", "members": "成员", - "utilization": "利用率" + "utilization": "利用率", + + "totalTimeLogged": "总记录时间", + "acrossAllTeamMembers": "跨所有团队成员", + "expectedCapacity": "预期容量", + "basedOnWorkingSchedule": "基于工作时间表", + "teamUtilization": "团队利用率", + "targetRange": "目标范围", + "variance": "差异", + "overCapacity": "超出容量", + "underCapacity": "容量不足", + "considerWorkloadRedistribution": "考虑工作负载重新分配", + "capacityAvailableForNewProjects": "可用于新项目的容量", + "optimal": "最佳", + "underUtilized": "利用率不足", + "overUtilized": "过度利用" } \ No newline at end of file diff --git a/worklenz-frontend/public/locales/alb/time-report.json b/worklenz-frontend/public/locales/alb/time-report.json index 027337d6..7991db8e 100644 --- a/worklenz-frontend/public/locales/alb/time-report.json +++ b/worklenz-frontend/public/locales/alb/time-report.json @@ -58,5 +58,20 @@ "showSelected": "Shfaq Vetëm të Zgjedhurat", "expandAll": "Zgjero 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ë" } diff --git a/worklenz-frontend/public/locales/de/time-report.json b/worklenz-frontend/public/locales/de/time-report.json index 20a9f223..825f889d 100644 --- a/worklenz-frontend/public/locales/de/time-report.json +++ b/worklenz-frontend/public/locales/de/time-report.json @@ -58,5 +58,20 @@ "showSelected": "Nur Ausgewählte anzeigen", "expandAll": "Alle erweitern", "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" } diff --git a/worklenz-frontend/public/locales/en/time-report.json b/worklenz-frontend/public/locales/en/time-report.json index f805324b..0e1afd07 100644 --- a/worklenz-frontend/public/locales/en/time-report.json +++ b/worklenz-frontend/public/locales/en/time-report.json @@ -59,5 +59,20 @@ "showSelected": "Show Selected Only", "expandAll": "Expand 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" } diff --git a/worklenz-frontend/public/locales/es/time-report.json b/worklenz-frontend/public/locales/es/time-report.json index defde12d..e11efc0a 100644 --- a/worklenz-frontend/public/locales/es/time-report.json +++ b/worklenz-frontend/public/locales/es/time-report.json @@ -59,5 +59,20 @@ "showSelected": "Mostrar Solo Seleccionados", "expandAll": "Expandir 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" } diff --git a/worklenz-frontend/public/locales/pt/time-report.json b/worklenz-frontend/public/locales/pt/time-report.json index 20520612..e3e0cf2a 100644 --- a/worklenz-frontend/public/locales/pt/time-report.json +++ b/worklenz-frontend/public/locales/pt/time-report.json @@ -59,5 +59,20 @@ "showSelected": "Mostrar Apenas Selecionados", "expandAll": "Expandir 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" } diff --git a/worklenz-frontend/public/locales/zh/time-report.json b/worklenz-frontend/public/locales/zh/time-report.json index 941e642a..18d5f625 100644 --- a/worklenz-frontend/public/locales/zh/time-report.json +++ b/worklenz-frontend/public/locales/zh/time-report.json @@ -47,5 +47,20 @@ "showSelected": "仅显示已选择", "expandAll": "全部展开", "collapseAll": "全部折叠", - "ungrouped": "未分组" + "ungrouped": "未分组", + + "totalTimeLogged": "总记录时间", + "acrossAllTeamMembers": "跨所有团队成员", + "expectedCapacity": "预期容量", + "basedOnWorkingSchedule": "基于工作时间表", + "teamUtilization": "团队利用率", + "targetRange": "目标范围", + "variance": "差异", + "overCapacity": "超出容量", + "underCapacity": "容量不足", + "considerWorkloadRedistribution": "考虑工作负载重新分配", + "capacityAvailableForNewProjects": "可用于新项目的容量", + "optimal": "最佳", + "underUtilized": "利用率不足", + "overUtilized": "过度利用" } \ No newline at end of file diff --git a/worklenz-frontend/src/components/reporting/time-reports/total-time-utilization/total-time-utilization.tsx b/worklenz-frontend/src/components/reporting/time-reports/total-time-utilization/total-time-utilization.tsx new file mode 100644 index 00000000..47ba171a --- /dev/null +++ b/worklenz-frontend/src/components/reporting/time-reports/total-time-utilization/total-time-utilization.tsx @@ -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 = ({ 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 = ; + let statusText = t('optimal'); + + if (utilizationPercent < 90) { + status = 'under'; + statusColor = '#faad14'; // Orange + statusIcon = ; + statusText = t('underUtilized'); + } else if (utilizationPercent > 110) { + status = 'over'; + statusColor = '#ff4d4f'; // Red + statusIcon = ; + 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 ( + + {/* Total Time Logs Card */} + + +
+ +
+
+
+ {t('totalTimeLogged')} +
+
+ {totals.total_time_logs}h +
+
+ {t('acrossAllTeamMembers')} +
+
+
+
+ + {/* Estimated Hours Card */} + + +
+ +
+
+
+ {t('expectedCapacity')} +
+
+ {totals.total_estimated_hours}h +
+
+ {t('basedOnWorkingSchedule')} +
+
+
+
+ + {/* Utilization Card with Progress */} + + +
+ {utilizationData.statusIcon} +
+
+ +
+ {t('teamUtilization')} +
+ +
+ {utilizationData.statusText} +
+
+
+
+ {totals.total_utilization}% +
+ + + 0% + 90% - 110% + 150%+ + +
+
+
+ + {/* Additional Insights Card */} + +
+
+ {t('variance')} +
+
utilizationData.estimatedHours + ? getThemeColors.varianceColors.negative + : getThemeColors.varianceColors.positive, + lineHeight: 1, + marginBottom: '4px', + }} + > + {utilizationData.timeLogged > utilizationData.estimatedHours ? '+' : ''} + {(utilizationData.timeLogged - utilizationData.estimatedHours).toFixed(1)}h +
+
+ {utilizationData.timeLogged > utilizationData.estimatedHours + ? t('overCapacity') + : t('underCapacity')} +
+
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')} +
+
+
+
+ ); +}; + +export default TotalTimeUtilization; diff --git a/worklenz-frontend/src/pages/reporting/timeReports/members-time-reports.tsx b/worklenz-frontend/src/pages/reporting/timeReports/members-time-reports.tsx index 4adfc249..12b32b0b 100644 --- a/worklenz-frontend/src/pages/reporting/timeReports/members-time-reports.tsx +++ b/worklenz-frontend/src/pages/reporting/timeReports/members-time-reports.tsx @@ -1,17 +1,21 @@ -import { Card, Flex } from '@/shared/antd-imports'; -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 TimeReportingRightHeader from '@/components/reporting/time-reports/right-header/TimeReportingRightHeader'; +import { Card, Flex } from 'antd'; +import MembersTimeSheet, { MembersTimeSheetRef } from '@/pages/reporting/time-reports/members-time-sheet/members-time-sheet'; import { useTranslation } from 'react-i18next'; 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 { t } = useTranslation('time-report'); const chartRef = useRef(null); - + const [totals, setTotals] = useState({ + total_time_logs: "0", + total_estimated_hours: "0", + total_utilization: "0", + }); useDocumentTitle('Reporting - Allocation'); const handleExport = (type: string) => { @@ -20,24 +24,18 @@ const MembersTimeReports = () => { } }; - const handleTotalsUpdate = (totals: { - total_time_logs: string; - 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); + const handleTotalsUpdate = (newTotals: IRPTTimeTotals) => { + setTotals(newTotals); }; return ( - + { }, }} > - + ); diff --git a/worklenz-frontend/src/shared/antd-imports.ts b/worklenz-frontend/src/shared/antd-imports.ts index 1dfd149b..a107af86 100644 --- a/worklenz-frontend/src/shared/antd-imports.ts +++ b/worklenz-frontend/src/shared/antd-imports.ts @@ -181,7 +181,9 @@ export { InfoCircleOutlined, WarningTwoTone, ShareAltOutlined, - CloudDownloadOutlined + CloudDownloadOutlined, + ArrowUpOutlined, + ArrowDownOutlined, } from '@ant-design/icons'; // Re-export all components with React diff --git a/worklenz-frontend/src/types/reporting/reporting.types.ts b/worklenz-frontend/src/types/reporting/reporting.types.ts index aa36069c..ebebbdf4 100644 --- a/worklenz-frontend/src/types/reporting/reporting.types.ts +++ b/worklenz-frontend/src/types/reporting/reporting.types.ts @@ -136,6 +136,8 @@ export interface IRPTMember { ongoing: number; todo: number; member_teams: any; + billable_time?: number; + non_billable_time?: number; } export interface ISingleMemberLogs { @@ -409,6 +411,16 @@ export interface IRPTTimeMember { utilized_hours?: string; utilization_percent?: 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 {