error - with override
This commit is contained in:
parent
4a8e08c54b
commit
36ef6e0316
295
assets/js/project_details.js
Normal file
295
assets/js/project_details.js
Normal file
@ -0,0 +1,295 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
81
db/migrate.php
Normal file
81
db/migrate.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?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,11 +1,19 @@
|
||||
DROP TABLE IF EXISTS `projectFinanceMonthly`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `projectFinanceMonthly` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`projectId` INT NOT NULL,
|
||||
`metricName` VARCHAR(255) NOT NULL,
|
||||
`month` DATE NOT NULL,
|
||||
`amount` DECIMAL(15, 2) NOT NULL,
|
||||
`opening_balance` DECIMAL(15, 2) DEFAULT 0,
|
||||
`payment` DECIMAL(15, 2) DEFAULT 0,
|
||||
`wip` DECIMAL(15, 2) DEFAULT 0,
|
||||
`expenses` DECIMAL(15, 2) DEFAULT 0,
|
||||
`cost` DECIMAL(15, 2) DEFAULT 0,
|
||||
`nsr` DECIMAL(15, 2) DEFAULT 0,
|
||||
`margin` DECIMAL(15, 5) DEFAULT 0,
|
||||
`is_confirmed` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updatedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
|
||||
UNIQUE KEY `project_metric_month` (`projectId`, `metricName`, `month`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
UNIQUE KEY `unique_project_month` (`projectId`, `month`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@ -1,3 +1,2 @@
|
||||
ALTER TABLE `roster`
|
||||
ADD COLUMN `grossRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||
ADD COLUMN `discountedRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
|
||||
ALTER TABLE `roster` ADD COLUMN `grossRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
|
||||
ALTER TABLE `roster` ADD COLUMN `discountedRevenue` DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
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;
|
||||
14
db/migrations/010_alter_override_table.sql
Normal file
14
db/migrations/010_alter_override_table.sql
Normal file
@ -0,0 +1,14 @@
|
||||
-- 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;
|
||||
12
describe_table.php
Normal file
12
describe_table.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?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();
|
||||
}
|
||||
?>
|
||||
20
logs/php_errors.log
Normal file
20
logs/php_errors.log
Normal file
@ -0,0 +1,20 @@
|
||||
[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
|
||||
24
logs/raw_input.log
Normal file
24
logs/raw_input.log
Normal file
@ -0,0 +1,24 @@
|
||||
{"project_id":25,"overrides":[{"month":"2025-11-01","nsr":27855,"cost":1350},{"month":"2025-12-01","nsr":35545,"cost":2600},{"month":"2026-01-01","nsr":43355,"cost":3225}]}Executing with params: Array
|
||||
(
|
||||
[:project_id] => 25
|
||||
[:month] => 2025-11-01
|
||||
[:nsr_override] => 27855
|
||||
[:cost_override] => 1350
|
||||
[:hours_override] =>
|
||||
)
|
||||
Executing with params: Array
|
||||
(
|
||||
[:project_id] => 25
|
||||
[:month] => 2025-12-01
|
||||
[:nsr_override] => 35545
|
||||
[:cost_override] => 2600
|
||||
[:hours_override] =>
|
||||
)
|
||||
Executing with params: Array
|
||||
(
|
||||
[:project_id] => 25
|
||||
[:month] => 2026-01-01
|
||||
[:nsr_override] => 43355
|
||||
[:cost_override] => 3225
|
||||
[:hours_override] =>
|
||||
)
|
||||
@ -10,7 +10,7 @@ function execute_sql_from_file($pdo, $filepath) {
|
||||
return true;
|
||||
} catch (PDOException $e) {
|
||||
// Ignore errors about tables/columns that already exist
|
||||
if (strpos($e->getMessage(), 'already exists') === false && strpos($e->getMessage(), 'Duplicate column name') === false) {
|
||||
if (strpos($e->getMessage(), 'already exists') === false && strpos($e->getMessage(), 'Duplicate column name') === false && strpos($e->getMessage(), 'Duplicate key name') === false) {
|
||||
error_log("SQL Execution Error in $filepath: " . $e->getMessage());
|
||||
}
|
||||
return false;
|
||||
@ -35,7 +35,7 @@ $project = null;
|
||||
$project_id = $_GET['id'] ?? null;
|
||||
$financial_data = [];
|
||||
$months = [];
|
||||
$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin"];
|
||||
$metrics = ["Opening Balance", "Billings", "WIP", "Expenses", "Cost", "NSR", "Margin", "Payment"];
|
||||
|
||||
if ($project_id) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id");
|
||||
@ -103,6 +103,15 @@ if ($project_id) {
|
||||
$expenses_stmt->execute([':pid' => $project_id]);
|
||||
$monthly_expenses = $expenses_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
// Fetch confirmed override data
|
||||
$finance_override_stmt = $pdo->prepare("SELECT * FROM projectFinanceMonthly WHERE projectId = :pid ORDER BY month ASC");
|
||||
$finance_override_stmt->execute([':pid' => $project_id]);
|
||||
$overrides_raw = $finance_override_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$finance_overrides = [];
|
||||
foreach ($overrides_raw as $row) {
|
||||
$finance_overrides[$row['month']] = $row;
|
||||
}
|
||||
|
||||
// 2. Calculate cumulative values month by month
|
||||
$cumulative_billing = 0;
|
||||
$cumulative_cost = 0;
|
||||
@ -110,36 +119,54 @@ if ($project_id) {
|
||||
$previous_month_wip = 0;
|
||||
|
||||
foreach ($months as $month) {
|
||||
// Normalize month keys from fetched data
|
||||
$cost = $monthly_costs[$month] ?? 0;
|
||||
$base_monthly_wip = $monthly_wip[$month] ?? 0;
|
||||
$billing = $monthly_billing[$month] ?? 0;
|
||||
$expenses = $monthly_expenses[$month] ?? 0;
|
||||
if (isset($finance_overrides[$month])) {
|
||||
// This month has saved data. Use its values.
|
||||
$financial_data['Opening Balance'][$month] = $finance_overrides[$month]['opening_balance'];
|
||||
$financial_data['Billings'][$month] = $finance_overrides[$month]['payment'];
|
||||
$financial_data['Payment'][$month] = $finance_overrides[$month]['payment'];
|
||||
$financial_data['WIP'][$month] = $finance_overrides[$month]['wip'];
|
||||
$financial_data['Expenses'][$month] = $finance_overrides[$month]['expenses'];
|
||||
$financial_data['Cost'][$month] = $finance_overrides[$month]['cost'];
|
||||
$financial_data['NSR'][$month] = $finance_overrides[$month]['nsr'];
|
||||
$financial_data['Margin'][$month] = $finance_overrides[$month]['margin'];
|
||||
|
||||
// Cumulative calculations
|
||||
$cumulative_billing += $billing;
|
||||
$cumulative_cost += $cost;
|
||||
$cumulative_expenses += $expenses;
|
||||
// Update cumulative trackers for the next potential calculation
|
||||
$cumulative_billing = $financial_data['Billings'][$month];
|
||||
$cumulative_cost = $financial_data['Cost'][$month];
|
||||
$cumulative_expenses= $financial_data['Expenses'][$month];
|
||||
$previous_month_wip = $financial_data['WIP'][$month];
|
||||
} else {
|
||||
// This month has no saved data. Calculate as usual.
|
||||
$cost = $monthly_costs[$month] ?? 0;
|
||||
$base_monthly_wip = $monthly_wip[$month] ?? 0;
|
||||
$billing = $monthly_billing[$month] ?? 0;
|
||||
$expenses = $monthly_expenses[$month] ?? 0;
|
||||
|
||||
// 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;
|
||||
// Cumulative calculations
|
||||
$cumulative_billing += $billing;
|
||||
$cumulative_cost += $cost;
|
||||
$cumulative_expenses += $expenses;
|
||||
|
||||
$financial_data['WIP'][$month] = $current_wip;
|
||||
$financial_data['Billings'][$month] = $cumulative_billing;
|
||||
$financial_data['Cost'][$month] = $cumulative_cost;
|
||||
$financial_data['Opening Balance'][$month] = 0; // Placeholder
|
||||
$financial_data['Expenses'][$month] = $cumulative_expenses;
|
||||
// WIP Calculation
|
||||
$current_wip = $previous_month_wip + $expenses + $base_monthly_wip - $billing;
|
||||
|
||||
// Calculated metrics (NSR and Margin)
|
||||
$nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
|
||||
$financial_data['NSR'][$month] = $nsr;
|
||||
$financial_data['WIP'][$month] = $current_wip;
|
||||
$financial_data['Billings'][$month] = $cumulative_billing;
|
||||
$financial_data['Cost'][$month] = $cumulative_cost;
|
||||
$financial_data['Opening Balance'][$month] = 0; // Placeholder
|
||||
$financial_data['Expenses'][$month] = $cumulative_expenses;
|
||||
$financial_data['Payment'][$month] = 0; // Not overridden, so no payment input
|
||||
|
||||
$margin = ($nsr != 0) ? (($nsr - $financial_data['Cost'][$month]) / $nsr) : 0;
|
||||
$financial_data['Margin'][$month] = $margin;
|
||||
// Calculated metrics (NSR and Margin)
|
||||
$nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
|
||||
$financial_data['NSR'][$month] = $nsr;
|
||||
|
||||
// Update previous month's WIP for the next iteration
|
||||
$previous_month_wip = $current_wip;
|
||||
$margin = ($nsr != 0) ? (($nsr - $financial_data['Cost'][$month]) / $nsr) : 0;
|
||||
$financial_data['Margin'][$month] = $margin;
|
||||
|
||||
// Update previous month's WIP for the next iteration
|
||||
$previous_month_wip = $current_wip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -232,21 +259,45 @@ if (!$project) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover">
|
||||
<?php
|
||||
// Determine override eligibility
|
||||
$override_eligible_month = null;
|
||||
foreach ($months as $month) {
|
||||
if (!isset($finance_overrides[$month]) || !$finance_overrides[$month]['is_confirmed']) {
|
||||
$override_eligible_month = $month;
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<table class="table table-bordered table-hover" id="financials-table">
|
||||
<thead>
|
||||
<tr class="text-center">
|
||||
<th class="bg-body-tertiary" style="min-width: 150px;">Metric</th>
|
||||
<?php foreach ($months as $month): ?>
|
||||
<th><?php echo date('M Y', strtotime($month)); ?></th>
|
||||
<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; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($metrics as $metric): ?>
|
||||
<tr>
|
||||
<tr data-metric="<?php echo htmlspecialchars($metric); ?>">
|
||||
<td class="fw-bold bg-body-tertiary"><?php echo $metric; ?></td>
|
||||
<?php foreach ($months as $month): ?>
|
||||
<td class="text-end">
|
||||
<td class="text-end financial-metric" data-month="<?php echo $month; ?>" data-metric="<?php echo htmlspecialchars($metric); ?>">
|
||||
<?php
|
||||
if ($metric === 'Margin') {
|
||||
echo number_format(($financial_data[$metric][$month] ?? 0) * 100, 2) . '%';
|
||||
@ -267,6 +318,42 @@ if (!$project) {
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script id="financial-data" type="application/json">
|
||||
<?php
|
||||
$project_id_js = $project['id'];
|
||||
|
||||
$finance_overrides_js = [];
|
||||
foreach($finance_overrides as $month => $data) {
|
||||
$finance_overrides_js[$month] = [
|
||||
'is_confirmed' => $data['is_confirmed'],
|
||||
'Opening Balance' => $data['opening_balance'],
|
||||
'Billings' => $data['payment'],
|
||||
'Payment' => $data['payment'],
|
||||
'WIP' => $data['wip'],
|
||||
'Expenses' => $data['expenses'],
|
||||
'Cost' => $data['cost'],
|
||||
'NSR' => $data['nsr'],
|
||||
'Margin' => $data['margin']
|
||||
];
|
||||
}
|
||||
|
||||
$js_data = [
|
||||
'projectId' => $project_id_js,
|
||||
'months' => $months,
|
||||
'metrics' => $metrics,
|
||||
'initialFinancialData' => $financial_data,
|
||||
'baseData' => [
|
||||
'monthly_costs' => $monthly_costs,
|
||||
'monthly_wip' => $monthly_wip,
|
||||
'monthly_billing' => $monthly_billing,
|
||||
'monthly_expenses' => $monthly_expenses,
|
||||
],
|
||||
'overrides' => $finance_overrides_js
|
||||
];
|
||||
echo json_encode($js_data, JSON_PRETTY_PRINT);
|
||||
?>
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/js/project_details.js?v=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
93
save_override.php
Normal file
93
save_override.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?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);
|
||||
?>
|
||||
27
test_save.php
Normal file
27
test_save.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?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
Normal file
12
test_sql.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?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