diff --git a/assets/js/project_details.js b/assets/js/project_details.js new file mode 100644 index 0000000..7925843 --- /dev/null +++ b/assets/js/project_details.js @@ -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 = ``; + } + }); + + 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 = ` +
+ + +
+ `; + } + + 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(); + } + }); +}); \ No newline at end of file diff --git a/db/migrate.php b/db/migrate.php new file mode 100644 index 0000000..b3df702 --- /dev/null +++ b/db/migrate.php @@ -0,0 +1,81 @@ +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 'Database Migrations'; + echo ''; + echo ''; + echo '
'; + echo '

Database Migrations

'; + + $migrationsApplied = false; + + foreach ($migrationFiles as $file) { + $migrationName = basename($file); + if (!in_array($migrationName, $runMigrations)) { + echo '
Applying migration: ' . htmlspecialchars($migrationName) . '...
'; + $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 '
Ignoring existing column in ' . htmlspecialchars($migrationName) . '.
'; + } else { + // It's another error, so we should stop. + echo '
Error applying migration ' . htmlspecialchars($migrationName) . ': ' . htmlspecialchars($e->getMessage()) . '
'; + $fileHasError = true; + break; // break from statements loop + } + } + } + + if (!$fileHasError) { + // Record migration + $stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)"); + $stmt->execute([$migrationName]); + + echo '
Successfully applied ' . htmlspecialchars($migrationName) . '
'; + $migrationsApplied = true; + } else { + // Stop on error + break; // break from files loop + } + } + } + + if (!$migrationsApplied) { + echo '
Database is already up to date.
'; + } + + echo 'Back to Home'; + echo '
'; + +} catch (PDOException $e) { + http_response_code(500); + die("Database connection failed: " . $e->getMessage()); +} \ No newline at end of file diff --git a/db/migrations/005_create_project_finance_monthly_table.sql b/db/migrations/005_create_project_finance_monthly_table.sql index e0d9ee7..e8d9d00 100644 --- a/db/migrations/005_create_project_finance_monthly_table.sql +++ b/db/migrations/005_create_project_finance_monthly_table.sql @@ -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; \ No newline at end of file diff --git a/db/migrations/007_add_revenue_fields_to_roster.sql b/db/migrations/007_add_revenue_fields_to_roster.sql index 8590309..0143998 100644 --- a/db/migrations/007_add_revenue_fields_to_roster.sql +++ b/db/migrations/007_add_revenue_fields_to_roster.sql @@ -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; diff --git a/db/migrations/009_create_project_finance_monthly_override_table.sql b/db/migrations/009_create_project_finance_monthly_override_table.sql new file mode 100644 index 0000000..fdf98b6 --- /dev/null +++ b/db/migrations/009_create_project_finance_monthly_override_table.sql @@ -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; \ No newline at end of file diff --git a/db/migrations/010_alter_override_table.sql b/db/migrations/010_alter_override_table.sql new file mode 100644 index 0000000..5159dc3 --- /dev/null +++ b/db/migrations/010_alter_override_table.sql @@ -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; diff --git a/describe_table.php b/describe_table.php new file mode 100644 index 0000000..2fd72e0 --- /dev/null +++ b/describe_table.php @@ -0,0 +1,12 @@ +query("DESCRIBE project_finance_monthly_override"); + $columns = $stmt->fetchAll(PDO::FETCH_ASSOC); + print_r($columns); +} catch (Exception $e) { + echo "Error: " . $e->getMessage(); +} +?> \ No newline at end of file diff --git a/logs/php_errors.log b/logs/php_errors.log new file mode 100644 index 0000000..6fc7ef1 --- /dev/null +++ b/logs/php_errors.log @@ -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 diff --git a/logs/raw_input.log b/logs/raw_input.log new file mode 100644 index 0000000..00786fd --- /dev/null +++ b/logs/raw_input.log @@ -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] => +) diff --git a/project_details.php b/project_details.php index 08fc482..eb9463f 100644 --- a/project_details.php +++ b/project_details.php @@ -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) {
- + +
- + - + -
Metric +
+ Confirmed'; + } elseif ($is_eligible) { + echo ''; + } else { + echo ''; + } + ?> +
+ + + \ No newline at end of file diff --git a/save_override.php b/save_override.php new file mode 100644 index 0000000..678ee7f --- /dev/null +++ b/save_override.php @@ -0,0 +1,93 @@ + 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); +?> \ No newline at end of file diff --git a/test_save.php b/test_save.php new file mode 100644 index 0000000..92024ea --- /dev/null +++ b/test_save.php @@ -0,0 +1,27 @@ + 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); +?> diff --git a/test_sql.php b/test_sql.php new file mode 100644 index 0000000..53670ad --- /dev/null +++ b/test_sql.php @@ -0,0 +1,12 @@ +exec($sql); + echo "Insert successful."; +} catch (Exception $e) { + echo "Error: " . $e->getMessage(); +} +?> \ No newline at end of file