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 = `
`; } async function confirmOverride() { try { 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`); if (!input) { throw new Error(`Could not find input for metric: ${metric} in month ${state.overrideMonth}`); } overrideValues[metric] = parseLocaleNumber(input.value) || 0; }); const finalData = recalculateFinancials(state.overrideMonth, overrideValues); const monthsToSave = months.slice(months.indexOf(state.overrideMonth)); const dataToSave = {}; monthsToSave.forEach(month => { dataToSave[month] = {}; metrics.forEach(metric => { const rawValue = finalData[metric][month]; let cleanValue = typeof rawValue === 'string' ? parseLocaleNumber(rawValue) : rawValue; if (!isFinite(cleanValue)) { console.warn(`Invalid number for ${metric} in ${month}. Resetting to 0. Original:`, rawValue); cleanValue = 0; } dataToSave[month][metric] = cleanValue; }); }); const payload = { project_id: projectId, overrides: Object.entries(dataToSave).map(([month, values]) => ({ month: month, opening_balance: values['Opening Balance'], payment: values.Payment, wip: values.WIP, expenses: values.Expenses, cost: values.Cost, nsr: values.NSR, margin: values.Margin, is_confirmed: month === state.overrideMonth ? 1 : 0 })) }; const response = await fetch('save_override.php?t=' + new Date().getTime(), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload) }); if (!response.ok) { let errorMsg = `HTTP error! status: ${response.status}`; try { const errorData = await response.json(); errorMsg = errorData.message || errorMsg; } catch (e) { // Not a JSON response } throw new Error(errorMsg); } const result = await response.json(); if (result.success) { alert(result.message || 'Override confirmed successfully!'); window.location.reload(); } else { throw new Error(result.message || 'Failed to save override.'); } } catch (error) { console.error('Error confirming override:', error); alert(`Error: ${error.message}`); } } function exitOverrideMode() { metrics.forEach(metric => { const cell = table.querySelector(`td[data-month="${state.overrideMonth}"][data-metric="${metric}"]`); if (cell) { cell.innerHTML = state.originalTableState[metric]; } }); const buttonCell = table.querySelector(`th .btn-group`).parentElement; buttonCell.innerHTML = state.originalTableState['button']; updateTable(initialFinancialData); state.isOverrideActive = false; state.overrideMonth = null; state.originalTableState = {}; } table.addEventListener('click', (e) => { if (e.target.classList.contains('override-btn')) { enterOverrideMode(e.target.dataset.month); } else if (e.target.classList.contains('confirm-override')) { confirmOverride(); } else if (e.target.classList.contains('cancel-override')) { exitOverrideMode(); } }); });