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 = `
-
';
- 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.
+
+
+
+
+
+ | SAP Code |
+ Full Name |
+ Legal Entity |
+ Business Unit |
+ Cost Center |
+ Level |
+ Salary |
+ Contributions |
+ Cars |
+ Ticket Restaurant |
+ Metlife |
+ Topus/Month |
+ Total Salary Cost |
+ Total Monthly Cost |
+ Total Annual Cost |
+ Gross Revenue |
+ Discounted Revenue |
+ Daily Cost |
+ Actions |
+
+
+
+
+
+ | No roster data found. |
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+