36293-vm/assets/js/project_details.js
2025-11-26 16:15:14 +00:00

295 lines
13 KiB
JavaScript

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 = `<input type="text" class="form-control form-control-sm" value="${localeValue}">`;
}
});
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 = `
<div class="btn-group btn-group-sm mt-1" role="group">
<button type="button" class="btn btn-success confirm-override">Confirm</button>
<button type="button" class="btn btn-danger cancel-override">Cancel</button>
</div>
`;
}
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();
}
});
});