From a28074f4f83795222a5163d246033188cd8989cf Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 25 Nov 2025 21:05:38 +0000 Subject: [PATCH] Roster + Projects --- assets/css/custom.css | 113 +++ assets/js/main.js | 71 ++ assets/js/projects.js | 35 + db/migrations/001_create_roster_table.sql | 21 + db/migrations/002_create_projects_table.sql | 13 + export.php | 33 + import.php | 134 ++++ index.php | 777 ++++++++++++++++---- projects.php | 326 ++++++++ 9 files changed, 1382 insertions(+), 141 deletions(-) create mode 100644 assets/css/custom.css create mode 100644 assets/js/main.js create mode 100644 assets/js/projects.js create mode 100644 db/migrations/001_create_roster_table.sql create mode 100644 db/migrations/002_create_projects_table.sql create mode 100644 export.php create mode 100644 import.php create mode 100644 projects.php 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… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+
+ Project Financials
-
- +
+ + +
+ + {$message} + +
"; + } + ?> + +
+ +
+ + + + +
+
+
+
+ + +
+
+ + Clear +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SAP CodeFull NameLegal EntityBusiness UnitCost CenterLevelSalaryContributionsCarsTicket RestaurantMetlifeTopus/MonthTotal Salary CostTotal Monthly CostTotal Annual CostActions
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 +
+
+ + +
+ +
+ +
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameWBSStart DateEnd DateBudgetRecoverabilityTarget MarginActions
No projects found.
%% +
+ +
+ + + +
+
+
+
+
+
+
+ + + + + + + + + + + \ No newline at end of file