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` (
|
CREATE TABLE IF NOT EXISTS `projectFinanceMonthly` (
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
`projectId` INT NOT NULL,
|
`projectId` INT NOT NULL,
|
||||||
|
`metricName` VARCHAR(255) NOT NULL,
|
||||||
`month` DATE NOT NULL,
|
`month` DATE NOT NULL,
|
||||||
`opening_balance` DECIMAL(15, 2) DEFAULT 0,
|
`amount` DECIMAL(15, 2) NOT NULL,
|
||||||
`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,
|
|
||||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
|
FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
|
||||||
UNIQUE KEY `unique_project_month` (`projectId`, `month`)
|
UNIQUE KEY `project_metric_month` (`projectId`, `metricName`, `month`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) 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`
|
||||||
ALTER TABLE `roster` ADD COLUMN `discountedRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
|
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">
|
<div class="main-wrapper">
|
||||||
<nav class="sidebar">
|
<nav class="sidebar">
|
||||||
<ul class="nav flex-column">
|
<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>
|
<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>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ session_start();
|
|||||||
require_once __DIR__ . '/db/config.php';
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
function redirect_with_message($status, $message) {
|
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();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
661
index.php
661
index.php
@ -1,32 +1,277 @@
|
|||||||
<?php
|
<?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';
|
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 {
|
try {
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
|
|
||||||
|
// Apply all migrations
|
||||||
$migration_files = glob(__DIR__ . '/db/migrations/*.sql');
|
$migration_files = glob(__DIR__ . '/db/migrations/*.sql');
|
||||||
sort($migration_files);
|
sort($migration_files);
|
||||||
foreach ($migration_files as $file) {
|
foreach ($migration_files as $file) {
|
||||||
$sql = file_get_contents($file);
|
execute_sql_from_file($pdo, $file);
|
||||||
$pdo->exec($sql);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seed_roster_data($pdo);
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM roster";
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if (!empty($search_term)) {
|
||||||
|
$sql .= " WHERE fullNameEn LIKE :search OR sapCode LIKE :search";
|
||||||
|
$params[':search'] = '%' . $search_term . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY fullNameEn";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$roster_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// If the table already exists, ignore the error
|
$db_error = "Database connection failed: " . $e->getMessage();
|
||||||
if (strpos($e->getMessage(), 'already exists') === false) {
|
|
||||||
die("Database migration failed: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- RENDER PAGE ---
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-bs-theme="dark">
|
<html lang="en" data-bs-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Project Financial Management</title>
|
<title>Project Financials</title>
|
||||||
<meta name="description" content="Project Financial Management Tool">
|
|
||||||
<meta property="og:title" content="Project Financial Management">
|
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'Project Financials Management Tool'); ?>">
|
||||||
<meta property="og:description" content="Manage your project financials, roster, and budget.">
|
<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 property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<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 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="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.googleapis.com">
|
||||||
@ -36,30 +281,416 @@ try {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="top-navbar">
|
<div class="top-navbar">
|
||||||
Project Financial Management
|
Project Financials
|
||||||
</div>
|
</div>
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<nav class="sidebar">
|
<nav class="sidebar">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<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>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="projects.php"><i class="bi bi-briefcase-fill me-2"></i>Projects</a>
|
<a class="nav-link" href="projects.php"><i class="bi bi-briefcase-fill me-2"></i>Projects</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="content-wrapper">
|
<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">
|
<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>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-header">
|
||||||
<p>This is the landing page. You can navigate to the Roster or Projects page using the sidebar.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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="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>
|
</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;
|
return true;
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
// Ignore errors about tables/columns that already exist
|
// 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());
|
error_log("SQL Execution Error in $filepath: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -35,7 +35,7 @@ $project = null;
|
|||||||
$project_id = $_GET['id'] ?? null;
|
$project_id = $_GET['id'] ?? null;
|
||||||
$financial_data = [];
|
$financial_data = [];
|
||||||
$months = [];
|
$months = [];
|
||||||
$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin", "Payment"];
|
$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin"];
|
||||||
|
|
||||||
if ($project_id) {
|
if ($project_id) {
|
||||||
$stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id");
|
$stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id");
|
||||||
@ -103,15 +103,6 @@ if ($project_id) {
|
|||||||
$expenses_stmt->execute([':pid' => $project_id]);
|
$expenses_stmt->execute([':pid' => $project_id]);
|
||||||
$monthly_expenses = $expenses_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
$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
|
// 2. Calculate cumulative values month by month
|
||||||
$cumulative_billing = 0;
|
$cumulative_billing = 0;
|
||||||
$cumulative_cost = 0;
|
$cumulative_cost = 0;
|
||||||
@ -119,24 +110,7 @@ if ($project_id) {
|
|||||||
$previous_month_wip = 0;
|
$previous_month_wip = 0;
|
||||||
|
|
||||||
foreach ($months as $month) {
|
foreach ($months as $month) {
|
||||||
if (isset($finance_overrides[$month])) {
|
// Normalize month keys from fetched data
|
||||||
// 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'];
|
|
||||||
|
|
||||||
// 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;
|
$cost = $monthly_costs[$month] ?? 0;
|
||||||
$base_monthly_wip = $monthly_wip[$month] ?? 0;
|
$base_monthly_wip = $monthly_wip[$month] ?? 0;
|
||||||
$billing = $monthly_billing[$month] ?? 0;
|
$billing = $monthly_billing[$month] ?? 0;
|
||||||
@ -147,7 +121,8 @@ if ($project_id) {
|
|||||||
$cumulative_cost += $cost;
|
$cumulative_cost += $cost;
|
||||||
$cumulative_expenses += $expenses;
|
$cumulative_expenses += $expenses;
|
||||||
|
|
||||||
// WIP Calculation
|
// 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;
|
$current_wip = $previous_month_wip + $expenses + $base_monthly_wip - $billing;
|
||||||
|
|
||||||
$financial_data['WIP'][$month] = $current_wip;
|
$financial_data['WIP'][$month] = $current_wip;
|
||||||
@ -155,7 +130,6 @@ if ($project_id) {
|
|||||||
$financial_data['Cost'][$month] = $cumulative_cost;
|
$financial_data['Cost'][$month] = $cumulative_cost;
|
||||||
$financial_data['Opening Balance'][$month] = 0; // Placeholder
|
$financial_data['Opening Balance'][$month] = 0; // Placeholder
|
||||||
$financial_data['Expenses'][$month] = $cumulative_expenses;
|
$financial_data['Expenses'][$month] = $cumulative_expenses;
|
||||||
$financial_data['Payment'][$month] = 0; // Not overridden, so no payment input
|
|
||||||
|
|
||||||
// Calculated metrics (NSR and Margin)
|
// Calculated metrics (NSR and Margin)
|
||||||
$nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
|
$nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
|
||||||
@ -168,7 +142,6 @@ if ($project_id) {
|
|||||||
$previous_month_wip = $current_wip;
|
$previous_month_wip = $current_wip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PAGE RENDER ---
|
// --- PAGE RENDER ---
|
||||||
@ -198,7 +171,7 @@ if (!$project) {
|
|||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<nav class="sidebar">
|
<nav class="sidebar">
|
||||||
<ul class="nav flex-column">
|
<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>
|
<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>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@ -259,45 +232,21 @@ if (!$project) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<?php
|
<table class="table table-bordered table-hover">
|
||||||
// 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">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-center">
|
<tr class="text-center">
|
||||||
<th class="bg-body-tertiary" style="min-width: 150px;">Metric</th>
|
<th class="bg-body-tertiary" style="min-width: 150px;">Metric</th>
|
||||||
<?php foreach ($months as $month): ?>
|
<?php foreach ($months as $month): ?>
|
||||||
<th>
|
<th><?php echo date('M Y', strtotime($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>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($metrics as $metric): ?>
|
<?php foreach ($metrics as $metric): ?>
|
||||||
<tr data-metric="<?php echo htmlspecialchars($metric); ?>">
|
<tr>
|
||||||
<td class="fw-bold bg-body-tertiary"><?php echo $metric; ?></td>
|
<td class="fw-bold bg-body-tertiary"><?php echo $metric; ?></td>
|
||||||
<?php foreach ($months as $month): ?>
|
<?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
|
<?php
|
||||||
if ($metric === 'Margin') {
|
if ($metric === 'Margin') {
|
||||||
echo number_format(($financial_data[$metric][$month] ?? 0) * 100, 2) . '%';
|
echo number_format(($financial_data[$metric][$month] ?? 0) * 100, 2) . '%';
|
||||||
@ -318,42 +267,6 @@ if (!$project) {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</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="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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -150,7 +150,7 @@ try {
|
|||||||
<nav class="sidebar">
|
<nav class="sidebar">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<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>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" href="projects.php"><i class="bi bi-briefcase-fill me-2"></i>Projects</a>
|
<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