document.addEventListener('DOMContentLoaded', () => { const dataScript = document.getElementById('financial-data'); if (!dataScript) return; const appData = JSON.parse(dataScript.textContent); const { projectId, months, metrics, initialFinancialData, baseData, overrides } = appData; let state = { isOverrideActive: false, overrideMonth: null, originalTableState: {}, currentFinancialData: JSON.parse(JSON.stringify(initialFinancialData)) // Deep copy }; const table = document.getElementById('financials-table'); if (!table) return; // UTILITY FUNCTIONS const formatCurrency = (value) => `€${(value || 0).toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; const formatMargin = (value) => `${((value || 0) * 100).toFixed(2).replace('.', ',')}%`; const parseLocaleNumber = (stringNumber) => { if (typeof stringNumber !== 'string') return stringNumber; // Remove thousands separators (.), then replace decimal comma with a period. return Number(String(stringNumber).replace(/\./g, '').replace(',', '.')); }; function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // MAIN LOGIC function recalculateFinancials(overrideMonth, overrideValues) { const newData = JSON.parse(JSON.stringify(state.currentFinancialData)); const overrideMonthIndex = months.indexOf(overrideMonth); if (overrideMonthIndex === -1) { console.error("Override month not found in months array!"); return state.currentFinancialData; } // Step 1: Apply the override values for the specific override month. for (const key in overrideValues) { overrideValues[key] = parseFloat(overrideValues[key] || 0); } newData['Opening Balance'][overrideMonth] = overrideValues['Opening Balance']; newData.Billings[overrideMonth] = overrideValues.Billings; newData.WIP[overrideMonth] = overrideValues.WIP; newData.Expenses[overrideMonth] = overrideValues.Expenses; newData.Cost[overrideMonth] = overrideValues.Cost; newData.Payment[overrideMonth] = overrideValues.Payment; let nsr = newData.WIP[overrideMonth] + newData.Billings[overrideMonth] - newData['Opening Balance'][overrideMonth] - newData.Expenses[overrideMonth]; newData.NSR[overrideMonth] = nsr; let margin = (nsr !== 0) ? ((nsr - newData.Cost[overrideMonth]) / nsr) : 0; newData.Margin[overrideMonth] = margin; // Step 2: Recalculate all subsequent months for (let i = overrideMonthIndex + 1; i < months.length; i++) { const month = months[i]; const prevMonth = months[i - 1]; const prevCumulativeBilling = parseFloat(newData.Billings[prevMonth] || 0); const prevCumulativeCost = parseFloat(newData.Cost[prevMonth] || 0); const prevCumulativeExpenses = parseFloat(newData.Expenses[prevMonth] || 0); const prevWIP = parseFloat(newData.WIP[prevMonth] || 0); const monthlyCost = parseFloat(baseData.monthly_costs[month] || 0); const monthlyBilling = parseFloat(baseData.monthly_billing[month] || 0); const monthlyExpenses = parseFloat(baseData.monthly_expenses[month] || 0); const monthlyWIPChange = parseFloat(baseData.monthly_wip[month] || 0); const newCumulativeBilling = prevCumulativeBilling + monthlyBilling; const newCumulativeCost = prevCumulativeCost + monthlyCost; const newCumulativeExpenses = prevCumulativeExpenses + monthlyExpenses; const newWIP = prevWIP + monthlyExpenses + monthlyWIPChange - monthlyBilling; newData.Billings[month] = newCumulativeBilling; newData.Cost[month] = newCumulativeCost; newData.Expenses[month] = newCumulativeExpenses; newData.WIP[month] = newWIP; // THE FIX: Carry over the previous month's Net Service Revenue as the next month's Opening Balance. newData['Opening Balance'][month] = newData.NSR[prevMonth] || 0; newData.Payment[month] = 0; nsr = newWIP + newCumulativeBilling - newData['Opening Balance'][month] - newCumulativeExpenses; newData.NSR[month] = nsr; margin = (nsr !== 0) ? ((nsr - newCumulativeCost) / nsr) : 0; newData.Margin[month] = margin; } return newData; } function updateTable(newData) { state.currentFinancialData = newData; for (const metric of metrics) { for (const month of months) { const cell = table.querySelector(`td[data-month="${month}"][data-metric="${metric}"]`); if (cell && cell.firstElementChild?.tagName !== 'INPUT') { const value = newData[metric][month]; cell.textContent = metric === 'Margin' ? formatMargin(value) : formatCurrency(value); } } } } function recalculateForOverrideMonth(overrideMonth, overrideValues) { const newData = JSON.parse(JSON.stringify(state.currentFinancialData)); // Deep copy // Use the user's input values for the override month newData['Opening Balance'][overrideMonth] = overrideValues['Opening Balance']; newData.Billings[overrideMonth] = overrideValues.Billings; newData.WIP[overrideMonth] = overrideValues.WIP; newData.Expenses[overrideMonth] = overrideValues.Expenses; newData.Cost[overrideMonth] = overrideValues.Cost; newData.Payment[overrideMonth] = overrideValues.Payment; // Recalculate dependent metrics for the override month const nsr = newData.WIP[overrideMonth] + newData.Billings[overrideMonth] - newData['Opening Balance'][overrideMonth] - newData.Expenses[overrideMonth]; newData.NSR[overrideMonth] = nsr; const margin = (nsr !== 0) ? ((nsr - newData.Cost[overrideMonth]) / nsr) : 0; newData.Margin[overrideMonth] = margin; return newData; } function handleInputChange() { const overrideValues = {}; const editableMetrics = ['Opening Balance', 'Billings', 'WIP', 'Expenses', 'Cost', 'Payment']; editableMetrics.forEach(metric => { const input = table.querySelector(`td[data-month="${state.overrideMonth}"][data-metric="${metric}"] input`); overrideValues[metric] = parseLocaleNumber(input.value) || 0; }); const recalculatedData = recalculateForOverrideMonth(state.overrideMonth, overrideValues); updateTable(recalculatedData); } function enterOverrideMode(month) { if (state.isOverrideActive) return; state.isOverrideActive = true; state.overrideMonth = month; const editableMetrics = ['Opening Balance', 'Billings', 'WIP', 'Expenses', 'Cost', 'Payment']; metrics.forEach(metric => { const cell = table.querySelector(`td[data-month="${month}"][data-metric="${metric}"]`); if (!cell) return; const originalValue = state.currentFinancialData[metric][month]; state.originalTableState[metric] = cell.innerHTML; if (editableMetrics.includes(metric)) { // Ensure originalValue is a number before formatting const numericValue = (typeof originalValue === 'number') ? originalValue : 0; // Format to locale string with comma decimal separator for the input value const localeValue = numericValue.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); cell.innerHTML = ``; } }); const debouncedRecalc = debounce(handleInputChange, 200); table.querySelectorAll(`td[data-month="${month}"] input`).forEach(input => { input.addEventListener('input', debouncedRecalc); }); const buttonCell = table.querySelector(`th button[data-month="${month}"]`).parentElement; state.originalTableState['button'] = buttonCell.innerHTML; buttonCell.innerHTML = `