Revert to version 36ef6e0

This commit is contained in:
Flatlogic Bot 2025-11-26 16:15:14 +00:00
parent 3dfe3836b0
commit 89109f468e
13 changed files with 719 additions and 37 deletions

View File

@ -0,0 +1,295 @@
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();
}
});
});

81
db/migrate.php Normal file
View File

@ -0,0 +1,81 @@
<?php
// Simple migration script
require_once __DIR__ . '/config.php';
try {
$pdo = db();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create migrations table if it doesn't exist
$pdo->exec("CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
migration_name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// Get all migration files
$migrationFiles = glob(__DIR__ . '/migrations/*.sql');
// Get already run migrations
$stmt = $pdo->query("SELECT migration_name FROM migrations");
$runMigrations = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo '<html><head><title>Database Migrations</title>';
echo '<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">';
echo '<style>body { padding: 2rem; } .log { font-family: monospace; white-space: pre; } </style>';
echo '</head><body><div class="container">';
echo '<h1>Database Migrations</h1>';
$migrationsApplied = false;
foreach ($migrationFiles as $file) {
$migrationName = basename($file);
if (!in_array($migrationName, $runMigrations)) {
echo '<div class="alert alert-info log">Applying migration: ' . htmlspecialchars($migrationName) . '...</div>';
$sql = file_get_contents($file);
$statements = array_filter(array_map('trim', explode(';', $sql)));
$fileHasError = false;
foreach ($statements as $statement) {
if (empty($statement)) continue;
try {
$pdo->exec($statement);
} catch (PDOException $e) {
// 1060 is the specific error code for "Duplicate column name"
if (strpos($e->getMessage(), '1060') !== false) {
// It's a duplicate column error, we can ignore it.
echo '<div class="alert alert-warning log">Ignoring existing column in ' . htmlspecialchars($migrationName) . '.</div>';
} else {
// It's another error, so we should stop.
echo '<div class="alert alert-danger log">Error applying migration ' . htmlspecialchars($migrationName) . ': ' . htmlspecialchars($e->getMessage()) . '</div>';
$fileHasError = true;
break; // break from statements loop
}
}
}
if (!$fileHasError) {
// Record migration
$stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
$stmt->execute([$migrationName]);
echo '<div class="alert alert-success log">Successfully applied ' . htmlspecialchars($migrationName) . '</div>';
$migrationsApplied = true;
} else {
// Stop on error
break; // break from files loop
}
}
}
if (!$migrationsApplied) {
echo '<div class="alert alert-success log">Database is already up to date.</div>';
}
echo '<a href="/" class="btn btn-primary mt-3">Back to Home</a>';
echo '</div></body></html>';
} catch (PDOException $e) {
http_response_code(500);
die("Database connection failed: " . $e->getMessage());
}

View File

@ -1,11 +1,19 @@
DROP TABLE IF EXISTS `projectFinanceMonthly`;
CREATE TABLE IF NOT EXISTS `projectFinanceMonthly` ( CREATE TABLE IF NOT EXISTS `projectFinanceMonthly` (
`id` INT AUTO_INCREMENT PRIMARY KEY, `id` INT AUTO_INCREMENT PRIMARY KEY,
`projectId` INT NOT NULL, `projectId` INT NOT NULL,
`metricName` VARCHAR(255) NOT NULL,
`month` DATE NOT NULL, `month` DATE NOT NULL,
`amount` DECIMAL(15, 2) NOT NULL, `opening_balance` DECIMAL(15, 2) DEFAULT 0,
`payment` DECIMAL(15, 2) DEFAULT 0,
`wip` DECIMAL(15, 2) DEFAULT 0,
`expenses` DECIMAL(15, 2) DEFAULT 0,
`cost` DECIMAL(15, 2) DEFAULT 0,
`nsr` DECIMAL(15, 2) DEFAULT 0,
`margin` DECIMAL(15, 5) DEFAULT 0,
`is_confirmed` TINYINT(1) NOT NULL DEFAULT 0,
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE, FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
UNIQUE KEY `project_metric_month` (`projectId`, `metricName`, `month`) UNIQUE KEY `unique_project_month` (`projectId`, `month`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -1,3 +1,2 @@
ALTER TABLE `roster` ALTER TABLE `roster` ADD COLUMN `grossRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
ADD COLUMN `grossRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, ALTER TABLE `roster` ADD COLUMN `discountedRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
ADD COLUMN `discountedRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;

View File

@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS `project_finance_monthly_override` (
`project_id` INT NOT NULL,
`nsr_override` DECIMAL(15, 2) DEFAULT NULL,
`cost_override` DECIMAL(15, 2) DEFAULT NULL,
`hours_override` DECIMAL(10, 2) DEFAULT NULL,
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`project_id`),
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,14 @@
-- Step 1: Drop the foreign key constraint
ALTER TABLE project_finance_monthly_override DROP FOREIGN KEY project_finance_monthly_override_ibfk_1;
-- Step 2: Drop the old primary key
ALTER TABLE project_finance_monthly_override DROP PRIMARY KEY;
-- Step 3: Add the new month column
ALTER TABLE project_finance_monthly_override ADD COLUMN month VARCHAR(7) NOT NULL;
-- Step 4: Add the new composite primary key
ALTER TABLE project_finance_monthly_override ADD PRIMARY KEY (project_id, month);
-- Step 5: Re-add the foreign key constraint with a more descriptive name
ALTER TABLE project_finance_monthly_override ADD CONSTRAINT fk_override_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

12
describe_table.php Normal file
View File

@ -0,0 +1,12 @@
<?php
require_once 'db/config.php';
try {
$pdo = db();
$stmt = $pdo->query("DESCRIBE project_finance_monthly_override");
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($columns);
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
?>

20
logs/php_errors.log Normal file
View File

@ -0,0 +1,20 @@
[26-Nov-2025 13:29:03 UTC] save_override.php: Script start
[26-Nov-2025 13:29:03 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:08 UTC] save_override.php: Script start
[26-Nov-2025 13:29:08 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:13 UTC] save_override.php: Script start
[26-Nov-2025 13:29:13 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:19 UTC] save_override.php: Script start
[26-Nov-2025 13:29:19 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:24 UTC] save_override.php: Script start
[26-Nov-2025 13:29:24 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:29 UTC] save_override.php: Script start
[26-Nov-2025 13:29:29 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:29 UTC] save_override.php: Script start
[26-Nov-2025 13:29:29 UTC] save_override.php: JSON data decoded
[26-Nov-2025 13:29:29 UTC] save_override.php: Database connection successful
[26-Nov-2025 13:29:29 UTC] save_override.php: Starting database transaction
[26-Nov-2025 13:29:29 UTC] save_override.php: SQL statement prepared
[26-Nov-2025 13:29:29 UTC] save_override.php: Processing month: 2025-11-01
[26-Nov-2025 13:29:29 UTC] save_override.php: An exception occurred: SQLSTATE[22001]: String data, right truncated: 1406 Data too long for column 'month' at row 1
[26-Nov-2025 13:29:29 UTC] save_override.php: Rolling back transaction

24
logs/raw_input.log Normal file
View File

@ -0,0 +1,24 @@
{"project_id":25,"overrides":[{"month":"2025-11-01","nsr":27855,"cost":1350},{"month":"2025-12-01","nsr":35545,"cost":2600},{"month":"2026-01-01","nsr":43355,"cost":3225}]}Executing with params: Array
(
[:project_id] => 25
[:month] => 2025-11-01
[:nsr_override] => 27855
[:cost_override] => 1350
[:hours_override] =>
)
Executing with params: Array
(
[:project_id] => 25
[:month] => 2025-12-01
[:nsr_override] => 35545
[:cost_override] => 2600
[:hours_override] =>
)
Executing with params: Array
(
[:project_id] => 25
[:month] => 2026-01-01
[:nsr_override] => 43355
[:cost_override] => 3225
[:hours_override] =>
)

View File

@ -10,7 +10,7 @@ function execute_sql_from_file($pdo, $filepath) {
return true; return true;
} catch (PDOException $e) { } catch (PDOException $e) {
// Ignore errors about tables/columns that already exist // Ignore errors about tables/columns that already exist
if (strpos($e->getMessage(), 'already exists') === false && strpos($e->getMessage(), 'Duplicate column name') === false) { if (strpos($e->getMessage(), 'already exists') === false && strpos($e->getMessage(), 'Duplicate column name') === false && strpos($e->getMessage(), 'Duplicate key name') === false) {
error_log("SQL Execution Error in $filepath: " . $e->getMessage()); error_log("SQL Execution Error in $filepath: " . $e->getMessage());
} }
return false; return false;
@ -35,7 +35,7 @@ $project = null;
$project_id = $_GET['id'] ?? null; $project_id = $_GET['id'] ?? null;
$financial_data = []; $financial_data = [];
$months = []; $months = [];
$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin"]; $metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin", "Payment"];
if ($project_id) { if ($project_id) {
$stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id"); $stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id");
@ -103,6 +103,15 @@ if ($project_id) {
$expenses_stmt->execute([':pid' => $project_id]); $expenses_stmt->execute([':pid' => $project_id]);
$monthly_expenses = $expenses_stmt->fetchAll(PDO::FETCH_KEY_PAIR); $monthly_expenses = $expenses_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
// Fetch confirmed override data
$finance_override_stmt = $pdo->prepare("SELECT * FROM projectFinanceMonthly WHERE projectId = :pid ORDER BY month ASC");
$finance_override_stmt->execute([':pid' => $project_id]);
$overrides_raw = $finance_override_stmt->fetchAll(PDO::FETCH_ASSOC);
$finance_overrides = [];
foreach ($overrides_raw as $row) {
$finance_overrides[$row['month']] = $row;
}
// 2. Calculate cumulative values month by month // 2. Calculate cumulative values month by month
$cumulative_billing = 0; $cumulative_billing = 0;
$cumulative_cost = 0; $cumulative_cost = 0;
@ -110,7 +119,24 @@ if ($project_id) {
$previous_month_wip = 0; $previous_month_wip = 0;
foreach ($months as $month) { foreach ($months as $month) {
// Normalize month keys from fetched data if (isset($finance_overrides[$month])) {
// This month has saved data. Use its values.
$financial_data['Opening Balance'][$month] = $finance_overrides[$month]['opening_balance'];
$financial_data['Billings'][$month] = $finance_overrides[$month]['payment'];
$financial_data['Payment'][$month] = $finance_overrides[$month]['payment'];
$financial_data['WIP'][$month] = $finance_overrides[$month]['wip'];
$financial_data['Expenses'][$month] = $finance_overrides[$month]['expenses'];
$financial_data['Cost'][$month] = $finance_overrides[$month]['cost'];
$financial_data['NSR'][$month] = $finance_overrides[$month]['nsr'];
$financial_data['Margin'][$month] = $finance_overrides[$month]['margin'];
// Update cumulative trackers for the next potential calculation
$cumulative_billing = $financial_data['Billings'][$month];
$cumulative_cost = $financial_data['Cost'][$month];
$cumulative_expenses= $financial_data['Expenses'][$month];
$previous_month_wip = $financial_data['WIP'][$month];
} else {
// This month has no saved data. Calculate as usual.
$cost = $monthly_costs[$month] ?? 0; $cost = $monthly_costs[$month] ?? 0;
$base_monthly_wip = $monthly_wip[$month] ?? 0; $base_monthly_wip = $monthly_wip[$month] ?? 0;
$billing = $monthly_billing[$month] ?? 0; $billing = $monthly_billing[$month] ?? 0;
@ -121,8 +147,7 @@ if ($project_id) {
$cumulative_cost += $cost; $cumulative_cost += $cost;
$cumulative_expenses += $expenses; $cumulative_expenses += $expenses;
// WIP Calculation (new formula) // WIP Calculation
// current month WIP = previous month WIP + Month Expenses + base_monthly_wip - month Billing
$current_wip = $previous_month_wip + $expenses + $base_monthly_wip - $billing; $current_wip = $previous_month_wip + $expenses + $base_monthly_wip - $billing;
$financial_data['WIP'][$month] = $current_wip; $financial_data['WIP'][$month] = $current_wip;
@ -130,6 +155,7 @@ if ($project_id) {
$financial_data['Cost'][$month] = $cumulative_cost; $financial_data['Cost'][$month] = $cumulative_cost;
$financial_data['Opening Balance'][$month] = 0; // Placeholder $financial_data['Opening Balance'][$month] = 0; // Placeholder
$financial_data['Expenses'][$month] = $cumulative_expenses; $financial_data['Expenses'][$month] = $cumulative_expenses;
$financial_data['Payment'][$month] = 0; // Not overridden, so no payment input
// Calculated metrics (NSR and Margin) // Calculated metrics (NSR and Margin)
$nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month]; $nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
@ -143,6 +169,7 @@ if ($project_id) {
} }
} }
} }
}
// --- PAGE RENDER --- // --- PAGE RENDER ---
if (!$project) { if (!$project) {
@ -232,21 +259,45 @@ if (!$project) {
</div> </div>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-bordered table-hover"> <?php
// Determine override eligibility
$override_eligible_month = null;
foreach ($months as $month) {
if (!isset($finance_overrides[$month]) || !$finance_overrides[$month]['is_confirmed']) {
$override_eligible_month = $month;
break;
}
}
?>
<table class="table table-bordered table-hover" id="financials-table">
<thead> <thead>
<tr class="text-center"> <tr class="text-center">
<th class="bg-body-tertiary" style="min-width: 150px;">Metric</th> <th class="bg-body-tertiary" style="min-width: 150px;">Metric</th>
<?php foreach ($months as $month): ?> <?php foreach ($months as $month): ?>
<th><?php echo date('M Y', strtotime($month)); ?></th> <th>
<?php echo date('M Y', strtotime($month)); ?><br>
<?php
$is_confirmed = isset($finance_overrides[$month]) && $finance_overrides[$month]['is_confirmed'];
$is_eligible = ($month === $override_eligible_month);
if ($is_confirmed) {
echo '<span class="badge bg-success mt-1">Confirmed</span>';
} elseif ($is_eligible) {
echo '<button class="btn btn-sm btn-warning override-btn mt-1" data-month="' . $month . '">Override</button>';
} else {
echo '<button class="btn btn-sm btn-secondary mt-1" disabled>Override</button>';
}
?>
</th>
<?php endforeach; ?> <?php endforeach; ?>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($metrics as $metric): ?> <?php foreach ($metrics as $metric): ?>
<tr> <tr data-metric="<?php echo htmlspecialchars($metric); ?>">
<td class="fw-bold bg-body-tertiary"><?php echo $metric; ?></td> <td class="fw-bold bg-body-tertiary"><?php echo $metric; ?></td>
<?php foreach ($months as $month): ?> <?php foreach ($months as $month): ?>
<td class="text-end"> <td class="text-end financial-metric" data-month="<?php echo $month; ?>" data-metric="<?php echo htmlspecialchars($metric); ?>">
<?php <?php
if ($metric === 'Margin') { if ($metric === 'Margin') {
echo number_format(($financial_data[$metric][$month] ?? 0) * 100, 2) . '%'; echo number_format(($financial_data[$metric][$month] ?? 0) * 100, 2) . '%';
@ -267,6 +318,42 @@ if (!$project) {
</main> </main>
</div> </div>
<script id="financial-data" type="application/json">
<?php
$project_id_js = $project['id'];
$finance_overrides_js = [];
foreach($finance_overrides as $month => $data) {
$finance_overrides_js[$month] = [
'is_confirmed' => $data['is_confirmed'],
'Opening Balance' => $data['opening_balance'],
'Billings' => $data['payment'],
'Payment' => $data['payment'],
'WIP' => $data['wip'],
'Expenses' => $data['expenses'],
'Cost' => $data['cost'],
'NSR' => $data['nsr'],
'Margin' => $data['margin']
];
}
$js_data = [
'projectId' => $project_id_js,
'months' => $months,
'metrics' => $metrics,
'initialFinancialData' => $financial_data,
'baseData' => [
'monthly_costs' => $monthly_costs,
'monthly_wip' => $monthly_wip,
'monthly_billing' => $monthly_billing,
'monthly_expenses' => $monthly_expenses,
],
'overrides' => $finance_overrides_js
];
echo json_encode($js_data, JSON_PRETTY_PRINT);
?>
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/project_details.js?v=<?php echo time(); ?>"></script>
</body> </body>
</html> </html>

93
save_override.php Normal file
View File

@ -0,0 +1,93 @@
<?php
require_once 'db/config.php';
header('Content-Type: application/json');
$raw_input = file_get_contents('php://input');
$response = ['success' => false, 'message' => 'An error occurred.'];
$pdo = null;
try {
$pdo = db();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$data = json_decode($raw_input, true);
if (empty($data['project_id']) || empty($data['overrides'])) {
throw new Exception("Project ID or overrides data not provided.");
}
$project_id = $data['project_id'];
$overrides = $data['overrides'];
$rows_affected_total = 0;
$pdo->beginTransaction();
$sql = "
INSERT INTO projectFinanceMonthly (projectId, month, opening_balance, payment, wip, expenses, cost, nsr, margin, is_confirmed)
VALUES (:project_id, :month, :opening_balance, :payment, :wip, :expenses, :cost, :nsr, :margin, :is_confirmed)
ON DUPLICATE KEY UPDATE
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 = VALUES(is_confirmed)
";
$stmt = $pdo->prepare($sql);
foreach ($overrides as $override) {
$date = new DateTime($override['month']);
$month = $date->format('Y-m-d');
$params = [
':project_id' => $project_id,
':month' => $month,
':opening_balance' => $override['opening_balance'] ?? null,
':payment' => $override['payment'] ?? null,
':wip' => $override['wip'] ?? null,
':expenses' => $override['expenses'] ?? null,
':cost' => $override['cost'] ?? null,
':nsr' => $override['nsr'] ?? null,
':margin' => $override['margin'] ?? null,
':is_confirmed' => $override['is_confirmed'] ?? 0,
];
$stmt->execute($params);
$rows_affected_total += $stmt->rowCount();
}
if ($rows_affected_total === 0) {
// It's possible that the data is identical, so 0 affected rows is not strictly an error.
// We can consider it a "soft" success.
$pdo->rollBack(); // Rollback to not leave an open transaction
$response['success'] = true; // Report success to the frontend
$response['message'] = 'No changes were detected. Nothing was saved.';
echo json_encode($response);
exit;
}
$pdo->commit();
$response['success'] = true;
$response['message'] = 'Override data saved successfully. ' . $rows_affected_total . ' records were updated.';
} catch (PDOException $e) {
if ($pdo && $pdo->inTransaction()) {
$pdo->rollBack();
}
error_log("save_override.php: PDOException caught: " . $e->getMessage());
$response['message'] = "Database Error: " . $e->getMessage();
http_response_code(500);
} catch (Exception $e) {
if ($pdo && $pdo->inTransaction()) {
$pdo->rollBack();
}
error_log("save_override.php: Exception caught: " . $e->getMessage());
$response['message'] = "General Error: " . $e->getMessage();
http_response_code(500);
}
echo json_encode($response);
?>

27
test_save.php Normal file
View File

@ -0,0 +1,27 @@
<?php
$url = 'http://localhost/save_override.php';
$data = [
'project_id' => 25,
'overrides' => [
['month' => '2025-11-01', 'nsr' => 1000, 'cost' => 500],
['month' => '2025-12-01', 'nsr' => 2000, 'cost' => 1000],
]
];
$options = [
'http' => [
'header' => "Content-type: application/json\r\n",
'method' => 'POST',
'content' => json_encode($data),
],
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
if ($result === FALSE) {
echo "Error fetching URL";
}
var_dump($result);
?>

12
test_sql.php Normal file
View File

@ -0,0 +1,12 @@
<?php
require_once 'db/config.php';
try {
$pdo = db();
$sql = "INSERT INTO project_finance_monthly_override (project_id, month, nsr_override, cost_override) VALUES (25, '2025-11-01', 1000, 500)";
$pdo->exec($sql);
echo "Insert successful.";
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
?>