import { Button, ConfigProvider, Flex, Select, Typography, message, Alert, Card, Row, Col, Statistic } from 'antd'; import { useEffect, useState, useMemo, useCallback } from 'react'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { CaretDownFilled, DownOutlined, CalculatorOutlined } from '@ant-design/icons'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppSelector } from '@/hooks/useAppSelector'; import { fetchProjectFinances, setActiveTab, setActiveGroup, updateProjectFinanceCurrency, fetchProjectFinancesSilent, setBillableFilter } from '@/features/projects/finance/project-finance.slice'; import { changeCurrency, toggleImportRatecardsDrawer } from '@/features/finance/finance-slice'; import { updateProjectCurrency } from '@/features/project/project.slice'; import { projectFinanceApiService } from '@/api/project-finance-ratecard/project-finance.api.service'; import { RootState } from '@/app/store'; import FinanceTableWrapper from './finance-tab/finance-table/finance-table-wrapper'; import RatecardTable from './ratecard-tab/reatecard-table/ratecard-table'; import ImportRatecardsDrawer from '@/features/finance/ratecard-drawer/import-ratecards-drawer'; import { useAuthService } from '@/hooks/useAuth'; import { hasFinanceEditPermission } from '@/utils/finance-permissions'; import { CURRENCY_OPTIONS, DEFAULT_CURRENCY } from '@/shared/constants/currencies'; import { useSocket } from '@/socket/socketContext'; import { SocketEvents } from '@/shared/socket-events'; const ProjectViewFinance = () => { const { projectId } = useParams<{ projectId: string }>(); const dispatch = useAppDispatch(); const { t } = useTranslation('project-view-finance'); const [exporting, setExporting] = useState(false); const [updatingCurrency, setUpdatingCurrency] = useState(false); const { socket } = useSocket(); const { activeTab, activeGroup, billableFilter, loading, taskGroups, project: financeProject } = useAppSelector((state: RootState) => state.projectFinances); const { refreshTimestamp, project } = useAppSelector((state: RootState) => state.projectReducer); const phaseList = useAppSelector((state) => state.phaseReducer.phaseList); // Auth and permissions const auth = useAuthService(); const currentSession = auth.getCurrentSession(); const hasEditPermission = hasFinanceEditPermission(currentSession, project); // Get project-specific currency from finance API response, fallback to project reducer, then default const projectCurrency = (financeProject?.currency || project?.currency || DEFAULT_CURRENCY).toLowerCase(); // Show loading state for currency selector until finance data is loaded const currencyLoading = loading || updatingCurrency || !financeProject; // Calculate project budget statistics const budgetStatistics = useMemo(() => { if (!taskGroups || taskGroups.length === 0) { return { totalEstimatedCost: 0, totalFixedCost: 0, totalBudget: 0, totalActualCost: 0, totalVariance: 0, budgetUtilization: 0 }; } const calculateTaskTotalsRecursively = (tasks: any[]): any => { return tasks.reduce((acc, task) => { // Add current task values const taskTotals = { totalEstimatedCost: acc.totalEstimatedCost + (task.estimated_cost || 0), totalFixedCost: acc.totalFixedCost + (task.fixed_cost || 0), totalBudget: acc.totalBudget + (task.total_budget || 0), totalActualCost: acc.totalActualCost + (task.total_actual || 0), totalVariance: acc.totalVariance + (task.variance || 0) }; // If task has subtasks, recursively add their totals if (task.sub_tasks && task.sub_tasks.length > 0) { const subTaskTotals = calculateTaskTotalsRecursively(task.sub_tasks); return { totalEstimatedCost: taskTotals.totalEstimatedCost + subTaskTotals.totalEstimatedCost, totalFixedCost: taskTotals.totalFixedCost + subTaskTotals.totalFixedCost, totalBudget: taskTotals.totalBudget + subTaskTotals.totalBudget, totalActualCost: taskTotals.totalActualCost + subTaskTotals.totalActualCost, totalVariance: taskTotals.totalVariance + subTaskTotals.totalVariance }; } return taskTotals; }, { totalEstimatedCost: 0, totalFixedCost: 0, totalBudget: 0, totalActualCost: 0, totalVariance: 0 }); }; const totals = taskGroups.reduce((acc, group) => { const groupTotals = calculateTaskTotalsRecursively(group.tasks); return { totalEstimatedCost: acc.totalEstimatedCost + groupTotals.totalEstimatedCost, totalFixedCost: acc.totalFixedCost + groupTotals.totalFixedCost, totalBudget: acc.totalBudget + groupTotals.totalBudget, totalActualCost: acc.totalActualCost + groupTotals.totalActualCost, totalVariance: acc.totalVariance + groupTotals.totalVariance }; }, { totalEstimatedCost: 0, totalFixedCost: 0, totalBudget: 0, totalActualCost: 0, totalVariance: 0 }); const budgetUtilization = totals.totalBudget > 0 ? (totals.totalActualCost / totals.totalBudget) * 100 : 0; return { ...totals, budgetUtilization }; }, [taskGroups]); // Silent refresh function for socket events const refreshFinanceData = useCallback(() => { if (projectId) { dispatch(fetchProjectFinancesSilent({ projectId, groupBy: activeGroup, billableFilter })); } }, [projectId, activeGroup, billableFilter, dispatch]); // Socket event handlers const handleTaskEstimationChange = useCallback(() => { refreshFinanceData(); }, [refreshFinanceData]); const handleTaskTimerStop = useCallback(() => { refreshFinanceData(); }, [refreshFinanceData]); const handleTaskProgressUpdate = useCallback(() => { refreshFinanceData(); }, [refreshFinanceData]); const handleTaskBillableChange = useCallback(() => { refreshFinanceData(); }, [refreshFinanceData]); useEffect(() => { if (projectId) { dispatch(fetchProjectFinances({ projectId, groupBy: activeGroup, billableFilter })); } }, [projectId, activeGroup, billableFilter, dispatch, refreshTimestamp]); // Socket event listeners for finance data refresh useEffect(() => { if (!socket) return; const eventHandlers = [ { event: SocketEvents.TASK_TIME_ESTIMATION_CHANGE.toString(), handler: handleTaskEstimationChange }, { event: SocketEvents.TASK_TIMER_STOP.toString(), handler: handleTaskTimerStop }, { event: SocketEvents.TASK_PROGRESS_UPDATED.toString(), handler: handleTaskProgressUpdate }, { event: SocketEvents.TASK_BILLABLE_CHANGE.toString(), handler: handleTaskBillableChange }, ]; // Register all event listeners eventHandlers.forEach(({ event, handler }) => { socket.on(event, handler); }); // Cleanup function return () => { eventHandlers.forEach(({ event, handler }) => { socket.off(event, handler); }); }; }, [socket, handleTaskEstimationChange, handleTaskTimerStop, handleTaskProgressUpdate, handleTaskBillableChange]); const handleExport = async () => { if (!projectId) { message.error('Project ID not found'); return; } try { setExporting(true); const blob = await projectFinanceApiService.exportFinanceData(projectId, activeGroup, billableFilter); const projectName = project?.name || 'Unknown_Project'; const sanitizedProjectName = projectName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '_'); const dateTime = new Date().toISOString().replace(/[:.]/g, '-').split('T'); const date = dateTime[0]; const time = dateTime[1].split('.')[0]; const filename = `${sanitizedProjectName}_Finance_Data_${date}_${time}.xlsx`; const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); message.success('Finance data exported successfully'); } catch (error) { console.error('Export failed:', error); message.error('Failed to export finance data'); } finally { setExporting(false); } }; const handleCurrencyChange = async (currency: string) => { if (!projectId || !hasEditPermission) { message.error('You do not have permission to change the project currency'); return; } try { setUpdatingCurrency(true); const upperCaseCurrency = currency.toUpperCase(); await projectFinanceApiService.updateProjectCurrency(projectId, upperCaseCurrency); // Update both global currency state and project-specific currency dispatch(changeCurrency(currency)); dispatch(updateProjectCurrency(upperCaseCurrency)); dispatch(updateProjectFinanceCurrency(upperCaseCurrency)); message.success('Project currency updated successfully'); } catch (error) { console.error('Currency update failed:', error); message.error('Failed to update project currency'); } finally { setUpdatingCurrency(false); } }; const groupDropdownMenuItems = [ { key: 'status', value: 'status', label: t('statusText') }, { key: 'priority', value: 'priority', label: t('priorityText') }, { key: 'phases', value: 'phases', label: phaseList.length > 0 ? project?.phase_label || t('phaseText') : t('phaseText'), }, ]; const billableFilterOptions = [ { key: 'billable', value: 'billable', label: t('billableOnlyText') }, { key: 'non-billable', value: 'non-billable', label: t('nonBillableOnlyText') }, { key: 'all', value: 'all', label: t('allTasksText') }, ]; return ( {/* Finance Header */} {activeTab === 'finance' && ( {t('groupByText')}: dispatch(setBillableFilter(value as 'all' | 'billable' | 'non-billable'))} suffixIcon={} style={{ minWidth: 140 }} /> )} {activeTab === 'finance' ? ( ) : ( {t('currencyText')}