Final v2 (Expenses)
This commit is contained in:
parent
8be23f0e45
commit
73cf51aa26
30
assets/js/expenses.js
Normal file
30
assets/js/expenses.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
function initExpensesPage(projectId) {
|
||||||
|
document.querySelectorAll('.expenses-amount').forEach(input => {
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
const month = this.dataset.month;
|
||||||
|
const amount = this.value;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('projectId', projectId);
|
||||||
|
formData.append('month', month);
|
||||||
|
formData.append('amount', amount);
|
||||||
|
|
||||||
|
fetch('save_expenses.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Maybe show a small success indicator
|
||||||
|
} else {
|
||||||
|
alert('Error saving expenses amount: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('An unexpected error occurred.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
8
db/migrations/008_create_expenses_monthly_table.sql
Normal file
8
db/migrations/008_create_expenses_monthly_table.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `expensesMonthly` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`projectId` INT NOT NULL,
|
||||||
|
`month` DATE NOT NULL,
|
||||||
|
`amount` DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY `project_month` (`projectId`, `month`)
|
||||||
|
);
|
||||||
121
expenses.php
Normal file
121
expenses.php
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
$projectId = $_GET['id'] ?? null;
|
||||||
|
if (!$projectId) {
|
||||||
|
header("Location: projects.php");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
// I will execute the migration to create the expensesMonthly table
|
||||||
|
$migrationFile = __DIR__ . '/db/migrations/008_create_expenses_monthly_table.sql';
|
||||||
|
if (file_exists($migrationFile)) {
|
||||||
|
$sql = file_get_contents($migrationFile);
|
||||||
|
$pdo->exec($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id");
|
||||||
|
$stmt->execute([':id' => $projectId]);
|
||||||
|
$project = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$project) {
|
||||||
|
header("Location: projects.php");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize expenses records if they don't exist
|
||||||
|
$startDate = new DateTime($project['startDate']);
|
||||||
|
$endDate = new DateTime($project['endDate']);
|
||||||
|
$currentMonth = clone $startDate;
|
||||||
|
|
||||||
|
while ($currentMonth <= $endDate) {
|
||||||
|
$monthStr = $currentMonth->format('Y-m-01');
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) FROM expensesMonthly WHERE projectId = :projectId AND month = :month");
|
||||||
|
$stmt->execute([':projectId' => $projectId, ':month' => $monthStr]);
|
||||||
|
$count = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($count == 0) {
|
||||||
|
$insertStmt = $pdo->prepare("INSERT INTO expensesMonthly (projectId, month, amount) VALUES (:projectId, :month, 0)");
|
||||||
|
$insertStmt->execute([':projectId' => $projectId, ':month' => $monthStr]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentMonth->modify('+1 month');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch expenses data
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM expensesMonthly WHERE projectId = :projectId ORDER BY month");
|
||||||
|
$stmt->execute([':projectId' => $projectId]);
|
||||||
|
$expensesData = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$db_error = "Database error: " . $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!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>Expenses - <?php echo htmlspecialchars($project['name']); ?></title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<main class="content-wrapper">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="h2">Expenses - <?php echo htmlspecialchars($project['name']); ?></h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a href="project_details.php?id=<?php echo $projectId; ?>" class="btn btn-secondary">Return</a>
|
||||||
|
<a href="export_expenses.php?id=<?php echo $projectId; ?>" class="btn btn-secondary">Export to Excel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($db_error)): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo htmlspecialchars($db_error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 150px;">Expenses</th>
|
||||||
|
<?php
|
||||||
|
$currentMonth = clone $startDate;
|
||||||
|
while ($currentMonth <= $endDate) {
|
||||||
|
echo '<th>' . $currentMonth->format('M Y') . '</th>';
|
||||||
|
$currentMonth->modify('+1 month');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Expenses</td>
|
||||||
|
<?php
|
||||||
|
foreach ($expensesData as $expensesMonth) {
|
||||||
|
echo '<td><input type="number" class="form-control expenses-amount" data-month="' . $expensesMonth['month'] . '" value="' . htmlspecialchars($expensesMonth['amount']) . '"></td>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="assets/js/expenses.js?v=<?php echo time(); ?>"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const projectId = <?php echo json_encode($projectId); ?>;
|
||||||
|
initExpensesPage(projectId);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
46
export_expenses.php
Normal file
46
export_expenses.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
$projectId = $_GET['id'] ?? null;
|
||||||
|
if (!$projectId) {
|
||||||
|
die("Project ID is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT name FROM projects WHERE id = :id");
|
||||||
|
$stmt->execute([':id' => $projectId]);
|
||||||
|
$project = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$projectName = $project ? $project['name'] : 'project';
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT month, amount FROM expensesMonthly WHERE projectId = :projectId ORDER BY month");
|
||||||
|
$stmt->execute([':projectId' => $projectId]);
|
||||||
|
$expensesData = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$filename = "expenses_" . strtolower(str_replace(' ', '_', $projectName)) . ".csv";
|
||||||
|
|
||||||
|
header('Content-Type: text/csv');
|
||||||
|
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||||
|
|
||||||
|
$output = fopen('php://output', 'w');
|
||||||
|
|
||||||
|
// Header row
|
||||||
|
$header = ['Expenses'];
|
||||||
|
foreach ($expensesData as $row) {
|
||||||
|
$header[] = date("M Y", strtotime($row['month']));
|
||||||
|
}
|
||||||
|
fputcsv($output, $header);
|
||||||
|
|
||||||
|
// Data row
|
||||||
|
$data = ['Expenses'];
|
||||||
|
foreach ($expensesData as $row) {
|
||||||
|
$data[] = $row['amount'];
|
||||||
|
}
|
||||||
|
fputcsv($output, $data);
|
||||||
|
|
||||||
|
fclose($output);
|
||||||
|
exit();
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Database error: " . $e->getMessage());
|
||||||
|
}
|
||||||
@ -98,9 +98,15 @@ if ($project_id) {
|
|||||||
$billing_stmt->execute([':pid' => $project_id]);
|
$billing_stmt->execute([':pid' => $project_id]);
|
||||||
$monthly_billing = $billing_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
$monthly_billing = $billing_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
|
// Base Monthly Expenses
|
||||||
|
$expenses_stmt = $pdo->prepare("SELECT month, amount FROM expensesMonthly WHERE projectId = :pid");
|
||||||
|
$expenses_stmt->execute([':pid' => $project_id]);
|
||||||
|
$monthly_expenses = $expenses_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
|
||||||
// 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;
|
||||||
|
$cumulative_expenses = 0;
|
||||||
$previous_month_wip = 0;
|
$previous_month_wip = 0;
|
||||||
|
|
||||||
foreach ($months as $month) {
|
foreach ($months as $month) {
|
||||||
@ -108,11 +114,12 @@ if ($project_id) {
|
|||||||
$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;
|
||||||
$expenses = 0; // Placeholder for expenses
|
$expenses = $monthly_expenses[$month] ?? 0;
|
||||||
|
|
||||||
// Cumulative calculations
|
// Cumulative calculations
|
||||||
$cumulative_billing += $billing;
|
$cumulative_billing += $billing;
|
||||||
$cumulative_cost += $cost;
|
$cumulative_cost += $cost;
|
||||||
|
$cumulative_expenses += $expenses;
|
||||||
|
|
||||||
// WIP Calculation (new formula)
|
// WIP Calculation (new formula)
|
||||||
// current month WIP = previous month WIP + Month Expenses + base_monthly_wip - month Billing
|
// current month WIP = previous month WIP + Month Expenses + base_monthly_wip - month Billing
|
||||||
@ -122,7 +129,7 @@ if ($project_id) {
|
|||||||
$financial_data['Billings'][$month] = $cumulative_billing;
|
$financial_data['Billings'][$month] = $cumulative_billing;
|
||||||
$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] = $expenses;
|
$financial_data['Expenses'][$month] = $cumulative_expenses;
|
||||||
|
|
||||||
// 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];
|
||||||
@ -177,6 +184,7 @@ if (!$project) {
|
|||||||
<h1 class="h2">Project: <?php echo htmlspecialchars($project['name']); ?></h1>
|
<h1 class="h2">Project: <?php echo htmlspecialchars($project['name']); ?></h1>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<a href="billing.php?id=<?php echo $project['id']; ?>" class="btn btn-primary"><i class="bi bi-credit-card-fill me-2"></i>Billing</a>
|
<a href="billing.php?id=<?php echo $project['id']; ?>" class="btn btn-primary"><i class="bi bi-credit-card-fill me-2"></i>Billing</a>
|
||||||
|
<a href="expenses.php?id=<?php echo $project['id']; ?>" class="btn btn-danger me-2"><i class="bi bi-wallet-fill me-2"></i>Expenses</a>
|
||||||
<a href="forecasting.php?projectId=<?php echo $project['id']; ?>" class="btn btn-success"><i class="bi bi-bar-chart-line-fill me-2"></i>Forecasting</a>
|
<a href="forecasting.php?projectId=<?php echo $project['id']; ?>" class="btn btn-success"><i class="bi bi-bar-chart-line-fill me-2"></i>Forecasting</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
36
save_expenses.php
Normal file
36
save_expenses.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
$response = ['success' => false, 'error' => 'Invalid request'];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$projectId = $_POST['projectId'] ?? null;
|
||||||
|
$month = $_POST['month'] ?? null;
|
||||||
|
$amount = $_POST['amount'] ?? null;
|
||||||
|
|
||||||
|
if ($projectId && $month && $amount !== null) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("UPDATE expensesMonthly SET amount = :amount WHERE projectId = :projectId AND month = :month");
|
||||||
|
$stmt->execute([
|
||||||
|
':amount' => $amount,
|
||||||
|
':projectId' => $projectId,
|
||||||
|
':month' => $month
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($stmt->rowCount() > 0) {
|
||||||
|
$response['success'] = true;
|
||||||
|
unset($response['error']);
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'No record found to update.';
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$response['error'] = 'Database error: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Missing required fields.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
Loading…
x
Reference in New Issue
Block a user