Revert to version 73cf51a

This commit is contained in:
Flatlogic Bot 2025-11-26 16:18:15 +00:00
parent 89109f468e
commit 38ec03060f
19 changed files with 689 additions and 1436 deletions

View File

@ -1,295 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const dataScript = document.getElementById('financial-data');
if (!dataScript) return;
const appData = JSON.parse(dataScript.textContent);
const { projectId, months, metrics, initialFinancialData, baseData, overrides } = appData;
let state = {
isOverrideActive: false,
overrideMonth: null,
originalTableState: {},
currentFinancialData: JSON.parse(JSON.stringify(initialFinancialData)) // Deep copy
};
const table = document.getElementById('financials-table');
if (!table) return;
// UTILITY FUNCTIONS
const formatCurrency = (value) => `${(value || 0).toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const formatMargin = (value) => `${((value || 0) * 100).toFixed(2).replace('.', ',')}%`;
const parseLocaleNumber = (stringNumber) => {
if (typeof stringNumber !== 'string') return stringNumber;
// Remove thousands separators (.), then replace decimal comma with a period.
return Number(String(stringNumber).replace(/\./g, '').replace(',', '.'));
};
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// MAIN LOGIC
function recalculateFinancials(overrideMonth, overrideValues) {
const newData = JSON.parse(JSON.stringify(state.currentFinancialData));
const overrideMonthIndex = months.indexOf(overrideMonth);
if (overrideMonthIndex === -1) {
console.error("Override month not found in months array!");
return state.currentFinancialData;
}
// Step 1: Apply the override values for the specific override month.
for (const key in overrideValues) {
overrideValues[key] = parseFloat(overrideValues[key] || 0);
}
newData['Opening Balance'][overrideMonth] = overrideValues['Opening Balance'];
newData.Billings[overrideMonth] = overrideValues.Billings;
newData.WIP[overrideMonth] = overrideValues.WIP;
newData.Expenses[overrideMonth] = overrideValues.Expenses;
newData.Cost[overrideMonth] = overrideValues.Cost;
newData.Payment[overrideMonth] = overrideValues.Payment;
let nsr = newData.WIP[overrideMonth] + newData.Billings[overrideMonth] - newData['Opening Balance'][overrideMonth] - newData.Expenses[overrideMonth];
newData.NSR[overrideMonth] = nsr;
let margin = (nsr !== 0) ? ((nsr - newData.Cost[overrideMonth]) / nsr) : 0;
newData.Margin[overrideMonth] = margin;
// Step 2: Recalculate all subsequent months
for (let i = overrideMonthIndex + 1; i < months.length; i++) {
const month = months[i];
const prevMonth = months[i - 1];
const prevCumulativeBilling = parseFloat(newData.Billings[prevMonth] || 0);
const prevCumulativeCost = parseFloat(newData.Cost[prevMonth] || 0);
const prevCumulativeExpenses = parseFloat(newData.Expenses[prevMonth] || 0);
const prevWIP = parseFloat(newData.WIP[prevMonth] || 0);
const monthlyCost = parseFloat(baseData.monthly_costs[month] || 0);
const monthlyBilling = parseFloat(baseData.monthly_billing[month] || 0);
const monthlyExpenses = parseFloat(baseData.monthly_expenses[month] || 0);
const monthlyWIPChange = parseFloat(baseData.monthly_wip[month] || 0);
const newCumulativeBilling = prevCumulativeBilling + monthlyBilling;
const newCumulativeCost = prevCumulativeCost + monthlyCost;
const newCumulativeExpenses = prevCumulativeExpenses + monthlyExpenses;
const newWIP = prevWIP + monthlyExpenses + monthlyWIPChange - monthlyBilling;
newData.Billings[month] = newCumulativeBilling;
newData.Cost[month] = newCumulativeCost;
newData.Expenses[month] = newCumulativeExpenses;
newData.WIP[month] = newWIP;
// THE FIX: Carry over the previous month's Net Service Revenue as the next month's Opening Balance.
newData['Opening Balance'][month] = newData.NSR[prevMonth] || 0;
newData.Payment[month] = 0;
nsr = newWIP + newCumulativeBilling - newData['Opening Balance'][month] - newCumulativeExpenses;
newData.NSR[month] = nsr;
margin = (nsr !== 0) ? ((nsr - newCumulativeCost) / nsr) : 0;
newData.Margin[month] = margin;
}
return newData;
}
function updateTable(newData) {
state.currentFinancialData = newData;
for (const metric of metrics) {
for (const month of months) {
const cell = table.querySelector(`td[data-month="${month}"][data-metric="${metric}"]`);
if (cell && cell.firstElementChild?.tagName !== 'INPUT') {
const value = newData[metric][month];
cell.textContent = metric === 'Margin' ? formatMargin(value) : formatCurrency(value);
}
}
}
}
function recalculateForOverrideMonth(overrideMonth, overrideValues) {
const newData = JSON.parse(JSON.stringify(state.currentFinancialData)); // Deep copy
// Use the user's input values for the override month
newData['Opening Balance'][overrideMonth] = overrideValues['Opening Balance'];
newData.Billings[overrideMonth] = overrideValues.Billings;
newData.WIP[overrideMonth] = overrideValues.WIP;
newData.Expenses[overrideMonth] = overrideValues.Expenses;
newData.Cost[overrideMonth] = overrideValues.Cost;
newData.Payment[overrideMonth] = overrideValues.Payment;
// Recalculate dependent metrics for the override month
const nsr = newData.WIP[overrideMonth] + newData.Billings[overrideMonth] - newData['Opening Balance'][overrideMonth] - newData.Expenses[overrideMonth];
newData.NSR[overrideMonth] = nsr;
const margin = (nsr !== 0) ? ((nsr - newData.Cost[overrideMonth]) / nsr) : 0;
newData.Margin[overrideMonth] = margin;
return newData;
}
function handleInputChange() {
const overrideValues = {};
const editableMetrics = ['Opening Balance', 'Billings', 'WIP', 'Expenses', 'Cost', 'Payment'];
editableMetrics.forEach(metric => {
const input = table.querySelector(`td[data-month="${state.overrideMonth}"][data-metric="${metric}"] input`);
overrideValues[metric] = parseLocaleNumber(input.value) || 0;
});
const recalculatedData = recalculateForOverrideMonth(state.overrideMonth, overrideValues);
updateTable(recalculatedData);
}
function enterOverrideMode(month) {
if (state.isOverrideActive) return;
state.isOverrideActive = true;
state.overrideMonth = month;
const editableMetrics = ['Opening Balance', 'Billings', 'WIP', 'Expenses', 'Cost', 'Payment'];
metrics.forEach(metric => {
const cell = table.querySelector(`td[data-month="${month}"][data-metric="${metric}"]`);
if (!cell) return;
const originalValue = state.currentFinancialData[metric][month];
state.originalTableState[metric] = cell.innerHTML;
if (editableMetrics.includes(metric)) {
// Ensure originalValue is a number before formatting
const numericValue = (typeof originalValue === 'number') ? originalValue : 0;
// Format to locale string with comma decimal separator for the input value
const localeValue = numericValue.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
cell.innerHTML = `<input type="text" class="form-control form-control-sm" value="${localeValue}">`;
}
});
const debouncedRecalc = debounce(handleInputChange, 200);
table.querySelectorAll(`td[data-month="${month}"] input`).forEach(input => {
input.addEventListener('input', debouncedRecalc);
});
const buttonCell = table.querySelector(`th button[data-month="${month}"]`).parentElement;
state.originalTableState['button'] = buttonCell.innerHTML;
buttonCell.innerHTML = `
<div class="btn-group btn-group-sm mt-1" role="group">
<button type="button" class="btn btn-success confirm-override">Confirm</button>
<button type="button" class="btn btn-danger cancel-override">Cancel</button>
</div>
`;
}
async function confirmOverride() {
try {
const overrideValues = {};
const editableMetrics = ['Opening Balance', 'Billings', 'WIP', 'Expenses', 'Cost', 'Payment'];
editableMetrics.forEach(metric => {
const input = table.querySelector(`td[data-month="${state.overrideMonth}"][data-metric="${metric}"] input`);
if (!input) {
throw new Error(`Could not find input for metric: ${metric} in month ${state.overrideMonth}`);
}
overrideValues[metric] = parseLocaleNumber(input.value) || 0;
});
const finalData = recalculateFinancials(state.overrideMonth, overrideValues);
const monthsToSave = months.slice(months.indexOf(state.overrideMonth));
const dataToSave = {};
monthsToSave.forEach(month => {
dataToSave[month] = {};
metrics.forEach(metric => {
const rawValue = finalData[metric][month];
let cleanValue = typeof rawValue === 'string' ? parseLocaleNumber(rawValue) : rawValue;
if (!isFinite(cleanValue)) {
console.warn(`Invalid number for ${metric} in ${month}. Resetting to 0. Original:`, rawValue);
cleanValue = 0;
}
dataToSave[month][metric] = cleanValue;
});
});
const payload = {
project_id: projectId,
overrides: Object.entries(dataToSave).map(([month, values]) => ({
month: month,
opening_balance: values['Opening Balance'],
payment: values.Payment,
wip: values.WIP,
expenses: values.Expenses,
cost: values.Cost,
nsr: values.NSR,
margin: values.Margin,
is_confirmed: month === state.overrideMonth ? 1 : 0
}))
};
const response = await fetch('save_override.php?t=' + new Date().getTime(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
let errorMsg = `HTTP error! status: ${response.status}`;
try {
const errorData = await response.json();
errorMsg = errorData.message || errorMsg;
} catch (e) {
// Not a JSON response
}
throw new Error(errorMsg);
}
const result = await response.json();
if (result.success) {
alert(result.message || 'Override confirmed successfully!');
window.location.reload();
} else {
throw new Error(result.message || 'Failed to save override.');
}
} catch (error) {
console.error('Error confirming override:', error);
alert(`Error: ${error.message}`);
}
}
function exitOverrideMode() {
metrics.forEach(metric => {
const cell = table.querySelector(`td[data-month="${state.overrideMonth}"][data-metric="${metric}"]`);
if (cell) {
cell.innerHTML = state.originalTableState[metric];
}
});
const buttonCell = table.querySelector(`th .btn-group`).parentElement;
buttonCell.innerHTML = state.originalTableState['button'];
updateTable(initialFinancialData);
state.isOverrideActive = false;
state.overrideMonth = null;
state.originalTableState = {};
}
table.addEventListener('click', (e) => {
if (e.target.classList.contains('override-btn')) {
enterOverrideMode(e.target.dataset.month);
} else if (e.target.classList.contains('confirm-override')) {
confirmOverride();
} else if (e.target.classList.contains('cancel-override')) {
exitOverrideMode();
}
});
});

View File

@ -1,81 +0,0 @@
<?php
// Simple migration script
require_once __DIR__ . '/config.php';
try {
$pdo = db();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create migrations table if it doesn't exist
$pdo->exec("CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
migration_name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// Get all migration files
$migrationFiles = glob(__DIR__ . '/migrations/*.sql');
// Get already run migrations
$stmt = $pdo->query("SELECT migration_name FROM migrations");
$runMigrations = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo '<html><head><title>Database Migrations</title>';
echo '<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">';
echo '<style>body { padding: 2rem; } .log { font-family: monospace; white-space: pre; } </style>';
echo '</head><body><div class="container">';
echo '<h1>Database Migrations</h1>';
$migrationsApplied = false;
foreach ($migrationFiles as $file) {
$migrationName = basename($file);
if (!in_array($migrationName, $runMigrations)) {
echo '<div class="alert alert-info log">Applying migration: ' . htmlspecialchars($migrationName) . '...</div>';
$sql = file_get_contents($file);
$statements = array_filter(array_map('trim', explode(';', $sql)));
$fileHasError = false;
foreach ($statements as $statement) {
if (empty($statement)) continue;
try {
$pdo->exec($statement);
} catch (PDOException $e) {
// 1060 is the specific error code for "Duplicate column name"
if (strpos($e->getMessage(), '1060') !== false) {
// It's a duplicate column error, we can ignore it.
echo '<div class="alert alert-warning log">Ignoring existing column in ' . htmlspecialchars($migrationName) . '.</div>';
} else {
// It's another error, so we should stop.
echo '<div class="alert alert-danger log">Error applying migration ' . htmlspecialchars($migrationName) . ': ' . htmlspecialchars($e->getMessage()) . '</div>';
$fileHasError = true;
break; // break from statements loop
}
}
}
if (!$fileHasError) {
// Record migration
$stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
$stmt->execute([$migrationName]);
echo '<div class="alert alert-success log">Successfully applied ' . htmlspecialchars($migrationName) . '</div>';
$migrationsApplied = true;
} else {
// Stop on error
break; // break from files loop
}
}
}
if (!$migrationsApplied) {
echo '<div class="alert alert-success log">Database is already up to date.</div>';
}
echo '<a href="/" class="btn btn-primary mt-3">Back to Home</a>';
echo '</div></body></html>';
} catch (PDOException $e) {
http_response_code(500);
die("Database connection failed: " . $e->getMessage());
}

View File

@ -1 +1 @@
ALTER TABLE `forecastAllocation` DROP INDEX `unique_allocation`, ADD UNIQUE KEY `unique_allocation` (`forecastingId`, `rosterId`, `month`);
ALTER TABLE `forecastAllocation` ADD UNIQUE KEY `unique_allocation` (`forecastingId`, `rosterId`, `month`);

View File

@ -1,19 +1,11 @@
DROP TABLE IF EXISTS `projectFinanceMonthly`;
CREATE TABLE IF NOT EXISTS `projectFinanceMonthly` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`projectId` INT NOT NULL,
`metricName` VARCHAR(255) NOT NULL,
`month` DATE NOT NULL,
`opening_balance` DECIMAL(15, 2) DEFAULT 0,
`payment` DECIMAL(15, 2) DEFAULT 0,
`wip` DECIMAL(15, 2) DEFAULT 0,
`expenses` DECIMAL(15, 2) DEFAULT 0,
`cost` DECIMAL(15, 2) DEFAULT 0,
`nsr` DECIMAL(15, 2) DEFAULT 0,
`margin` DECIMAL(15, 5) DEFAULT 0,
`is_confirmed` TINYINT(1) NOT NULL DEFAULT 0,
`amount` DECIMAL(15, 2) NOT NULL,
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
UNIQUE KEY `unique_project_month` (`projectId`, `month`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
UNIQUE KEY `project_metric_month` (`projectId`, `metricName`, `month`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -1,2 +1,3 @@
ALTER TABLE `roster` ADD COLUMN `grossRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
ALTER TABLE `roster` ADD COLUMN `discountedRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
ALTER TABLE `roster`
ADD COLUMN `grossRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
ADD COLUMN `discountedRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;

View File

@ -1,10 +0,0 @@
CREATE TABLE IF NOT EXISTS `project_finance_monthly_override` (
`project_id` INT NOT NULL,
`nsr_override` DECIMAL(15, 2) DEFAULT NULL,
`cost_override` DECIMAL(15, 2) DEFAULT NULL,
`hours_override` DECIMAL(10, 2) DEFAULT NULL,
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`project_id`),
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -1,14 +0,0 @@
-- Step 1: Drop the foreign key constraint
ALTER TABLE project_finance_monthly_override DROP FOREIGN KEY project_finance_monthly_override_ibfk_1;
-- Step 2: Drop the old primary key
ALTER TABLE project_finance_monthly_override DROP PRIMARY KEY;
-- Step 3: Add the new month column
ALTER TABLE project_finance_monthly_override ADD COLUMN month VARCHAR(7) NOT NULL;
-- Step 4: Add the new composite primary key
ALTER TABLE project_finance_monthly_override ADD PRIMARY KEY (project_id, month);
-- Step 5: Re-add the foreign key constraint with a more descriptive name
ALTER TABLE project_finance_monthly_override ADD CONSTRAINT fk_override_project_id FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -1,12 +0,0 @@
<?php
require_once 'db/config.php';
try {
$pdo = db();
$stmt = $pdo->query("DESCRIBE project_finance_monthly_override");
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($columns);
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
?>

View File

@ -143,7 +143,7 @@ $months_headers = $project ? get_months($project['startDate'], $project['endDate
<div class="main-wrapper">
<nav class="sidebar">
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="roster.php"><i class="bi bi-people-fill me-2"></i>Roster</a></li>
<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>

View File

@ -4,7 +4,7 @@ session_start();
require_once __DIR__ . '/db/config.php';
function redirect_with_message($status, $message) {
header("Location: roster.php?import_status=$status&import_message=" . urlencode($message));
header("Location: index.php?import_status=$status&import_message=" . urlencode($message));
exit();
}

663
index.php
View File

@ -1,32 +1,277 @@
<?php
// --- FORM PROCESSING ---
$form_error = null;
$form_success = null;
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);
$grossRevenue = (float)($_POST['grossRevenue'] ?? 0);
$discountedRevenue = (float)($_POST['discountedRevenue'] ?? 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, grossRevenue, discountedRevenue) VALUES (:sapCode, :fullNameEn, :legalEntity, :functionBusinessUnit, :costCenterCode, :level, :newAmendedSalary, :employerContributions, :cars, :ticketRestaurant, :metlife, :topusPerMonth, :totalSalaryCostWithLabor, :totalMonthlyCost, :totalAnnualCost, :grossRevenue, :discountedRevenue)";
$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,
':grossRevenue' => $grossRevenue,
':discountedRevenue' => $discountedRevenue
]);
// 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);
$grossRevenue = (float)($_POST['grossRevenue'] ?? 0);
$discountedRevenue = (float)($_POST['discountedRevenue'] ?? 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,
grossRevenue = :grossRevenue,
discountedRevenue = :discountedRevenue
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,
':grossRevenue' => $grossRevenue,
':discountedRevenue' => $discountedRevenue
]);
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();
// Apply all migrations
$migration_files = glob(__DIR__ . '/db/migrations/*.sql');
sort($migration_files);
foreach ($migration_files as $file) {
$sql = file_get_contents($file);
$pdo->exec($sql);
execute_sql_from_file($pdo, $file);
}
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) {
// If the table already exists, ignore the error
if (strpos($e->getMessage(), 'already exists') === false) {
die("Database migration failed: " . $e->getMessage());
}
$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>Project Financial Management</title>
<meta name="description" content="Project Financial Management Tool">
<meta property="og:title" content="Project Financial Management">
<meta property="og:description" content="Manage your project financials, roster, and budget.">
<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">
@ -36,30 +281,416 @@ try {
</head>
<body>
<div class="top-navbar">
Project Financial Management
Project Financials
</div>
<div class="main-wrapper">
<nav class="sidebar">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="roster.php"><i class="bi bi-people-fill me-2"></i>Roster</a>
<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">Welcome to Project Financial Management</h1>
<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-body">
<p>This is the landing page. You can navigate to the Roster or Projects page using the sidebar.</p>
<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>Gross Revenue</th>
<th>Discounted Revenue</th>
<th>Daily 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>&euro;<?php echo number_format($row['grossRevenue'], 2); ?></td>
<td>&euro;<?php echo number_format($row['discountedRevenue'], 2); ?></td>
<td>&euro;<?php echo number_format($row['totalMonthlyCost'] / 20, 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 class="col-md-6 mb-3">
<label for="grossRevenue" class="form-label">Gross Revenue (&euro;)</label>
<input type="number" step="0.01" class="form-control" id="grossRevenue" name="grossRevenue" value="0">
</div>
<div class="col-md-6 mb-3">
<label for="discountedRevenue" class="form-label">Discounted Revenue (&euro;)</label>
<input type="number" step="0.01" class="form-control" id="discountedRevenue" name="discountedRevenue" 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 class="col-md-6 mb-3">
<label for="edit-grossRevenue" class="form-label">Gross Revenue (&euro;)</label>
<input type="number" step="0.01" class="form-control" id="edit-grossRevenue" name="grossRevenue">
</div>
<div class="col-md-6 mb-3">
<label for="edit-discountedRevenue" class="form-label">Discounted Revenue (&euro;)</label>
<input type="number" step="0.01" class="form-control" id="edit-discountedRevenue" name="discountedRevenue">
</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 class="col-md-6 mb-3">
<label class="form-label">Gross Revenue (&euro;)</label>
<input type="text" class="form-control" id="view-grossRevenue" readonly>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Discounted Revenue (&euro;)</label>
<input type="text" class="form-control" id="view-discountedRevenue" readonly>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Daily Cost (&euro;)</label>
<input type="text" class="form-control" id="view-dailyCost" 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>
</html>

View File

@ -1,20 +0,0 @@
[26-Nov-2025 13:29:03 UTC] save_override.php: Script start
[26-Nov-2025 13:29:03 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:08 UTC] save_override.php: Script start
[26-Nov-2025 13:29:08 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:13 UTC] save_override.php: Script start
[26-Nov-2025 13:29:13 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:19 UTC] save_override.php: Script start
[26-Nov-2025 13:29:19 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:24 UTC] save_override.php: Script start
[26-Nov-2025 13:29:24 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:29 UTC] save_override.php: Script start
[26-Nov-2025 13:29:29 UTC] save_override.php: Invalid request method
[26-Nov-2025 13:29:29 UTC] save_override.php: Script start
[26-Nov-2025 13:29:29 UTC] save_override.php: JSON data decoded
[26-Nov-2025 13:29:29 UTC] save_override.php: Database connection successful
[26-Nov-2025 13:29:29 UTC] save_override.php: Starting database transaction
[26-Nov-2025 13:29:29 UTC] save_override.php: SQL statement prepared
[26-Nov-2025 13:29:29 UTC] save_override.php: Processing month: 2025-11-01
[26-Nov-2025 13:29:29 UTC] save_override.php: An exception occurred: SQLSTATE[22001]: String data, right truncated: 1406 Data too long for column 'month' at row 1
[26-Nov-2025 13:29:29 UTC] save_override.php: Rolling back transaction

View File

@ -1,24 +0,0 @@
{"project_id":25,"overrides":[{"month":"2025-11-01","nsr":27855,"cost":1350},{"month":"2025-12-01","nsr":35545,"cost":2600},{"month":"2026-01-01","nsr":43355,"cost":3225}]}Executing with params: Array
(
[:project_id] => 25
[:month] => 2025-11-01
[:nsr_override] => 27855
[:cost_override] => 1350
[:hours_override] =>
)
Executing with params: Array
(
[:project_id] => 25
[:month] => 2025-12-01
[:nsr_override] => 35545
[:cost_override] => 2600
[:hours_override] =>
)
Executing with params: Array
(
[:project_id] => 25
[:month] => 2026-01-01
[:nsr_override] => 43355
[:cost_override] => 3225
[:hours_override] =>
)

View File

@ -10,7 +10,7 @@ function execute_sql_from_file($pdo, $filepath) {
return true;
} catch (PDOException $e) {
// Ignore errors about tables/columns that already exist
if (strpos($e->getMessage(), 'already exists') === false && strpos($e->getMessage(), 'Duplicate column name') === false && strpos($e->getMessage(), 'Duplicate key name') === false) {
if (strpos($e->getMessage(), 'already exists') === false && strpos($e->getMessage(), 'Duplicate column name') === false) {
error_log("SQL Execution Error in $filepath: " . $e->getMessage());
}
return false;
@ -35,7 +35,7 @@ $project = null;
$project_id = $_GET['id'] ?? null;
$financial_data = [];
$months = [];
$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin", "Payment"];
$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin"];
if ($project_id) {
$stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id");
@ -103,15 +103,6 @@ if ($project_id) {
$expenses_stmt->execute([':pid' => $project_id]);
$monthly_expenses = $expenses_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
// Fetch confirmed override data
$finance_override_stmt = $pdo->prepare("SELECT * FROM projectFinanceMonthly WHERE projectId = :pid ORDER BY month ASC");
$finance_override_stmt->execute([':pid' => $project_id]);
$overrides_raw = $finance_override_stmt->fetchAll(PDO::FETCH_ASSOC);
$finance_overrides = [];
foreach ($overrides_raw as $row) {
$finance_overrides[$row['month']] = $row;
}
// 2. Calculate cumulative values month by month
$cumulative_billing = 0;
$cumulative_cost = 0;
@ -119,54 +110,36 @@ if ($project_id) {
$previous_month_wip = 0;
foreach ($months as $month) {
if (isset($finance_overrides[$month])) {
// This month has saved data. Use its values.
$financial_data['Opening Balance'][$month] = $finance_overrides[$month]['opening_balance'];
$financial_data['Billings'][$month] = $finance_overrides[$month]['payment'];
$financial_data['Payment'][$month] = $finance_overrides[$month]['payment'];
$financial_data['WIP'][$month] = $finance_overrides[$month]['wip'];
$financial_data['Expenses'][$month] = $finance_overrides[$month]['expenses'];
$financial_data['Cost'][$month] = $finance_overrides[$month]['cost'];
$financial_data['NSR'][$month] = $finance_overrides[$month]['nsr'];
$financial_data['Margin'][$month] = $finance_overrides[$month]['margin'];
// Normalize month keys from fetched data
$cost = $monthly_costs[$month] ?? 0;
$base_monthly_wip = $monthly_wip[$month] ?? 0;
$billing = $monthly_billing[$month] ?? 0;
$expenses = $monthly_expenses[$month] ?? 0;
// Update cumulative trackers for the next potential calculation
$cumulative_billing = $financial_data['Billings'][$month];
$cumulative_cost = $financial_data['Cost'][$month];
$cumulative_expenses= $financial_data['Expenses'][$month];
$previous_month_wip = $financial_data['WIP'][$month];
} else {
// This month has no saved data. Calculate as usual.
$cost = $monthly_costs[$month] ?? 0;
$base_monthly_wip = $monthly_wip[$month] ?? 0;
$billing = $monthly_billing[$month] ?? 0;
$expenses = $monthly_expenses[$month] ?? 0;
// Cumulative calculations
$cumulative_billing += $billing;
$cumulative_cost += $cost;
$cumulative_expenses += $expenses;
// Cumulative calculations
$cumulative_billing += $billing;
$cumulative_cost += $cost;
$cumulative_expenses += $expenses;
// WIP Calculation (new formula)
// current month WIP = previous month WIP + Month Expenses + base_monthly_wip - month Billing
$current_wip = $previous_month_wip + $expenses + $base_monthly_wip - $billing;
// WIP Calculation
$current_wip = $previous_month_wip + $expenses + $base_monthly_wip - $billing;
$financial_data['WIP'][$month] = $current_wip;
$financial_data['Billings'][$month] = $cumulative_billing;
$financial_data['Cost'][$month] = $cumulative_cost;
$financial_data['Opening Balance'][$month] = 0; // Placeholder
$financial_data['Expenses'][$month] = $cumulative_expenses;
$financial_data['WIP'][$month] = $current_wip;
$financial_data['Billings'][$month] = $cumulative_billing;
$financial_data['Cost'][$month] = $cumulative_cost;
$financial_data['Opening Balance'][$month] = 0; // Placeholder
$financial_data['Expenses'][$month] = $cumulative_expenses;
$financial_data['Payment'][$month] = 0; // Not overridden, so no payment input
// Calculated metrics (NSR and Margin)
$nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
$financial_data['NSR'][$month] = $nsr;
// Calculated metrics (NSR and Margin)
$nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
$financial_data['NSR'][$month] = $nsr;
$margin = ($nsr != 0) ? (($nsr - $financial_data['Cost'][$month]) / $nsr) : 0;
$financial_data['Margin'][$month] = $margin;
$margin = ($nsr != 0) ? (($nsr - $financial_data['Cost'][$month]) / $nsr) : 0;
$financial_data['Margin'][$month] = $margin;
// Update previous month's WIP for the next iteration
$previous_month_wip = $current_wip;
}
// Update previous month's WIP for the next iteration
$previous_month_wip = $current_wip;
}
}
}
@ -198,7 +171,7 @@ if (!$project) {
<div class="main-wrapper">
<nav class="sidebar">
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="roster.php"><i class="bi bi-people-fill me-2"></i>Roster</a></li>
<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>
@ -259,45 +232,21 @@ if (!$project) {
</div>
</div>
<div class="table-responsive">
<?php
// Determine override eligibility
$override_eligible_month = null;
foreach ($months as $month) {
if (!isset($finance_overrides[$month]) || !$finance_overrides[$month]['is_confirmed']) {
$override_eligible_month = $month;
break;
}
}
?>
<table class="table table-bordered table-hover" id="financials-table">
<table class="table table-bordered table-hover">
<thead>
<tr class="text-center">
<th class="bg-body-tertiary" style="min-width: 150px;">Metric</th>
<?php foreach ($months as $month): ?>
<th>
<?php echo date('M Y', strtotime($month)); ?><br>
<?php
$is_confirmed = isset($finance_overrides[$month]) && $finance_overrides[$month]['is_confirmed'];
$is_eligible = ($month === $override_eligible_month);
if ($is_confirmed) {
echo '<span class="badge bg-success mt-1">Confirmed</span>';
} elseif ($is_eligible) {
echo '<button class="btn btn-sm btn-warning override-btn mt-1" data-month="' . $month . '">Override</button>';
} else {
echo '<button class="btn btn-sm btn-secondary mt-1" disabled>Override</button>';
}
?>
</th>
<th><?php echo date('M Y', strtotime($month)); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($metrics as $metric): ?>
<tr data-metric="<?php echo htmlspecialchars($metric); ?>">
<tr>
<td class="fw-bold bg-body-tertiary"><?php echo $metric; ?></td>
<?php foreach ($months as $month): ?>
<td class="text-end financial-metric" data-month="<?php echo $month; ?>" data-metric="<?php echo htmlspecialchars($metric); ?>">
<td class="text-end">
<?php
if ($metric === 'Margin') {
echo number_format(($financial_data[$metric][$month] ?? 0) * 100, 2) . '%';
@ -318,42 +267,6 @@ if (!$project) {
</main>
</div>
<script id="financial-data" type="application/json">
<?php
$project_id_js = $project['id'];
$finance_overrides_js = [];
foreach($finance_overrides as $month => $data) {
$finance_overrides_js[$month] = [
'is_confirmed' => $data['is_confirmed'],
'Opening Balance' => $data['opening_balance'],
'Billings' => $data['payment'],
'Payment' => $data['payment'],
'WIP' => $data['wip'],
'Expenses' => $data['expenses'],
'Cost' => $data['cost'],
'NSR' => $data['nsr'],
'Margin' => $data['margin']
];
}
$js_data = [
'projectId' => $project_id_js,
'months' => $months,
'metrics' => $metrics,
'initialFinancialData' => $financial_data,
'baseData' => [
'monthly_costs' => $monthly_costs,
'monthly_wip' => $monthly_wip,
'monthly_billing' => $monthly_billing,
'monthly_expenses' => $monthly_expenses,
],
'overrides' => $finance_overrides_js
];
echo json_encode($js_data, JSON_PRETTY_PRINT);
?>
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/project_details.js?v=<?php echo time(); ?>"></script>
</body>
</html>

View File

@ -150,7 +150,7 @@ try {
<nav class="sidebar">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="roster.php"><i class="bi bi-people-fill me-2"></i>Roster</a>
<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>

View File

@ -1,696 +0,0 @@
<?php
// --- FORM PROCESSING ---
$form_error = null;
$form_success = null;
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);
$grossRevenue = (float)($_POST['grossRevenue'] ?? 0);
$discountedRevenue = (float)($_POST['discountedRevenue'] ?? 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, grossRevenue, discountedRevenue) VALUES (:sapCode, :fullNameEn, :legalEntity, :functionBusinessUnit, :costCenterCode, :level, :newAmendedSalary, :employerContributions, :cars, :ticketRestaurant, :metlife, :topusPerMonth, :totalSalaryCostWithLabor, :totalMonthlyCost, :totalAnnualCost, :grossRevenue, :discountedRevenue)";
$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,
':grossRevenue' => $grossRevenue,
':discountedRevenue' => $discountedRevenue
]);
// 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);
$grossRevenue = (float)($_POST['grossRevenue'] ?? 0);
$discountedRevenue = (float)($_POST['discountedRevenue'] ?? 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,
grossRevenue = :grossRevenue,
discountedRevenue = :discountedRevenue
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,
':grossRevenue' => $grossRevenue,
':discountedRevenue' => $discountedRevenue
]);
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();
// Apply all migrations
$migration_files = glob(__DIR__ . '/db/migrations/*.sql');
sort($migration_files);
foreach ($migration_files as $file) {
execute_sql_from_file($pdo, $file);
}
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" data-bs-theme="dark">
<head>
<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>
<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 active" href="roster.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="roster.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="roster.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>Gross Revenue</th>
<th>Discounted Revenue</th>
<th>Daily 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>&euro;<?php echo number_format($row['grossRevenue'], 2); ?></td>
<td>&euro;<?php echo number_format($row['discountedRevenue'], 2); ?></td>
<td>&euro;<?php echo number_format($row['totalMonthlyCost'] / 20, 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="roster.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="roster.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 class="col-md-6 mb-3">
<label for="grossRevenue" class="form-label">Gross Revenue (&euro;)</label>
<input type="number" step="0.01" class="form-control" id="grossRevenue" name="grossRevenue" value="0">
</div>
<div class="col-md-6 mb-3">
<label for="discountedRevenue" class="form-label">Discounted Revenue (&euro;)</label>
<input type="number" step="0.01" class="form-control" id="discountedRevenue" name="discountedRevenue" 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="roster.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 class="col-md-6 mb-3">
<label for="edit-grossRevenue" class="form-label">Gross Revenue (&euro;)</label>
<input type="number" step="0.01" class="form-control" id="edit-grossRevenue" name="grossRevenue">
</div>
<div class="col-md-6 mb-3">
<label for="edit-discountedRevenue" class="form-label">Discounted Revenue (&euro;)</label>
<input type="number" step="0.01" class="form-control" id="edit-discountedRevenue" name="discountedRevenue">
</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 class="col-md-6 mb-3">
<label class="form-label">Gross Revenue (&euro;)</label>
<input type="text" class="form-control" id="view-grossRevenue" readonly>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Discounted Revenue (&euro;)</label>
<input type="text" class="form-control" id="view-discountedRevenue" readonly>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Daily Cost (&euro;)</label>
<input type="text" class="form-control" id="view-dailyCost" 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>

View File

@ -1,93 +0,0 @@
<?php
require_once 'db/config.php';
header('Content-Type: application/json');
$raw_input = file_get_contents('php://input');
$response = ['success' => false, 'message' => 'An error occurred.'];
$pdo = null;
try {
$pdo = db();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$data = json_decode($raw_input, true);
if (empty($data['project_id']) || empty($data['overrides'])) {
throw new Exception("Project ID or overrides data not provided.");
}
$project_id = $data['project_id'];
$overrides = $data['overrides'];
$rows_affected_total = 0;
$pdo->beginTransaction();
$sql = "
INSERT INTO projectFinanceMonthly (projectId, month, opening_balance, payment, wip, expenses, cost, nsr, margin, is_confirmed)
VALUES (:project_id, :month, :opening_balance, :payment, :wip, :expenses, :cost, :nsr, :margin, :is_confirmed)
ON DUPLICATE KEY UPDATE
opening_balance = VALUES(opening_balance),
payment = VALUES(payment),
wip = VALUES(wip),
expenses = VALUES(expenses),
cost = VALUES(cost),
nsr = VALUES(nsr),
margin = VALUES(margin),
is_confirmed = VALUES(is_confirmed)
";
$stmt = $pdo->prepare($sql);
foreach ($overrides as $override) {
$date = new DateTime($override['month']);
$month = $date->format('Y-m-d');
$params = [
':project_id' => $project_id,
':month' => $month,
':opening_balance' => $override['opening_balance'] ?? null,
':payment' => $override['payment'] ?? null,
':wip' => $override['wip'] ?? null,
':expenses' => $override['expenses'] ?? null,
':cost' => $override['cost'] ?? null,
':nsr' => $override['nsr'] ?? null,
':margin' => $override['margin'] ?? null,
':is_confirmed' => $override['is_confirmed'] ?? 0,
];
$stmt->execute($params);
$rows_affected_total += $stmt->rowCount();
}
if ($rows_affected_total === 0) {
// It's possible that the data is identical, so 0 affected rows is not strictly an error.
// We can consider it a "soft" success.
$pdo->rollBack(); // Rollback to not leave an open transaction
$response['success'] = true; // Report success to the frontend
$response['message'] = 'No changes were detected. Nothing was saved.';
echo json_encode($response);
exit;
}
$pdo->commit();
$response['success'] = true;
$response['message'] = 'Override data saved successfully. ' . $rows_affected_total . ' records were updated.';
} catch (PDOException $e) {
if ($pdo && $pdo->inTransaction()) {
$pdo->rollBack();
}
error_log("save_override.php: PDOException caught: " . $e->getMessage());
$response['message'] = "Database Error: " . $e->getMessage();
http_response_code(500);
} catch (Exception $e) {
if ($pdo && $pdo->inTransaction()) {
$pdo->rollBack();
}
error_log("save_override.php: Exception caught: " . $e->getMessage());
$response['message'] = "General Error: " . $e->getMessage();
http_response_code(500);
}
echo json_encode($response);
?>

View File

@ -1,27 +0,0 @@
<?php
$url = 'http://localhost/save_override.php';
$data = [
'project_id' => 25,
'overrides' => [
['month' => '2025-11-01', 'nsr' => 1000, 'cost' => 500],
['month' => '2025-12-01', 'nsr' => 2000, 'cost' => 1000],
]
];
$options = [
'http' => [
'header' => "Content-type: application/json\r\n",
'method' => 'POST',
'content' => json_encode($data),
],
];
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
if ($result === FALSE) {
echo "Error fetching URL";
}
var_dump($result);
?>

View File

@ -1,12 +0,0 @@
<?php
require_once 'db/config.php';
try {
$pdo = db();
$sql = "INSERT INTO project_finance_monthly_override (project_id, month, nsr_override, cost_override) VALUES (25, '2025-11-01', 1000, 500)";
$pdo->exec($sql);
echo "Insert successful.";
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
?>