diff --git a/assets/js/project_details.js b/assets/js/project_details.js
new file mode 100644
index 0000000..7925843
--- /dev/null
+++ b/assets/js/project_details.js
@@ -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 = ``;
+ }
+ });
+
+ 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();
+ }
+ });
+});
\ No newline at end of file
diff --git a/db/migrate.php b/db/migrate.php
new file mode 100644
index 0000000..b3df702
--- /dev/null
+++ b/db/migrate.php
@@ -0,0 +1,81 @@
+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 'Database Migrations';
+ echo '';
+ echo '';
+ echo '';
+ echo '
Database Migrations
';
+
+ $migrationsApplied = false;
+
+ foreach ($migrationFiles as $file) {
+ $migrationName = basename($file);
+ if (!in_array($migrationName, $runMigrations)) {
+ echo '
Applying migration: ' . htmlspecialchars($migrationName) . '...
';
+ $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 '
Ignoring existing column in ' . htmlspecialchars($migrationName) . '.
';
+ } else {
+ // It's another error, so we should stop.
+ echo '
Error applying migration ' . htmlspecialchars($migrationName) . ': ' . htmlspecialchars($e->getMessage()) . '
';
+ $fileHasError = true;
+ break; // break from statements loop
+ }
+ }
+ }
+
+ if (!$fileHasError) {
+ // Record migration
+ $stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
+ $stmt->execute([$migrationName]);
+
+ echo '
Successfully applied ' . htmlspecialchars($migrationName) . '
';
+ $migrationsApplied = true;
+ } else {
+ // Stop on error
+ break; // break from files loop
+ }
+ }
+ }
+
+ if (!$migrationsApplied) {
+ echo '
Database is already up to date.
';
+ }
+
+ echo '
Back to Home';
+ echo '
';
+
+} catch (PDOException $e) {
+ http_response_code(500);
+ die("Database connection failed: " . $e->getMessage());
+}
\ No newline at end of file
diff --git a/db/migrations/005_create_project_finance_monthly_table.sql b/db/migrations/005_create_project_finance_monthly_table.sql
index e0d9ee7..e8d9d00 100644
--- a/db/migrations/005_create_project_finance_monthly_table.sql
+++ b/db/migrations/005_create_project_finance_monthly_table.sql
@@ -1,11 +1,19 @@
+DROP TABLE IF EXISTS `projectFinanceMonthly`;
+
CREATE TABLE IF NOT EXISTS `projectFinanceMonthly` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`projectId` INT NOT NULL,
- `metricName` VARCHAR(255) 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,
`updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
- UNIQUE KEY `project_metric_month` (`projectId`, `metricName`, `month`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+ UNIQUE KEY `unique_project_month` (`projectId`, `month`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
\ No newline at end of file
diff --git a/db/migrations/007_add_revenue_fields_to_roster.sql b/db/migrations/007_add_revenue_fields_to_roster.sql
index 8590309..0143998 100644
--- a/db/migrations/007_add_revenue_fields_to_roster.sql
+++ b/db/migrations/007_add_revenue_fields_to_roster.sql
@@ -1,3 +1,2 @@
-ALTER TABLE `roster`
-ADD COLUMN `grossRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
-ADD COLUMN `discountedRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
+ALTER TABLE `roster` 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;
diff --git a/db/migrations/009_create_project_finance_monthly_override_table.sql b/db/migrations/009_create_project_finance_monthly_override_table.sql
new file mode 100644
index 0000000..fdf98b6
--- /dev/null
+++ b/db/migrations/009_create_project_finance_monthly_override_table.sql
@@ -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;
\ No newline at end of file
diff --git a/db/migrations/010_alter_override_table.sql b/db/migrations/010_alter_override_table.sql
new file mode 100644
index 0000000..5159dc3
--- /dev/null
+++ b/db/migrations/010_alter_override_table.sql
@@ -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;
diff --git a/describe_table.php b/describe_table.php
new file mode 100644
index 0000000..2fd72e0
--- /dev/null
+++ b/describe_table.php
@@ -0,0 +1,12 @@
+query("DESCRIBE project_finance_monthly_override");
+ $columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ print_r($columns);
+} catch (Exception $e) {
+ echo "Error: " . $e->getMessage();
+}
+?>
\ No newline at end of file
diff --git a/logs/php_errors.log b/logs/php_errors.log
new file mode 100644
index 0000000..6fc7ef1
--- /dev/null
+++ b/logs/php_errors.log
@@ -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
diff --git a/logs/raw_input.log b/logs/raw_input.log
new file mode 100644
index 0000000..00786fd
--- /dev/null
+++ b/logs/raw_input.log
@@ -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] =>
+)
diff --git a/project_details.php b/project_details.php
index 08fc482..eb9463f 100644
--- a/project_details.php
+++ b/project_details.php
@@ -10,7 +10,7 @@ function execute_sql_from_file($pdo, $filepath) {
return true;
} catch (PDOException $e) {
// 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());
}
return false;
@@ -35,7 +35,7 @@ $project = null;
$project_id = $_GET['id'] ?? null;
$financial_data = [];
$months = [];
-$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin"];
+$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin", "Payment"];
if ($project_id) {
$stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id");
@@ -103,6 +103,15 @@ if ($project_id) {
$expenses_stmt->execute([':pid' => $project_id]);
$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
$cumulative_billing = 0;
$cumulative_cost = 0;
@@ -110,36 +119,54 @@ if ($project_id) {
$previous_month_wip = 0;
foreach ($months as $month) {
- // Normalize month keys from fetched data
- $cost = $monthly_costs[$month] ?? 0;
- $base_monthly_wip = $monthly_wip[$month] ?? 0;
- $billing = $monthly_billing[$month] ?? 0;
- $expenses = $monthly_expenses[$month] ?? 0;
+ 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'];
- // Cumulative calculations
- $cumulative_billing += $billing;
- $cumulative_cost += $cost;
- $cumulative_expenses += $expenses;
+ // 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;
+ $base_monthly_wip = $monthly_wip[$month] ?? 0;
+ $billing = $monthly_billing[$month] ?? 0;
+ $expenses = $monthly_expenses[$month] ?? 0;
- // WIP Calculation (new formula)
- // current month WIP = previous month WIP + Month Expenses + base_monthly_wip - month Billing
- $current_wip = $previous_month_wip + $expenses + $base_monthly_wip - $billing;
+ // Cumulative calculations
+ $cumulative_billing += $billing;
+ $cumulative_cost += $cost;
+ $cumulative_expenses += $expenses;
- $financial_data['WIP'][$month] = $current_wip;
- $financial_data['Billings'][$month] = $cumulative_billing;
- $financial_data['Cost'][$month] = $cumulative_cost;
- $financial_data['Opening Balance'][$month] = 0; // Placeholder
- $financial_data['Expenses'][$month] = $cumulative_expenses;
+ // WIP Calculation
+ $current_wip = $previous_month_wip + $expenses + $base_monthly_wip - $billing;
- // Calculated metrics (NSR and Margin)
- $nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
- $financial_data['NSR'][$month] = $nsr;
+ $financial_data['WIP'][$month] = $current_wip;
+ $financial_data['Billings'][$month] = $cumulative_billing;
+ $financial_data['Cost'][$month] = $cumulative_cost;
+ $financial_data['Opening Balance'][$month] = 0; // Placeholder
+ $financial_data['Expenses'][$month] = $cumulative_expenses;
+ $financial_data['Payment'][$month] = 0; // Not overridden, so no payment input
- $margin = ($nsr != 0) ? (($nsr - $financial_data['Cost'][$month]) / $nsr) : 0;
- $financial_data['Margin'][$month] = $margin;
+ // Calculated metrics (NSR and Margin)
+ $nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
+ $financial_data['NSR'][$month] = $nsr;
- // Update previous month's WIP for the next iteration
- $previous_month_wip = $current_wip;
+ $margin = ($nsr != 0) ? (($nsr - $financial_data['Cost'][$month]) / $nsr) : 0;
+ $financial_data['Margin'][$month] = $margin;
+
+ // Update previous month's WIP for the next iteration
+ $previous_month_wip = $current_wip;
+ }
}
}
}
@@ -232,21 +259,45 @@ if (!$project) {
-
+
+
| Metric |
- |
+
+
+ Confirmed';
+ } elseif ($is_eligible) {
+ echo '';
+ } else {
+ echo '';
+ }
+ ?>
+ |
-
+
|
-
+ |
+
+
|