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());
|
||||
}
|
||||
777
index.php
777
index.php
@ -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>€<?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>
|
||||
</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 (€)</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>
|
||||
</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