Revert to version 73cf51a
This commit is contained in:
parent
89109f468e
commit
38ec03060f
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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());
|
||||
}
|
||||
@ -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`);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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();
|
||||
}
|
||||
?>
|
||||
@ -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>
|
||||
|
||||
@ -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
663
index.php
@ -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>€<?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>€<?php echo number_format($row['grossRevenue'], 2); ?></td>
|
||||
<td>€<?php echo number_format($row['discountedRevenue'], 2); ?></td>
|
||||
<td>€<?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 (€)</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 class="col-md-6 mb-3">
|
||||
<label for="grossRevenue" class="form-label">Gross Revenue (€)</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 (€)</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 (€)</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 class="col-md-6 mb-3">
|
||||
<label for="edit-grossRevenue" class="form-label">Gross Revenue (€)</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 (€)</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 (€)</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 class="col-md-6 mb-3">
|
||||
<label class="form-label">Gross Revenue (€)</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 (€)</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 (€)</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>
|
||||
|
||||
@ -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
|
||||
@ -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] =>
|
||||
)
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
696
roster.php
696
roster.php
@ -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>€<?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>€<?php echo number_format($row['grossRevenue'], 2); ?></td>
|
||||
<td>€<?php echo number_format($row['discountedRevenue'], 2); ?></td>
|
||||
<td>€<?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 (€)</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 class="col-md-6 mb-3">
|
||||
<label for="grossRevenue" class="form-label">Gross Revenue (€)</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 (€)</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 (€)</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 class="col-md-6 mb-3">
|
||||
<label for="edit-grossRevenue" class="form-label">Gross Revenue (€)</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 (€)</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 (€)</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 class="col-md-6 mb-3">
|
||||
<label class="form-label">Gross Revenue (€)</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 (€)</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 (€)</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>
|
||||
@ -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);
|
||||
?>
|
||||
@ -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);
|
||||
?>
|
||||
12
test_sql.php
12
test_sql.php
@ -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();
|
||||
}
|
||||
?>
|
||||
Loading…
x
Reference in New Issue
Block a user