Roster + Projects
This commit is contained in:
parent
5ed87e3c99
commit
a28074f4f8
113
assets/css/custom.css
Normal file
113
assets/css/custom.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
71
assets/js/main.js
Normal file
71
assets/js/main.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
35
assets/js/projects.js
Normal file
35
assets/js/projects.js
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
21
db/migrations/001_create_roster_table.sql
Normal file
21
db/migrations/001_create_roster_table.sql
Normal file
@ -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)
|
||||||
|
);
|
||||||
13
db/migrations/002_create_projects_table.sql
Normal file
13
db/migrations/002_create_projects_table.sql
Normal file
@ -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;
|
||||||
33
export.php
Normal file
33
export.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
// --- DATABASE CONNECTION ---
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->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();
|
||||||
134
import.php
Normal file
134
import.php
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
function redirect_with_message($status, $message) {
|
||||||
|
header("Location: index.php?import_status=$status&import_message=" . urlencode($message));
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_FILES['importFile'])) {
|
||||||
|
redirect_with_message('error', 'Invalid request.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['importFile'];
|
||||||
|
|
||||||
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
redirect_with_message('error', 'File upload failed with error code: ' . $file['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
|
if ($file_ext !== 'csv') {
|
||||||
|
redirect_with_message('error', 'Invalid file type. Only .csv files are accepted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = fopen($file['tmp_name'], 'r');
|
||||||
|
if ($handle === false) {
|
||||||
|
redirect_with_message('error', 'Could not open the uploaded file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$pdo->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());
|
||||||
|
}
|
||||||
769
index.php
769
index.php
@ -1,150 +1,645 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
// --- FORM PROCESSING ---
|
||||||
@ini_set('display_errors', '1');
|
$form_error = null;
|
||||||
@error_reporting(E_ALL);
|
$form_success = null;
|
||||||
@date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_roster') {
|
||||||
$now = date('Y-m-d H:i:s');
|
// Basic validation
|
||||||
|
if (empty($_POST['sapCode']) || empty($_POST['fullNameEn'])) {
|
||||||
|
$form_error = "SAP Code and Full Name are required.";
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
$pdo_form = 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;
|
||||||
|
|
||||||
|
$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_form->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 ---
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-bs-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>New Style</title>
|
<title>Project Financials</title>
|
||||||
<?php
|
|
||||||
// Read project preview data from environment
|
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'Project Financials Management Tool'); ?>">
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
<meta property="og:title" content="Project Financials">
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
<meta property="og:description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'Manage your project financials, roster, and budget.'); ?>">
|
||||||
?>
|
<meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
||||||
<?php if ($projectDescription): ?>
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<!-- Meta description -->
|
|
||||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<!-- Open Graph meta tags -->
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
|
||||||
<!-- Twitter meta tags -->
|
|
||||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($projectImageUrl): ?>
|
|
||||||
<!-- Open Graph image -->
|
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
|
||||||
<!-- Twitter image -->
|
|
||||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
|
||||||
<?php endif; ?>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<style>
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
:root {
|
|
||||||
--bg-color-start: #6a11cb;
|
|
||||||
--bg-color-end: #2575fc;
|
|
||||||
--text-color: #ffffff;
|
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
|
||||||
animation: bg-pan 20s linear infinite;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
@keyframes bg-pan {
|
|
||||||
0% { background-position: 0% 0%; }
|
|
||||||
100% { background-position: 100% 100%; }
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background: var(--card-bg-color);
|
|
||||||
border: 1px solid var(--card-border-color);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 2rem;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
.loader {
|
|
||||||
margin: 1.25rem auto 1.25rem;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
|
||||||
border-top-color: #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
from { transform: rotate(0deg); }
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
.hint {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px; height: 1px;
|
|
||||||
padding: 0; margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap; border: 0;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background: rgba(0,0,0,0.2);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<div class="top-navbar">
|
||||||
<div class="card">
|
Project Financials
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
</div>
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<div class="main-wrapper">
|
||||||
<span class="sr-only">Loading…</span>
|
<nav class="sidebar">
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="index.php"><i class="bi bi-people-fill me-2"></i>Roster</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="projects.php"><i class="bi bi-briefcase-fill me-2"></i>Projects</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="content-wrapper">
|
||||||
|
<?php
|
||||||
|
if (isset($_GET['import_status'])) {
|
||||||
|
$status = $_GET['import_status'];
|
||||||
|
$message = htmlspecialchars($_GET['import_message'] ?? '');
|
||||||
|
$alert_class = $status === 'success' ? 'alert-success' : 'alert-danger';
|
||||||
|
echo "<div class='alert {$alert_class} alert-dismissible fade show' role='alert'>
|
||||||
|
{$message}
|
||||||
|
<button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Close'></button>
|
||||||
|
</div>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<?php if (isset($db_error)): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo htmlspecialchars($db_error); ?></div>
|
||||||
|
<?php elseif (isset($form_error)): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo htmlspecialchars($form_error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="h2">Roster</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#importModal"><i class="bi bi-upload me-2"></i>Import Excel</button>
|
||||||
|
<a href="export.php" class="btn btn-secondary"><i class="bi bi-download me-2"></i>Export Excel</a>
|
||||||
|
<button class="btn btn-primary" id="newResourceBtn"><i class="bi bi-plus-circle-fill me-2"></i>New Resource</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<form action="index.php" method="GET" class="row g-3 align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label for="search" class="visually-hidden">Search</label>
|
||||||
|
<input type="text" class="form-control" id="search" name="search" placeholder="Search by name or SAP..." value="<?php echo htmlspecialchars($_GET['search'] ?? ''); ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button type="submit" class="btn btn-primary">Search</button>
|
||||||
|
<a href="index.php" class="btn btn-secondary">Clear</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>SAP Code</th>
|
||||||
|
<th>Full Name</th>
|
||||||
|
<th>Legal Entity</th>
|
||||||
|
<th>Business Unit</th>
|
||||||
|
<th>Cost Center</th>
|
||||||
|
<th>Level</th>
|
||||||
|
<th>Salary</th>
|
||||||
|
<th>Contributions</th>
|
||||||
|
<th>Cars</th>
|
||||||
|
<th>Ticket Restaurant</th>
|
||||||
|
<th>Metlife</th>
|
||||||
|
<th>Topus/Month</th>
|
||||||
|
<th>Total Salary Cost</th>
|
||||||
|
<th>Total Monthly Cost</th>
|
||||||
|
<th>Total Annual Cost</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($roster_data)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="15" class="text-center text-secondary">No roster data found.</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($roster_data as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($row['sapCode']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($row['fullNameEn']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($row['legalEntity']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($row['functionBusinessUnit']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($row['costCenterCode']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($row['level']); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['newAmendedSalary'], 2); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['employerContributions'], 2); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['cars'], 2); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['ticketRestaurant'], 2); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['metlife'], 2); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['topusPerMonth'], 2); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['totalSalaryCostWithLabor'], 2); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['totalMonthlyCost'], 2); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['totalAnnualCost'], 2); ?></td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex">
|
||||||
|
<button class="btn btn-sm btn-outline-info me-2 view-btn"
|
||||||
|
data-row='<?php echo htmlspecialchars(json_encode($row), ENT_QUOTES, 'UTF-8'); ?>'>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary me-2 edit-btn"
|
||||||
|
data-row='<?php echo htmlspecialchars(json_encode($row), ENT_QUOTES, 'UTF-8'); ?>'>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<form action="index.php" method="POST" onsubmit="return confirm('Are you sure you want to delete this item?');">
|
||||||
|
<input type="hidden" name="action" value="delete_roster">
|
||||||
|
<input type="hidden" name="id" value="<?php echo $row['id']; ?>">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
|
||||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
|
||||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
</div>
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
|
||||||
</footer>
|
<!-- New Resource Modal -->
|
||||||
|
<div class="modal fade" id="newResourceModal" tabindex="-1" aria-labelledby="newResourceModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form action="index.php" method="POST">
|
||||||
|
<input type="hidden" name="action" value="create_roster">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="newResourceModalLabel">New Resource</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="sapCode" class="form-label">SAP Code</label>
|
||||||
|
<input type="text" class="form-control" id="sapCode" name="sapCode" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="fullNameEn" class="form-label">Full Name</label>
|
||||||
|
<input type="text" class="form-control" id="fullNameEn" name="fullNameEn" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="legalEntity" class="form-label">Legal Entity</label>
|
||||||
|
<input type="text" class="form-control" id="legalEntity" name="legalEntity">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="functionBusinessUnit" class="form-label">Function Business Unit</label>
|
||||||
|
<input type="text" class="form-control" id="functionBusinessUnit" name="functionBusinessUnit">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="costCenterCode" class="form-label">Cost Center Code</label>
|
||||||
|
<input type="text" class="form-control" id="costCenterCode" name="costCenterCode">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="level" class="form-label">Level</label>
|
||||||
|
<input type="text" class="form-control" id="level" name="level">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="newAmendedSalary" class="form-label">New Amended Salary (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="newAmendedSalary" name="newAmendedSalary" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="employerContributions" class="form-label">Employer Contributions (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="employerContributions" name="employerContributions" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="cars" class="form-label">Cars (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="cars" name="cars" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="ticketRestaurant" class="form-label">Ticket Restaurant (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="ticketRestaurant" name="ticketRestaurant" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="metlife" class="form-label">Metlife (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="metlife" name="metlife" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="topusPerMonth" class="form-label">Topus/Month (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="topusPerMonth" name="topusPerMonth" value="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Resource</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
|
|
||||||
|
<!-- Edit Resource Modal -->
|
||||||
|
<div class="modal fade" id="editResourceModal" tabindex="-1" aria-labelledby="editResourceModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form action="index.php" method="POST">
|
||||||
|
<input type="hidden" name="action" value="update_roster">
|
||||||
|
<input type="hidden" name="id" id="edit-id">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editResourceModalLabel">Edit Resource</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-sapCode" class="form-label">SAP Code</label>
|
||||||
|
<input type="text" class="form-control" id="edit-sapCode" name="sapCode" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-fullNameEn" class="form-label">Full Name</label>
|
||||||
|
<input type="text" class="form-control" id="edit-fullNameEn" name="fullNameEn" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-legalEntity" class="form-label">Legal Entity</label>
|
||||||
|
<input type="text" class="form-control" id="edit-legalEntity" name="legalEntity">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-functionBusinessUnit" class="form-label">Function Business Unit</label>
|
||||||
|
<input type="text" class="form-control" id="edit-functionBusinessUnit" name="functionBusinessUnit">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-costCenterCode" class="form-label">Cost Center Code</label>
|
||||||
|
<input type="text" class="form-control" id="edit-costCenterCode" name="costCenterCode">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-level" class="form-label">Level</label>
|
||||||
|
<input type="text" class="form-control" id="edit-level" name="level">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-newAmendedSalary" class="form-label">New Amended Salary (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="edit-newAmendedSalary" name="newAmendedSalary">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-employerContributions" class="form-label">Employer Contributions (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="edit-employerContributions" name="employerContributions">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-cars" class="form-label">Cars (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="edit-cars" name="cars">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-ticketRestaurant" class="form-label">Ticket Restaurant (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="edit-ticketRestaurant" name="ticketRestaurant">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-metlife" class="form-label">Metlife (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="edit-metlife" name="metlife">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-topusPerMonth" class="form-label">Topus/Month (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="edit-topusPerMonth" name="topusPerMonth">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View Resource Modal -->
|
||||||
|
<div class="modal fade" id="viewResourceModal" tabindex="-1" aria-labelledby="viewResourceModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="viewResourceModalLabel">View Resource</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">SAP Code</label>
|
||||||
|
<input type="text" class="form-control" id="view-sapCode" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Full Name</label>
|
||||||
|
<input type="text" class="form-control" id="view-fullNameEn" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Legal Entity</label>
|
||||||
|
<input type="text" class="form-control" id="view-legalEntity" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Function Business Unit</label>
|
||||||
|
<input type="text" class="form-control" id="view-functionBusinessUnit" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Cost Center Code</label>
|
||||||
|
<input type="text" class="form-control" id="view-costCenterCode" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Level</label>
|
||||||
|
<input type="text" class="form-control" id="view-level" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">New Amended Salary (€)</label>
|
||||||
|
<input type="text" class="form-control" id="view-newAmendedSalary" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Employer Contributions (€)</label>
|
||||||
|
<input type="text" class="form-control" id="view-employerContributions" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Cars (€)</label>
|
||||||
|
<input type="text" class="form-control" id="view-cars" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Ticket Restaurant (€)</label>
|
||||||
|
<input type="text" class="form-control" id="view-ticketRestaurant" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Metlife (€)</label>
|
||||||
|
<input type="text" class="form-control" id="view-metlife" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Topus/Month (€)</label>
|
||||||
|
<input type="text" class="form-control" id="view-topusPerMonth" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Total Salary Cost With Labor (€)</label>
|
||||||
|
<input type="text" class="form-control" id="view-totalSalaryCostWithLabor" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Total Monthly Cost (€)</label>
|
||||||
|
<input type="text" class="form-control" id="view-totalMonthlyCost" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Total Annual Cost (€)</label>
|
||||||
|
<input type="text" class="form-control" id="view-totalAnnualCost" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Import Modal -->
|
||||||
|
<div class="modal fade" id="importModal" tabindex="-1" aria-labelledby="importModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form action="import.php" method="POST" enctype="multipart/form-data">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="importModalLabel">Import Excel</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="importFile" class="form-label">Select .csv or .xlsx file</label>
|
||||||
|
<input class="form-control" type="file" id="importFile" name="importFile" accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
The file should have columns matching the roster table: sapCode, fullNameEn, legalEntity, etc.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Upload and Import</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
326
projects.php
Normal file
326
projects.php
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
<?php
|
||||||
|
// --- FORM PROCESSING ---
|
||||||
|
$form_error = null;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_project') {
|
||||||
|
if (empty($_POST['name'])) {
|
||||||
|
$form_error = "Project Name is required.";
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
$pdo_form = db();
|
||||||
|
$insert_sql = "INSERT INTO projects (name, wbs, startDate, endDate, budget, recoverability, targetMargin) VALUES (:name, :wbs, :startDate, :endDate, :budget, :recoverability, :targetMargin)";
|
||||||
|
$stmt = $pdo_form->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 ---
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Projects - Project Financials</title>
|
||||||
|
|
||||||
|
<meta name="description" content="Manage Projects - Project Financials Management Tool">
|
||||||
|
<meta property="og:title" content="Projects - Project Financials">
|
||||||
|
<meta property="og:description" content="Manage your project financials, roster, and budget.">
|
||||||
|
<meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="top-navbar">
|
||||||
|
Project Financials
|
||||||
|
</div>
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<nav class="sidebar">
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="index.php"><i class="bi bi-people-fill me-2"></i>Roster</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="projects.php"><i class="bi bi-briefcase-fill me-2"></i>Projects</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="content-wrapper">
|
||||||
|
<?php if (isset($db_error)): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo htmlspecialchars($db_error); ?></div>
|
||||||
|
<?php elseif (isset($form_error)): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo htmlspecialchars($form_error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="h2">Projects</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="btn btn-secondary disabled" disabled><i class="bi bi-upload me-2"></i>Import Excel</button>
|
||||||
|
<button class="btn btn-secondary disabled" disabled><i class="bi bi-download me-2"></i>Export Excel</button>
|
||||||
|
<button class="btn btn-primary" id="newProjectBtn"><i class="bi bi-plus-circle-fill me-2"></i>New Project</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>WBS</th>
|
||||||
|
<th>Start Date</th>
|
||||||
|
<th>End Date</th>
|
||||||
|
<th>Budget</th>
|
||||||
|
<th>Recoverability</th>
|
||||||
|
<th>Target Margin</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($projects_data)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center text-secondary">No projects found.</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($projects_data as $row): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($row['name']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($row['wbs']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($row['startDate']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($row['endDate']); ?></td>
|
||||||
|
<td>€<?php echo number_format($row['budget'], 2); ?></td>
|
||||||
|
<td><?php echo number_format($row['recoverability'], 2); ?>%</td>
|
||||||
|
<td><?php echo number_format($row['targetMargin'], 2); ?>%</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex">
|
||||||
|
<button class="btn btn-sm btn-outline-primary me-2 edit-btn"
|
||||||
|
data-row='<?php echo htmlspecialchars(json_encode($row), ENT_QUOTES, 'UTF-8'); ?>'>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<form action="projects.php" method="POST" onsubmit="return confirm('Are you sure you want to delete this item?');">
|
||||||
|
<input type="hidden" name="action" value="delete_project">
|
||||||
|
<input type="hidden" name="id" value="<?php echo $row['id']; ?>">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Project Modal -->
|
||||||
|
<div class="modal fade" id="newProjectModal" tabindex="-1" aria-labelledby="newProjectModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form action="projects.php" method="POST">
|
||||||
|
<input type="hidden" name="action" value="create_project">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="newProjectModalLabel">New Project</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">Project Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wbs" class="form-label">WBS</label>
|
||||||
|
<input type="text" class="form-control" id="wbs" name="wbs">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="startDate" class="form-label">Start Date</label>
|
||||||
|
<input type="date" class="form-control" id="startDate" name="startDate">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="endDate" class="form-label">End Date</label>
|
||||||
|
<input type="date" class="form-control" id="endDate" name="endDate">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="budget" class="form-label">Budget (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="budget" name="budget" value="0">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="recoverability" class="form-label">Recoverability (%)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="recoverability" name="recoverability" value="100">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="targetMargin" class="form-label">Target Margin (%)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="targetMargin" name="targetMargin" value="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Project</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="assets/js/projects.js?v=<?php echo time(); ?>"></script>
|
||||||
|
|
||||||
|
<!-- Edit Project Modal -->
|
||||||
|
<div class="modal fade" id="editProjectModal" tabindex="-1" aria-labelledby="editProjectModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form action="projects.php" method="POST">
|
||||||
|
<input type="hidden" name="action" value="update_project">
|
||||||
|
<input type="hidden" name="id" id="edit-id">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editProjectModalLabel">Edit Project</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="edit-name" class="form-label">Project Name</label>
|
||||||
|
<input type="text" class="form-control" id="edit-name" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="edit-wbs" class="form-label">WBS</label>
|
||||||
|
<input type="text" class="form-control" id="edit-wbs" name="wbs">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-startDate" class="form-label">Start Date</label>
|
||||||
|
<input type="date" class="form-control" id="edit-startDate" name="startDate">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-endDate" class="form-label">End Date</label>
|
||||||
|
<input type="date" class="form-control" id="edit-endDate" name="endDate">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="edit-budget" class="form-label">Budget (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="edit-budget" name="budget">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-recoverability" class="form-label">Recoverability (%)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="edit-recoverability" name="recoverability">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="edit-targetMargin" class="form-label">Target Margin (%)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="edit-targetMargin" name="targetMargin">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user