From 38ec03060f8a1629ae0a88009b7087bc8bd4535c Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 26 Nov 2025 16:18:15 +0000 Subject: [PATCH] Revert to version 73cf51a --- assets/js/project_details.js | 295 -------- db/migrate.php | 81 -- ..._add_unique_key_to_forecast_allocation.sql | 2 +- ...5_create_project_finance_monthly_table.sql | 16 +- .../007_add_revenue_fields_to_roster.sql | 5 +- ...project_finance_monthly_override_table.sql | 10 - db/migrations/010_alter_override_table.sql | 14 - describe_table.php | 12 - forecasting.php | 2 +- import.php | 2 +- index.php | 663 ++++++++++++++++- logs/php_errors.log | 20 - logs/raw_input.log | 24 - project_details.php | 149 +--- projects.php | 2 +- roster.php | 696 ------------------ save_override.php | 93 --- test_save.php | 27 - test_sql.php | 12 - 19 files changed, 689 insertions(+), 1436 deletions(-) delete mode 100644 assets/js/project_details.js delete mode 100644 db/migrate.php delete mode 100644 db/migrations/009_create_project_finance_monthly_override_table.sql delete mode 100644 db/migrations/010_alter_override_table.sql delete mode 100644 describe_table.php delete mode 100644 logs/php_errors.log delete mode 100644 logs/raw_input.log delete mode 100644 roster.php delete mode 100644 save_override.php delete mode 100644 test_save.php delete mode 100644 test_sql.php diff --git a/assets/js/project_details.js b/assets/js/project_details.js deleted file mode 100644 index 7925843..0000000 --- a/assets/js/project_details.js +++ /dev/null @@ -1,295 +0,0 @@ -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 deleted file mode 100644 index b3df702..0000000 --- a/db/migrate.php +++ /dev/null @@ -1,81 +0,0 @@ -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/004_add_unique_key_to_forecast_allocation.sql b/db/migrations/004_add_unique_key_to_forecast_allocation.sql index 7d51880..fc8dc73 100644 --- a/db/migrations/004_add_unique_key_to_forecast_allocation.sql +++ b/db/migrations/004_add_unique_key_to_forecast_allocation.sql @@ -1 +1 @@ -ALTER TABLE `forecastAllocation` DROP INDEX `unique_allocation`, ADD UNIQUE KEY `unique_allocation` (`forecastingId`, `rosterId`, `month`); \ No newline at end of file +ALTER TABLE `forecastAllocation` ADD UNIQUE KEY `unique_allocation` (`forecastingId`, `rosterId`, `month`); diff --git a/db/migrations/005_create_project_finance_monthly_table.sql b/db/migrations/005_create_project_finance_monthly_table.sql index e8d9d00..e0d9ee7 100644 --- a/db/migrations/005_create_project_finance_monthly_table.sql +++ b/db/migrations/005_create_project_finance_monthly_table.sql @@ -1,19 +1,11 @@ -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, - `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, + `amount` DECIMAL(15, 2) NOT NULL, `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 `unique_project_month` (`projectId`, `month`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file + UNIQUE KEY `project_metric_month` (`projectId`, `metricName`, `month`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/db/migrations/007_add_revenue_fields_to_roster.sql b/db/migrations/007_add_revenue_fields_to_roster.sql index 0143998..8590309 100644 --- a/db/migrations/007_add_revenue_fields_to_roster.sql +++ b/db/migrations/007_add_revenue_fields_to_roster.sql @@ -1,2 +1,3 @@ -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; +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; diff --git a/db/migrations/009_create_project_finance_monthly_override_table.sql b/db/migrations/009_create_project_finance_monthly_override_table.sql deleted file mode 100644 index fdf98b6..0000000 --- a/db/migrations/009_create_project_finance_monthly_override_table.sql +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 5159dc3..0000000 --- a/db/migrations/010_alter_override_table.sql +++ /dev/null @@ -1,14 +0,0 @@ --- 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 deleted file mode 100644 index 2fd72e0..0000000 --- a/describe_table.php +++ /dev/null @@ -1,12 +0,0 @@ -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/forecasting.php b/forecasting.php index 99cf62c..0b3c036 100644 --- a/forecasting.php +++ b/forecasting.php @@ -143,7 +143,7 @@ $months_headers = $project ? get_months($project['startDate'], $project['endDate
diff --git a/import.php b/import.php index 689d232..42e4821 100644 --- a/import.php +++ b/import.php @@ -4,7 +4,7 @@ session_start(); require_once __DIR__ . '/db/config.php'; function redirect_with_message($status, $message) { - header("Location: roster.php?import_status=$status&import_message=" . urlencode($message)); + header("Location: index.php?import_status=$status&import_message=" . urlencode($message)); exit(); } diff --git a/index.php b/index.php index df5aabe..3083d67 100644 --- a/index.php +++ b/index.php @@ -1,32 +1,277 @@ prepare($insert_sql); + + $stmt->execute([ + ':sapCode' => $_POST['sapCode'], + ':fullNameEn' => $_POST['fullNameEn'], + ':legalEntity' => $_POST['legalEntity'] ?? null, + ':functionBusinessUnit' => $_POST['functionBusinessUnit'] ?? null, + ':costCenterCode' => $_POST['costCenterCode'] ?? null, + ':level' => $_POST['level'] ?? null, + ':newAmendedSalary' => $newAmendedSalary, + ':employerContributions' => $employerContributions, + ':cars' => $cars, + ':ticketRestaurant' => $ticketRestaurant, + ':metlife' => $metlife, + ':topusPerMonth' => $topusPerMonth, + ':totalSalaryCostWithLabor' => $totalSalaryCostWithLabor, + ':totalMonthlyCost' => $totalMonthlyCost, + ':totalAnnualCost' => $totalAnnualCost, + ':grossRevenue' => $grossRevenue, + ':discountedRevenue' => $discountedRevenue + ]); + + // To prevent form resubmission on refresh, redirect + header("Location: " . $_SERVER['PHP_SELF']); + exit(); + + } catch (PDOException $e) { + // Check for duplicate entry + if ($e->errorInfo[1] == 1062) { + $form_error = "Error: A resource with this SAP Code already exists."; + } else { + $form_error = "Database error: " . $e->getMessage(); + } + } + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_roster') { + try { + require_once __DIR__ . '/db/config.php'; + $pdo_delete = db(); + $delete_sql = "DELETE FROM roster WHERE id = :id"; + $stmt = $pdo_delete->prepare($delete_sql); + $stmt->execute([':id' => $_POST['id']]); + header("Location: " . $_SERVER['PHP_SELF']); + exit(); + } catch (PDOException $e) { + $form_error = "Database error: " . $e->getMessage(); + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_roster') { + if (empty($_POST['id']) || empty($_POST['sapCode']) || empty($_POST['fullNameEn'])) { + $form_error = "ID, SAP Code, and Full Name are required for an update."; + } else { + try { + require_once __DIR__ . '/db/config.php'; + $pdo_update = db(); + + // Prepare data from form + $newAmendedSalary = (float)($_POST['newAmendedSalary'] ?? 0); + $employerContributions = (float)($_POST['employerContributions'] ?? 0); + $cars = (float)($_POST['cars'] ?? 0); + $ticketRestaurant = (float)($_POST['ticketRestaurant'] ?? 0); + $metlife = (float)($_POST['metlife'] ?? 0); + $topusPerMonth = (float)($_POST['topusPerMonth'] ?? 0); + $grossRevenue = (float)($_POST['grossRevenue'] ?? 0); + $discountedRevenue = (float)($_POST['discountedRevenue'] ?? 0); + + // Auto-calculations + $totalSalaryCostWithLabor = $newAmendedSalary + $employerContributions; + $totalMonthlyCost = $totalSalaryCostWithLabor + $cars + $ticketRestaurant + $metlife + $topusPerMonth; + $totalAnnualCost = $totalMonthlyCost * 14; + + $update_sql = "UPDATE roster SET + sapCode = :sapCode, + fullNameEn = :fullNameEn, + legalEntity = :legalEntity, + functionBusinessUnit = :functionBusinessUnit, + costCenterCode = :costCenterCode, + `level` = :level, + newAmendedSalary = :newAmendedSalary, + employerContributions = :employerContributions, + cars = :cars, + ticketRestaurant = :ticketRestaurant, + metlife = :metlife, + topusPerMonth = :topusPerMonth, + totalSalaryCostWithLabor = :totalSalaryCostWithLabor, + totalMonthlyCost = :totalMonthlyCost, + totalAnnualCost = :totalAnnualCost, + grossRevenue = :grossRevenue, + discountedRevenue = :discountedRevenue + WHERE id = :id"; + + $stmt = $pdo_update->prepare($update_sql); + $stmt->execute([ + ':id' => $_POST['id'], + ':sapCode' => $_POST['sapCode'], + ':fullNameEn' => $_POST['fullNameEn'], + ':legalEntity' => $_POST['legalEntity'] ?? null, + ':functionBusinessUnit' => $_POST['functionBusinessUnit'] ?? null, + ':costCenterCode' => $_POST['costCenterCode'] ?? null, + ':level' => $_POST['level'] ?? null, + ':newAmendedSalary' => $newAmendedSalary, + ':employerContributions' => $employerContributions, + ':cars' => $cars, + ':ticketRestaurant' => $ticketRestaurant, + ':metlife' => $metlife, + ':topusPerMonth' => $topusPerMonth, + ':totalSalaryCostWithLabor' => $totalSalaryCostWithLabor, + ':totalMonthlyCost' => $totalMonthlyCost, + ':totalAnnualCost' => $totalAnnualCost, + ':grossRevenue' => $grossRevenue, + ':discountedRevenue' => $discountedRevenue + ]); + + header("Location: " . $_SERVER['PHP_SELF']); + exit(); + + } catch (PDOException $e) { + if ($e->errorInfo[1] == 1062) { + $form_error = "Error: A resource with this SAP Code already exists."; + } else { + $form_error = "Database error: " . $e->getMessage(); + } + } + } +} + +// --- DATABASE INITIALIZATION --- require_once __DIR__ . '/db/config.php'; +function execute_sql_from_file($pdo, $filepath) { + try { + $sql = file_get_contents($filepath); + $pdo->exec($sql); + return true; + } catch (PDOException $e) { + if (strpos($e->getMessage(), 'already exists') === false) { + error_log("SQL Execution Error: " . $e->getMessage()); + } + return false; + } +} + +function seed_roster_data($pdo) { + try { + $stmt = $pdo->query("SELECT COUNT(*) FROM roster"); + if ($stmt->fetchColumn() > 0) { + return; // Data already exists + } + + $seed_data = [ + [ + 'sapCode' => '1001', 'fullNameEn' => 'John Doe', 'legalEntity' => 'Entity A', 'functionBusinessUnit' => 'Finance', + 'costCenterCode' => 'CC100', 'level' => 'Senior', 'newAmendedSalary' => 6000, 'employerContributions' => 1500, + 'cars' => 500, 'ticketRestaurant' => 150, 'metlife' => 50, 'topusPerMonth' => 100 + ], + [ + 'sapCode' => '1002', 'fullNameEn' => 'Jane Smith', 'legalEntity' => 'Entity B', 'functionBusinessUnit' => 'IT', + 'costCenterCode' => 'CC200', 'level' => 'Manager', 'newAmendedSalary' => 8000, 'employerContributions' => 2000, + 'cars' => 600, 'ticketRestaurant' => 150, 'metlife' => 60, 'topusPerMonth' => 120 + ], + ]; + + $insert_sql = "INSERT INTO roster (sapCode, fullNameEn, legalEntity, functionBusinessUnit, costCenterCode, `level`, newAmendedSalary, employerContributions, cars, ticketRestaurant, metlife, topusPerMonth, totalSalaryCostWithLabor, totalMonthlyCost, totalAnnualCost) VALUES (:sapCode, :fullNameEn, :legalEntity, :functionBusinessUnit, :costCenterCode, :level, :newAmendedSalary, :employerContributions, :cars, :ticketRestaurant, :metlife, :topusPerMonth, :totalSalaryCostWithLabor, :totalMonthlyCost, :totalAnnualCost)"; + $stmt = $pdo->prepare($insert_sql); + + foreach ($seed_data as $row) { + $totalSalaryCostWithLabor = $row['newAmendedSalary'] + $row['employerContributions']; + $totalMonthlyCost = $totalSalaryCostWithLabor + $row['cars'] + $row['ticketRestaurant'] + $row['metlife'] + $row['topusPerMonth']; + $totalAnnualCost = $totalMonthlyCost * 14; + + $stmt->execute([ + ':sapCode' => $row['sapCode'], + ':fullNameEn' => $row['fullNameEn'], + ':legalEntity' => $row['legalEntity'], + ':functionBusinessUnit' => $row['functionBusinessUnit'], + ':costCenterCode' => $row['costCenterCode'], + ':level' => $row['level'], + ':newAmendedSalary' => $row['newAmendedSalary'], + ':employerContributions' => $row['employerContributions'], + ':cars' => $row['cars'], + ':ticketRestaurant' => $row['ticketRestaurant'], + ':metlife' => $row['metlife'], + ':topusPerMonth' => $row['topusPerMonth'], + ':totalSalaryCostWithLabor' => $totalSalaryCostWithLabor, + ':totalMonthlyCost' => $totalMonthlyCost, + ':totalAnnualCost' => $totalAnnualCost + ]); + } + + } catch (PDOException $e) { + error_log("Seeding Error: " . $e->getMessage()); + } +} + +$roster_data = []; +$search_term = $_GET['search'] ?? ''; + try { $pdo = db(); + + // Apply all migrations $migration_files = glob(__DIR__ . '/db/migrations/*.sql'); sort($migration_files); foreach ($migration_files as $file) { - $sql = file_get_contents($file); - $pdo->exec($sql); + execute_sql_from_file($pdo, $file); } + + seed_roster_data($pdo); + + $sql = "SELECT * FROM roster"; + $params = []; + + if (!empty($search_term)) { + $sql .= " WHERE fullNameEn LIKE :search OR sapCode LIKE :search"; + $params[':search'] = '%' . $search_term . '%'; + } + + $sql .= " ORDER BY fullNameEn"; + $stmt = $pdo->prepare($sql); + $stmt->execute($params); + $roster_data = $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { - // If the table already exists, ignore the error - if (strpos($e->getMessage(), 'already exists') === false) { - die("Database migration failed: " . $e->getMessage()); - } + $db_error = "Database connection failed: " . $e->getMessage(); } + +// --- RENDER PAGE --- ?> - Project Financial Management - - - + Project Financials + + + + + @@ -36,30 +281,416 @@ try {
- Project Financial Management + Project Financials
+
+ + {$message} + +
"; + } + ?> + +
+ +
+ + +
-
-

This is the landing page. You can navigate to the Roster or Projects page using the sidebar.

+
+
+
+ + +
+
+ + Clear +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SAP CodeFull NameLegal EntityBusiness UnitCost CenterLevelSalaryContributionsCarsTicket RestaurantMetlifeTopus/MonthTotal Salary CostTotal Monthly CostTotal Annual CostGross RevenueDiscounted RevenueDaily CostActions
No roster data found.
+
+ + +
+ + + +
+
+
+ + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/logs/php_errors.log b/logs/php_errors.log deleted file mode 100644 index 6fc7ef1..0000000 --- a/logs/php_errors.log +++ /dev/null @@ -1,20 +0,0 @@ -[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 deleted file mode 100644 index 00786fd..0000000 --- a/logs/raw_input.log +++ /dev/null @@ -1,24 +0,0 @@ -{"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 eb9463f..e599aa6 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 && strpos($e->getMessage(), 'Duplicate key name') === false) { + if (strpos($e->getMessage(), 'already exists') === false && strpos($e->getMessage(), 'Duplicate column 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", "Payment"]; +$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin"]; if ($project_id) { $stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id"); @@ -103,15 +103,6 @@ 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; @@ -119,54 +110,36 @@ if ($project_id) { $previous_month_wip = 0; foreach ($months as $month) { - 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']; + // 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; - // 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; + // Cumulative calculations + $cumulative_billing += $billing; + $cumulative_cost += $cost; + $cumulative_expenses += $expenses; - // Cumulative calculations - $cumulative_billing += $billing; - $cumulative_cost += $cost; - $cumulative_expenses += $expenses; + // 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; - // WIP Calculation - $current_wip = $previous_month_wip + $expenses + $base_monthly_wip - $billing; + $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['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 + // 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; - // 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; + $margin = ($nsr != 0) ? (($nsr - $financial_data['Cost'][$month]) / $nsr) : 0; + $financial_data['Margin'][$month] = $margin; - $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; - } + // Update previous month's WIP for the next iteration + $previous_month_wip = $current_wip; } } } @@ -198,7 +171,7 @@ if (!$project) {
@@ -259,45 +232,21 @@ if (!$project) {
- - +
- + - + -
Metric -
- Confirmed'; - } elseif ($is_eligible) { - echo ''; - } else { - echo ''; - } - ?> -
+ - - \ No newline at end of file diff --git a/projects.php b/projects.php index d3cc2de..a46d706 100644 --- a/projects.php +++ b/projects.php @@ -150,7 +150,7 @@ try {