Roster + Projects

This commit is contained in:
Flatlogic Bot 2025-11-25 21:05:38 +00:00
parent 5ed87e3c99
commit a28074f4f8
9 changed files with 1382 additions and 141 deletions

113
assets/css/custom.css Normal file
View 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
View 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
View 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();
});
});
}
});

View 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)
);

View 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
View 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
View 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());
}

777
index.php
View File

@ -1,150 +1,645 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
// --- FORM PROCESSING ---
$form_error = null;
$form_success = null;
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_roster') {
// 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>
<html lang="en">
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<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.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
: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>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Financials</title>
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'Project Financials Management Tool'); ?>">
<meta property="og:title" content="Project Financials">
<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'] ?? ''); ?>">
<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>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</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 class="top-navbar">
Project Financials
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
<div class="main-wrapper">
<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>&euro;<?php echo number_format($row['newAmendedSalary'], 2); ?></td>
<td>&euro;<?php echo number_format($row['employerContributions'], 2); ?></td>
<td>&euro;<?php echo number_format($row['cars'], 2); ?></td>
<td>&euro;<?php echo number_format($row['ticketRestaurant'], 2); ?></td>
<td>&euro;<?php echo number_format($row['metlife'], 2); ?></td>
<td>&euro;<?php echo number_format($row['topusPerMonth'], 2); ?></td>
<td>&euro;<?php echo number_format($row['totalSalaryCostWithLabor'], 2); ?></td>
<td>&euro;<?php echo number_format($row['totalMonthlyCost'], 2); ?></td>
<td>&euro;<?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>
</main>
</div>
<!-- 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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</label>
<input type="text" class="form-control" id="view-employerContributions" readonly>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Cars (&euro;)</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 (&euro;)</label>
<input type="text" class="form-control" id="view-ticketRestaurant" readonly>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Metlife (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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 (&euro;)</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>
</html>

326
projects.php Normal file
View 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>&euro;<?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 (&euro;)</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 (&euro;)</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>