+WIP+NSR
This commit is contained in:
parent
00cb8e93bb
commit
8cc108c2d6
@ -32,6 +32,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
document.getElementById('edit-ticketRestaurant').value = data.ticketRestaurant;
|
||||
document.getElementById('edit-metlife').value = data.metlife;
|
||||
document.getElementById('edit-topusPerMonth').value = data.topusPerMonth;
|
||||
document.getElementById('edit-grossRevenue').value = data.grossRevenue;
|
||||
document.getElementById('edit-discountedRevenue').value = data.discountedRevenue;
|
||||
|
||||
editResourceModal.show();
|
||||
});
|
||||
@ -63,6 +65,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
document.getElementById('view-totalSalaryCostWithLabor').value = '€' + parseFloat(data.totalSalaryCostWithLabor).toFixed(2);
|
||||
document.getElementById('view-totalMonthlyCost').value = '€' + parseFloat(data.totalMonthlyCost).toFixed(2);
|
||||
document.getElementById('view-totalAnnualCost').value = '€' + parseFloat(data.totalAnnualCost).toFixed(2);
|
||||
document.getElementById('view-grossRevenue').value = '€' + parseFloat(data.grossRevenue).toFixed(2);
|
||||
document.getElementById('view-discountedRevenue').value = '€' + parseFloat(data.discountedRevenue).toFixed(2);
|
||||
|
||||
viewResourceModal.show();
|
||||
});
|
||||
|
||||
3
db/migrations/007_add_revenue_fields_to_roster.sql
Normal file
3
db/migrations/007_add_revenue_fields_to_roster.sql
Normal file
@ -0,0 +1,3 @@
|
||||
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;
|
||||
@ -36,34 +36,67 @@ try {
|
||||
}
|
||||
$months = get_month_range($project['startDate'], $project['endDate']);
|
||||
|
||||
foreach ($metrics as $metric) {
|
||||
foreach ($months as $month) {
|
||||
$financial_data[$metric][$month] = 0;
|
||||
}
|
||||
}
|
||||
// --- Financial Calculations ---
|
||||
|
||||
// 1. Fetch base monthly data
|
||||
$monthly_costs = [];
|
||||
$monthly_wip = [];
|
||||
|
||||
$forecast_stmt = $pdo->prepare("SELECT id FROM forecasting WHERE projectId = :pid ORDER BY versionNumber DESC LIMIT 1");
|
||||
$forecast_stmt->execute([':pid' => $project_id]);
|
||||
$latest_forecast = $forecast_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($latest_forecast) {
|
||||
$cost_sql = "
|
||||
SELECT fa.month, SUM(fa.allocatedDays * r.totalMonthlyCost) as totalCost
|
||||
FROM forecastAllocation fa
|
||||
JOIN roster r ON fa.rosterId = r.id
|
||||
WHERE fa.forecastingId = :fid
|
||||
GROUP BY fa.month
|
||||
";
|
||||
$fid = $latest_forecast['id'];
|
||||
|
||||
// Base Monthly Cost
|
||||
$cost_sql = "SELECT fa.month, SUM(fa.allocatedDays * r.totalMonthlyCost) FROM forecastAllocation fa JOIN roster r ON fa.rosterId = r.id WHERE fa.forecastingId = :fid GROUP BY fa.month";
|
||||
$cost_stmt = $pdo->prepare($cost_sql);
|
||||
$cost_stmt->execute([':fid' => $latest_forecast['id']]);
|
||||
$cost_stmt->execute([':fid' => $fid]);
|
||||
$monthly_costs = $cost_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
foreach ($monthly_costs as $month => $cost) {
|
||||
$formatted_month = date('Y-m-01', strtotime($month));
|
||||
if (isset($financial_data['Cost'][$formatted_month])) {
|
||||
$financial_data['Cost'][$formatted_month] = $cost;
|
||||
}
|
||||
}
|
||||
// Base Monthly WIP
|
||||
$wip_sql = "SELECT fa.month, SUM(fa.allocatedDays * r.discountedRevenue) FROM forecastAllocation fa JOIN roster r ON fa.rosterId = r.id WHERE fa.forecastingId = :fid GROUP BY fa.month";
|
||||
$wip_stmt = $pdo->prepare($wip_sql);
|
||||
$wip_stmt->execute([':fid' => $fid]);
|
||||
$monthly_wip = $wip_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
}
|
||||
|
||||
// Base Monthly Billings
|
||||
$billing_stmt = $pdo->prepare("SELECT month, amount FROM billingMonthly WHERE projectId = :pid");
|
||||
$billing_stmt->execute([':pid' => $project_id]);
|
||||
$monthly_billing = $billing_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
// 2. Calculate cumulative values month by month
|
||||
$cumulative_billing = 0;
|
||||
$cumulative_wip = 0;
|
||||
$cumulative_cost = 0;
|
||||
|
||||
foreach ($months as $month) {
|
||||
// Normalize month keys from fetched data
|
||||
$cost = $monthly_costs[$month] ?? 0;
|
||||
$wip = $monthly_wip[$month] ?? 0;
|
||||
$billing = $monthly_billing[$month] ?? 0;
|
||||
|
||||
// Cumulative calculations
|
||||
$cumulative_billing += $billing;
|
||||
$cumulative_wip += $wip;
|
||||
$cumulative_cost += $cost;
|
||||
|
||||
$financial_data['Billings'][$month] = $cumulative_billing;
|
||||
$financial_data['WIP'][$month] = $cumulative_wip;
|
||||
$financial_data['Cost'][$month] = $cumulative_cost;
|
||||
|
||||
// Other metrics are 0 for now
|
||||
$financial_data['Opening Balance'][$month] = 0;
|
||||
$financial_data['Expenses'][$month] = 0;
|
||||
|
||||
// Calculated metrics (NSR and Margin)
|
||||
$nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
|
||||
$financial_data['NSR'][$month] = $nsr;
|
||||
|
||||
$margin = ($nsr != 0) ? (($nsr - $financial_data['Cost'][$month]) / $nsr) : 0;
|
||||
$financial_data['Margin'][$month] = $margin;
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
|
||||
46
index.php
46
index.php
@ -19,13 +19,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
$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) VALUES (:sapCode, :fullNameEn, :legalEntity, :functionBusinessUnit, :costCenterCode, :level, :newAmendedSalary, :employerContributions, :cars, :ticketRestaurant, :metlife, :topusPerMonth, :totalSalaryCostWithLabor, :totalMonthlyCost, :totalAnnualCost)";
|
||||
$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([
|
||||
@ -43,7 +45,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
':topusPerMonth' => $topusPerMonth,
|
||||
':totalSalaryCostWithLabor' => $totalSalaryCostWithLabor,
|
||||
':totalMonthlyCost' => $totalMonthlyCost,
|
||||
':totalAnnualCost' => $totalAnnualCost
|
||||
':totalAnnualCost' => $totalAnnualCost,
|
||||
':grossRevenue' => $grossRevenue,
|
||||
':discountedRevenue' => $discountedRevenue
|
||||
]);
|
||||
|
||||
// To prevent form resubmission on refresh, redirect
|
||||
@ -90,6 +94,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
$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;
|
||||
@ -111,7 +117,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
topusPerMonth = :topusPerMonth,
|
||||
totalSalaryCostWithLabor = :totalSalaryCostWithLabor,
|
||||
totalMonthlyCost = :totalMonthlyCost,
|
||||
totalAnnualCost = :totalAnnualCost
|
||||
totalAnnualCost = :totalAnnualCost,
|
||||
grossRevenue = :grossRevenue,
|
||||
discountedRevenue = :discountedRevenue
|
||||
WHERE id = :id";
|
||||
|
||||
$stmt = $pdo_update->prepare($update_sql);
|
||||
@ -131,7 +139,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
':topusPerMonth' => $topusPerMonth,
|
||||
':totalSalaryCostWithLabor' => $totalSalaryCostWithLabor,
|
||||
':totalMonthlyCost' => $totalMonthlyCost,
|
||||
':totalAnnualCost' => $totalAnnualCost
|
||||
':totalAnnualCost' => $totalAnnualCost,
|
||||
':grossRevenue' => $grossRevenue,
|
||||
':discountedRevenue' => $discountedRevenue
|
||||
]);
|
||||
|
||||
header("Location: " . $_SERVER['PHP_SELF']);
|
||||
@ -344,6 +354,8 @@ try {
|
||||
<th>Total Salary Cost</th>
|
||||
<th>Total Monthly Cost</th>
|
||||
<th>Total Annual Cost</th>
|
||||
<th>Gross Revenue</th>
|
||||
<th>Discounted Revenue</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -370,6 +382,8 @@ try {
|
||||
<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>
|
||||
<div class="d-flex">
|
||||
<button class="btn btn-sm btn-outline-info me-2 view-btn"
|
||||
@ -457,6 +471,14 @@ try {
|
||||
<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">
|
||||
@ -532,6 +554,14 @@ try {
|
||||
<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">
|
||||
@ -613,6 +643,14 @@ try {
|
||||
<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>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
@ -67,33 +67,67 @@ if ($project_id) {
|
||||
}
|
||||
}
|
||||
|
||||
// Find the latest forecast version
|
||||
// --- Financial Calculations ---
|
||||
|
||||
// 1. Fetch base monthly data
|
||||
$monthly_costs = [];
|
||||
$monthly_wip = [];
|
||||
|
||||
$forecast_stmt = $pdo->prepare("SELECT id FROM forecasting WHERE projectId = :pid ORDER BY versionNumber DESC LIMIT 1");
|
||||
$forecast_stmt->execute([':pid' => $project_id]);
|
||||
$latest_forecast = $forecast_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($latest_forecast) {
|
||||
// Calculate monthly costs
|
||||
$cost_sql = "
|
||||
SELECT
|
||||
fa.month,
|
||||
SUM(fa.allocatedDays * r.totalMonthlyCost) as totalCost
|
||||
FROM forecastAllocation fa
|
||||
JOIN roster r ON fa.rosterId = r.id
|
||||
WHERE fa.forecastingId = :fid
|
||||
GROUP BY fa.month
|
||||
";
|
||||
$fid = $latest_forecast['id'];
|
||||
|
||||
// Base Monthly Cost
|
||||
$cost_sql = "SELECT fa.month, SUM(fa.allocatedDays * r.totalMonthlyCost) FROM forecastAllocation fa JOIN roster r ON fa.rosterId = r.id WHERE fa.forecastingId = :fid GROUP BY fa.month";
|
||||
$cost_stmt = $pdo->prepare($cost_sql);
|
||||
$cost_stmt->execute([':fid' => $latest_forecast['id']]);
|
||||
$cost_stmt->execute([':fid' => $fid]);
|
||||
$monthly_costs = $cost_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
foreach ($monthly_costs as $month => $cost) {
|
||||
// Ensure month format is consistent
|
||||
$formatted_month = date('Y-m-01', strtotime($month));
|
||||
if (isset($financial_data['Cost'][$formatted_month])) {
|
||||
$financial_data['Cost'][$formatted_month] = $cost;
|
||||
}
|
||||
}
|
||||
// Base Monthly WIP
|
||||
$wip_sql = "SELECT fa.month, SUM(fa.allocatedDays * r.discountedRevenue) FROM forecastAllocation fa JOIN roster r ON fa.rosterId = r.id WHERE fa.forecastingId = :fid GROUP BY fa.month";
|
||||
$wip_stmt = $pdo->prepare($wip_sql);
|
||||
$wip_stmt->execute([':fid' => $fid]);
|
||||
$monthly_wip = $wip_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
}
|
||||
|
||||
// Base Monthly Billings
|
||||
$billing_stmt = $pdo->prepare("SELECT month, amount FROM billingMonthly WHERE projectId = :pid");
|
||||
$billing_stmt->execute([':pid' => $project_id]);
|
||||
$monthly_billing = $billing_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
|
||||
// 2. Calculate cumulative values month by month
|
||||
$cumulative_billing = 0;
|
||||
$cumulative_wip = 0;
|
||||
$cumulative_cost = 0;
|
||||
|
||||
foreach ($months as $month) {
|
||||
// Normalize month keys from fetched data
|
||||
$cost = $monthly_costs[$month] ?? 0;
|
||||
$wip = $monthly_wip[$month] ?? 0;
|
||||
$billing = $monthly_billing[$month] ?? 0;
|
||||
|
||||
// Cumulative calculations
|
||||
$cumulative_billing += $billing;
|
||||
$cumulative_wip += $wip;
|
||||
$cumulative_cost += $cost;
|
||||
|
||||
$financial_data['Billings'][$month] = $cumulative_billing;
|
||||
$financial_data['WIP'][$month] = $cumulative_wip;
|
||||
$financial_data['Cost'][$month] = $cumulative_cost;
|
||||
|
||||
// Other metrics are 0 for now
|
||||
$financial_data['Opening Balance'][$month] = 0;
|
||||
$financial_data['Expenses'][$month] = 0;
|
||||
|
||||
// Calculated metrics (NSR and Margin)
|
||||
$nsr = $financial_data['WIP'][$month] + $financial_data['Billings'][$month] - $financial_data['Opening Balance'][$month] - $financial_data['Expenses'][$month];
|
||||
$financial_data['NSR'][$month] = $nsr;
|
||||
|
||||
$margin = ($nsr != 0) ? (($nsr - $financial_data['Cost'][$month]) / $nsr) : 0;
|
||||
$financial_data['Margin'][$month] = $margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,7 +234,13 @@ if (!$project) {
|
||||
<td class="fw-bold bg-body-tertiary"><?php echo $metric; ?></td>
|
||||
<?php foreach ($months as $month): ?>
|
||||
<td class="text-end">
|
||||
€<?php echo number_format($financial_data[$metric][$month] ?? 0, 2); ?>
|
||||
<?php
|
||||
if ($metric === 'Margin') {
|
||||
echo number_format(($financial_data[$metric][$month] ?? 0) * 100, 2) . '%';
|
||||
} else {
|
||||
echo '€' . number_format($financial_data[$metric][$month] ?? 0, 2);
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
</tr>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user