diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..0c37bc7
--- /dev/null
+++ b/assets/css/custom.css
@@ -0,0 +1,113 @@
+
+/* General Body Styling */
+body {
+ background-color: #121212;
+ color: #E0E0E0;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+/* Main wrapper for sidebar and content */
+.main-wrapper {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* Sidebar Styling */
+.sidebar {
+ width: 260px;
+ background-color: #1E1E1E;
+ padding: 1.5rem;
+ border-right: 1px solid #333333;
+}
+
+.sidebar .nav-link {
+ color: #A0A0A0;
+ padding: 0.75rem 1rem;
+ margin-bottom: 0.5rem;
+ border-radius: 0.5rem;
+ font-weight: 500;
+}
+
+.sidebar .nav-link:hover {
+ color: #E0E0E0;
+ background-color: #282828;
+}
+
+.sidebar .nav-link.active {
+ color: #FFFFFF;
+ background-color: #377DFF;
+}
+
+/* Content Area */
+.content-wrapper {
+ flex-grow: 1;
+ padding: 2rem;
+ overflow-y: auto;
+}
+
+/* Top Navbar */
+.top-navbar {
+ background-color: #1E1E1E;
+ border-bottom: 1px solid #333333;
+ padding: 1rem 2rem;
+ color: #E0E0E0;
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+/* Page Header */
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+}
+
+/* Card & Table Styling */
+.card {
+ background-color: #1E1E1E;
+ border: 1px solid #333333;
+ border-radius: 0.5rem;
+}
+
+.table {
+ color: #E0E0E0;
+ border-color: #333333;
+ margin-bottom: 0; /* Remove default margin */
+}
+
+.table th, .table td {
+ border-color: #333333;
+ padding: 1rem; /* Comfortable row height */
+ white-space: nowrap;
+}
+
+.table thead th {
+ background-color: #282828;
+ border-bottom-width: 2px;
+}
+
+.table-hover tbody tr:hover {
+ color: #E0E0E0;
+ background-color: #2a2a2a;
+}
+
+/* Button Styling */
+.btn-primary {
+ background-color: #377DFF;
+ border-color: #377DFF;
+}
+.btn-primary:hover, .btn-primary:focus {
+ background-color: #2968d6;
+ border-color: #2968d6;
+}
+
+/* Make disabled buttons look right */
+.btn.disabled, .btn:disabled {
+ opacity: 0.5;
+}
+
+/* Utility for spacious layout */
+.header-actions .btn {
+ margin-left: 0.5rem;
+}
diff --git a/assets/js/main.js b/assets/js/main.js
new file mode 100644
index 0000000..26d50d9
--- /dev/null
+++ b/assets/js/main.js
@@ -0,0 +1,71 @@
+document.addEventListener('DOMContentLoaded', function () {
+ // New Resource Modal
+ const newResourceBtn = document.getElementById('newResourceBtn');
+ const newResourceModalEl = document.getElementById('newResourceModal');
+ if (newResourceBtn && newResourceModalEl) {
+ const newResourceModal = new bootstrap.Modal(newResourceModalEl);
+ newResourceBtn.addEventListener('click', function () {
+ newResourceModal.show();
+ });
+ }
+
+ // Edit Resource Modal
+ const editResourceModalEl = document.getElementById('editResourceModal');
+ if (editResourceModalEl) {
+ const editResourceModal = new bootstrap.Modal(editResourceModalEl);
+ const editButtons = document.querySelectorAll('.edit-btn');
+
+ editButtons.forEach(button => {
+ button.addEventListener('click', function () {
+ const data = JSON.parse(this.getAttribute('data-row'));
+
+ document.getElementById('edit-id').value = data.id;
+ document.getElementById('edit-sapCode').value = data.sapCode;
+ document.getElementById('edit-fullNameEn').value = data.fullNameEn;
+ document.getElementById('edit-legalEntity').value = data.legalEntity;
+ document.getElementById('edit-functionBusinessUnit').value = data.functionBusinessUnit;
+ document.getElementById('edit-costCenterCode').value = data.costCenterCode;
+ document.getElementById('edit-level').value = data.level;
+ document.getElementById('edit-newAmendedSalary').value = data.newAmendedSalary;
+ document.getElementById('edit-employerContributions').value = data.employerContributions;
+ document.getElementById('edit-cars').value = data.cars;
+ document.getElementById('edit-ticketRestaurant').value = data.ticketRestaurant;
+ document.getElementById('edit-metlife').value = data.metlife;
+ document.getElementById('edit-topusPerMonth').value = data.topusPerMonth;
+
+ editResourceModal.show();
+ });
+ });
+ }
+
+ // View Resource Modal
+ const viewResourceModalEl = document.getElementById('viewResourceModal');
+ if (viewResourceModalEl) {
+ const viewResourceModal = new bootstrap.Modal(viewResourceModalEl);
+ const viewButtons = document.querySelectorAll('.view-btn');
+
+ viewButtons.forEach(button => {
+ button.addEventListener('click', function () {
+ const data = JSON.parse(this.getAttribute('data-row'));
+
+ document.getElementById('view-sapCode').value = data.sapCode;
+ document.getElementById('view-fullNameEn').value = data.fullNameEn;
+ document.getElementById('view-legalEntity').value = data.legalEntity;
+ document.getElementById('view-functionBusinessUnit').value = data.functionBusinessUnit;
+ document.getElementById('view-costCenterCode').value = data.costCenterCode;
+ document.getElementById('view-level').value = data.level;
+ document.getElementById('view-newAmendedSalary').value = '€' + parseFloat(data.newAmendedSalary).toFixed(2);
+ document.getElementById('view-employerContributions').value = '€' + parseFloat(data.employerContributions).toFixed(2);
+ document.getElementById('view-cars').value = '€' + parseFloat(data.cars).toFixed(2);
+ document.getElementById('view-ticketRestaurant').value = '€' + parseFloat(data.ticketRestaurant).toFixed(2);
+ document.getElementById('view-metlife').value = '€' + parseFloat(data.metlife).toFixed(2);
+ document.getElementById('view-topusPerMonth').value = '€' + parseFloat(data.topusPerMonth).toFixed(2);
+ document.getElementById('view-totalSalaryCostWithLabor').value = '€' + parseFloat(data.totalSalaryCostWithLabor).toFixed(2);
+ document.getElementById('view-totalMonthlyCost').value = '€' + parseFloat(data.totalMonthlyCost).toFixed(2);
+ document.getElementById('view-totalAnnualCost').value = '€' + parseFloat(data.totalAnnualCost).toFixed(2);
+
+ viewResourceModal.show();
+ });
+ });
+ }
+});
diff --git a/assets/js/projects.js b/assets/js/projects.js
new file mode 100644
index 0000000..c430474
--- /dev/null
+++ b/assets/js/projects.js
@@ -0,0 +1,35 @@
+document.addEventListener('DOMContentLoaded', function () {
+ // New Project Modal
+ const newProjectBtn = document.getElementById('newProjectBtn');
+ const newProjectModalEl = document.getElementById('newProjectModal');
+ if (newProjectBtn && newProjectModalEl) {
+ const newProjectModal = new bootstrap.Modal(newProjectModalEl);
+ newProjectBtn.addEventListener('click', function () {
+ newProjectModal.show();
+ });
+ }
+
+ // Edit Project Modal
+ const editProjectModalEl = document.getElementById('editProjectModal');
+ if (editProjectModalEl) {
+ const editProjectModal = new bootstrap.Modal(editProjectModalEl);
+ const editButtons = document.querySelectorAll('.edit-btn');
+
+ editButtons.forEach(button => {
+ button.addEventListener('click', function () {
+ const data = JSON.parse(this.getAttribute('data-row'));
+
+ document.getElementById('edit-id').value = data.id;
+ document.getElementById('edit-name').value = data.name;
+ document.getElementById('edit-wbs').value = data.wbs;
+ document.getElementById('edit-startDate').value = data.startDate;
+ document.getElementById('edit-endDate').value = data.endDate;
+ document.getElementById('edit-budget').value = data.budget;
+ document.getElementById('edit-recoverability').value = data.recoverability;
+ document.getElementById('edit-targetMargin').value = data.targetMargin;
+
+ editProjectModal.show();
+ });
+ });
+ }
+});
\ No newline at end of file
diff --git a/db/migrations/001_create_roster_table.sql b/db/migrations/001_create_roster_table.sql
new file mode 100644
index 0000000..454fb54
--- /dev/null
+++ b/db/migrations/001_create_roster_table.sql
@@ -0,0 +1,21 @@
+CREATE TABLE IF NOT EXISTS roster (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ sapCode VARCHAR(255) NOT NULL,
+ fullNameEn VARCHAR(255) NOT NULL,
+ legalEntity VARCHAR(255),
+ functionBusinessUnit VARCHAR(255),
+ costCenterCode VARCHAR(255),
+ level VARCHAR(255),
+ newAmendedSalary DECIMAL(12, 2) DEFAULT 0.00,
+ employerContributions DECIMAL(12, 2) DEFAULT 0.00,
+ cars DECIMAL(12, 2) DEFAULT 0.00,
+ ticketRestaurant DECIMAL(12, 2) DEFAULT 0.00,
+ metlife DECIMAL(12, 2) DEFAULT 0.00,
+ topusPerMonth DECIMAL(12, 2) DEFAULT 0.00,
+ totalSalaryCostWithLabor DECIMAL(12, 2) DEFAULT 0.00,
+ totalMonthlyCost DECIMAL(12, 2) DEFAULT 0.00,
+ totalAnnualCost DECIMAL(14, 2) DEFAULT 0.00,
+ createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE(sapCode)
+);
\ No newline at end of file
diff --git a/db/migrations/002_create_projects_table.sql b/db/migrations/002_create_projects_table.sql
new file mode 100644
index 0000000..b7bb928
--- /dev/null
+++ b/db/migrations/002_create_projects_table.sql
@@ -0,0 +1,13 @@
+CREATE TABLE IF NOT EXISTS `projects` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) NOT NULL,
+ `wbs` varchar(255) DEFAULT NULL,
+ `startDate` date DEFAULT NULL,
+ `endDate` date DEFAULT NULL,
+ `budget` decimal(15,2) DEFAULT 0.00,
+ `recoverability` decimal(5,2) DEFAULT 100.00,
+ `targetMargin` decimal(5,2) DEFAULT 0.00,
+ `createdAt` timestamp NOT NULL DEFAULT current_timestamp(),
+ `updatedAt` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
diff --git a/export.php b/export.php
new file mode 100644
index 0000000..5fdad9b
--- /dev/null
+++ b/export.php
@@ -0,0 +1,33 @@
+query("SELECT * FROM roster ORDER BY fullNameEn");
+ $roster_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+} catch (PDOException $e) {
+ die("Database error: " . $e->getMessage());
+}
+
+// --- CSV EXPORT ---
+$filename = "roster_export_" . date('Y-m-d') . ".csv";
+
+header('Content-Type: text/csv; charset=utf-8');
+header('Content-Disposition: attachment; filename="' . $filename . '"');
+
+$output = fopen('php://output', 'w');
+
+// Add headers
+if (!empty($roster_data)) {
+ fputcsv($output, array_keys($roster_data[0]));
+}
+
+// Add data
+foreach ($roster_data as $row) {
+ fputcsv($output, $row);
+}
+
+fclose($output);
+exit();
diff --git a/import.php b/import.php
new file mode 100644
index 0000000..42e4821
--- /dev/null
+++ b/import.php
@@ -0,0 +1,134 @@
+beginTransaction();
+
+ $header = fgetcsv($handle);
+ if ($header === false) {
+ redirect_with_message('error', 'Could not read the CSV header.');
+ }
+
+ // Normalize headers to camelCase
+ $normalized_header = array_map(function($h) {
+ $h = trim($h);
+ $h = preg_replace('/[^a-zA-Z0-9_\s]/', '', $h); // Remove special chars
+ $h = preg_replace('/\s+/', ' ', $h); // Normalize whitespace
+ $h = str_replace(' ', '', ucwords(strtolower($h))); // To PascalCase
+ return lcfirst($h); // To camelCase
+ }, $header);
+
+ $expected_headers = [
+ 'sapCode', 'fullNameEn', 'legalEntity', 'functionBusinessUnit', 'costCenterCode', 'level',
+ 'newAmendedSalary', 'employerContributions', 'cars', 'ticketRestaurant', 'metlife', 'topusPerMonth'
+ ];
+
+ $col_map = array_flip($normalized_header);
+
+ foreach ($expected_headers as $expected) {
+ if (!isset($col_map[$expected])) {
+ redirect_with_message('error', "Missing required column: '{$expected}'. Please check the CSV file header.");
+ }
+ }
+
+ $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)
+ ON DUPLICATE KEY UPDATE
+ fullNameEn = VALUES(fullNameEn),
+ legalEntity = VALUES(legalEntity),
+ functionBusinessUnit = VALUES(functionBusinessUnit),
+ costCenterCode = VALUES(costCenterCode),
+ `level` = VALUES(`level`),
+ newAmendedSalary = VALUES(newAmendedSalary),
+ employerContributions = VALUES(employerContributions),
+ cars = VALUES(cars),
+ ticketRestaurant = VALUES(ticketRestaurant),
+ metlife = VALUES(metlife),
+ topusPerMonth = VALUES(topusPerMonth),
+ totalSalaryCostWithLabor = VALUES(totalSalaryCostWithLabor),
+ totalMonthlyCost = VALUES(totalMonthlyCost),
+ totalAnnualCost = VALUES(totalAnnualCost)";
+
+ $stmt = $pdo->prepare($sql);
+
+ $rows_processed = 0;
+ while (($row = fgetcsv($handle)) !== false) {
+ $data = array_combine(array_keys($col_map), $row);
+
+ $sapCode = $data['sapCode'] ?? null;
+ if (empty($sapCode)) {
+ continue; // Skip rows without a SAP Code
+ }
+
+ // Prepare data and perform calculations
+ $newAmendedSalary = (float)($data['newAmendedSalary'] ?? 0);
+ $employerContributions = (float)($data['employerContributions'] ?? 0);
+ $cars = (float)($data['cars'] ?? 0);
+ $ticketRestaurant = (float)($data['ticketRestaurant'] ?? 0);
+ $metlife = (float)($data['metlife'] ?? 0);
+ $topusPerMonth = (float)($data['topusPerMonth'] ?? 0);
+
+ $totalSalaryCostWithLabor = $newAmendedSalary + $employerContributions;
+ $totalMonthlyCost = $totalSalaryCostWithLabor + $cars + $ticketRestaurant + $metlife + $topusPerMonth;
+ $totalAnnualCost = $totalMonthlyCost * 14;
+
+ $stmt->execute([
+ ':sapCode' => $sapCode,
+ ':fullNameEn' => $data['fullNameEn'] ?? null,
+ ':legalEntity' => $data['legalEntity'] ?? null,
+ ':functionBusinessUnit' => $data['functionBusinessUnit'] ?? null,
+ ':costCenterCode' => $data['costCenterCode'] ?? null,
+ ':level' => $data['level'] ?? null,
+ ':newAmendedSalary' => $newAmendedSalary,
+ ':employerContributions' => $employerContributions,
+ ':cars' => $cars,
+ ':ticketRestaurant' => $ticketRestaurant,
+ ':metlife' => $metlife,
+ ':topusPerMonth' => $topusPerMonth,
+ ':totalSalaryCostWithLabor' => $totalSalaryCostWithLabor,
+ ':totalMonthlyCost' => $totalMonthlyCost,
+ ':totalAnnualCost' => $totalAnnualCost
+ ]);
+ $rows_processed++;
+ }
+
+ $pdo->commit();
+ fclose($handle);
+ redirect_with_message('success', "Successfully imported {$rows_processed} records.");
+
+} catch (Exception $e) {
+ if ($pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+ fclose($handle);
+ error_log("Import Error: " . $e->getMessage());
+ redirect_with_message('error', 'An error occurred during the import process. Details: ' . $e->getMessage());
+}
\ No newline at end of file
diff --git a/index.php b/index.php
index 7205f3d..6f6ce6f 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,645 @@
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
+ ]);
+
+ // 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);
+
+ // 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
+ 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
+ ]);
+
+ 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();
+ execute_sql_from_file($pdo, __DIR__ . '/db/migrations/001_create_roster_table.sql');
+ 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) {
+ $db_error = "Database connection failed: " . $e->getMessage();
+}
+
+// --- RENDER PAGE ---
?>
-
-
+
+
-
-
- New Style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Project Financials
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
Analyzing your requirements and generating your website…
-
- Loading…
-
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
+
+ Project Financials
-
-
+
+
+
+
+
+ {$message}
+
+
";
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 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 |
+ Actions |
+
+
+
+
+
+ | No roster data found. |
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+ € |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects.php b/projects.php
new file mode 100644
index 0000000..2a74934
--- /dev/null
+++ b/projects.php
@@ -0,0 +1,326 @@
+prepare($insert_sql);
+ $stmt->execute([
+ ':name' => $_POST['name'],
+ ':wbs' => $_POST['wbs'] ?? null,
+ ':startDate' => empty($_POST['startDate']) ? null : $_POST['startDate'],
+ ':endDate' => empty($_POST['endDate']) ? null : $_POST['endDate'],
+ ':budget' => (float)($_POST['budget'] ?? 0),
+ ':recoverability' => (float)($_POST['recoverability'] ?? 100),
+ ':targetMargin' => (float)($_POST['targetMargin'] ?? 0),
+ ]);
+ 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'] === 'delete_project') {
+ try {
+ require_once __DIR__ . '/db/config.php';
+ $pdo_delete = db();
+ $delete_sql = "DELETE FROM projects 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_project') {
+ if (empty($_POST['id']) || empty($_POST['name'])) {
+ $form_error = "ID and Project Name are required for an update.";
+ } else {
+ try {
+ require_once __DIR__ . '/db/config.php';
+ $pdo_update = db();
+ $update_sql = "UPDATE projects SET
+ name = :name,
+ wbs = :wbs,
+ startDate = :startDate,
+ endDate = :endDate,
+ budget = :budget,
+ recoverability = :recoverability,
+ targetMargin = :targetMargin
+ WHERE id = :id";
+
+ $stmt = $pdo_update->prepare($update_sql);
+ $stmt->execute([
+ ':id' => $_POST['id'],
+ ':name' => $_POST['name'],
+ ':wbs' => $_POST['wbs'] ?? null,
+ ':startDate' => empty($_POST['startDate']) ? null : $_POST['startDate'],
+ ':endDate' => empty($_POST['endDate']) ? null : $_POST['endDate'],
+ ':budget' => (float)($_POST['budget'] ?? 0),
+ ':recoverability' => (float)($_POST['recoverability'] ?? 100),
+ ':targetMargin' => (float)($_POST['targetMargin'] ?? 0),
+ ]);
+
+ header("Location: " . $_SERVER['PHP_SELF']);
+ exit();
+
+ } catch (PDOException $e) {
+ $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;
+ }
+}
+
+$projects_data = [];
+try {
+ $pdo = db();
+ execute_sql_from_file($pdo, __DIR__ . '/db/migrations/002_create_projects_table.sql');
+ $stmt = $pdo->query("SELECT * FROM projects ORDER BY name");
+ $projects_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+} catch (PDOException $e) {
+ $db_error = "Database connection failed: " . $e->getMessage();
+}
+
+// --- RENDER PAGE ---
+?>
+
+
+
+
+
+ Projects - Project Financials
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Project Financials
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Name |
+ WBS |
+ Start Date |
+ End Date |
+ Budget |
+ Recoverability |
+ Target Margin |
+ Actions |
+
+
+
+
+
+ | No projects found. |
+
+
+
+
+ |
+ |
+ |
+ |
+ € |
+ % |
+ % |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file