38438-vm/projects.php
2026-02-15 15:54:39 +00:00

318 lines
17 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
$tenant_id = 1;
// Handle Add Project
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_project'])) {
$name = $_POST['name'] ?? '';
$code = $_POST['code'] ?? '';
$start_date = $_POST['start_date'] ?? date('Y-m-d');
$owner_id = !empty($_POST['owner_id']) ? (int)$_POST['owner_id'] : null;
$est_completion = !empty($_POST['estimated_completion_date']) ? $_POST['estimated_completion_date'] : null;
$type = $_POST['type'] ?? 'Internal';
$est_hours = (float)($_POST['estimated_hours'] ?? 0);
if ($name && $code) {
$stmt = db()->prepare("INSERT INTO projects (tenant_id, name, code, start_date, owner_id, estimated_completion_date, type, estimated_hours) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$tenant_id, $name, $code, $start_date, $owner_id, $est_completion, $type, $est_hours]);
$stmt = db()->prepare("INSERT INTO activity_log (tenant_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([$tenant_id, 'Project Created', "Added project: $name ($code)"]);
header("Location: projects.php?success=1");
exit;
}
}
// Handle Archive/Unarchive
if (isset($_GET['archive'])) {
$id = (int)$_GET['archive'];
$stmt = db()->prepare("UPDATE projects SET is_archived = 1 WHERE id = ? AND tenant_id = ?");
$stmt->execute([$id, $tenant_id]);
header("Location: projects.php?archived=1");
exit;
}
if (isset($_GET['unarchive'])) {
$id = (int)$_GET['unarchive'];
$stmt = db()->prepare("UPDATE projects SET is_archived = 0 WHERE id = ? AND tenant_id = ?");
$stmt->execute([$id, $tenant_id]);
header("Location: projects.php?unarchived=1");
exit;
}
// Fetch Data
$search = $_GET['search'] ?? '';
$status_filter = $_GET['status'] ?? '';
$date_preset = $_GET['date_preset'] ?? '';
$start_from = $_GET['start_from'] ?? '';
$start_to = $_GET['start_to'] ?? '';
$include_archived = isset($_GET['include_archived']) && $_GET['include_archived'] === '1';
$query = "
SELECT p.*,
CONCAT(e.first_name, ' ', e.last_name) as owner_name,
COALESCE((SELECT SUM(hours) FROM labour_entries WHERE project_id = p.id), 0) as total_hours
FROM projects p
LEFT JOIN employees e ON p.owner_id = e.id
WHERE p.tenant_id = ?";
$params = [$tenant_id];
if (!$include_archived) {
$query .= " AND p.is_archived = 0";
}
if ($search) {
$query .= " AND (p.name LIKE ? OR p.code LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
}
if ($status_filter) {
$query .= " AND p.status = ?";
$params[] = $status_filter;
}
if ($date_preset && $date_preset !== 'custom') {
switch ($date_preset) {
case 'today': $query .= " AND p.start_date = CURRENT_DATE"; break;
case 'this_week': $query .= " AND p.start_date >= DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) DAY)"; break;
case 'last_week': $query .= " AND p.start_date >= DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) + 7 DAY) AND p.start_date < DATE_SUB(CURRENT_DATE, INTERVAL WEEKDAY(CURRENT_DATE) DAY)"; break;
case 'this_month': $query .= " AND p.start_date >= DATE_FORMAT(CURRENT_DATE, '%Y-%m-01')"; break;
case 'last_month': $query .= " AND p.start_date >= DATE_FORMAT(DATE_SUB(CURRENT_DATE, INTERVAL 1 MONTH), '%Y-%m-01') AND p.start_date < DATE_FORMAT(CURRENT_DATE, '%Y-%m-01')"; break;
case 'this_year': $query .= " AND p.start_date >= DATE_FORMAT(CURRENT_DATE, '%Y-01-01')"; break;
case 'last_year': $query .= " AND p.start_date >= DATE_FORMAT(DATE_SUB(CURRENT_DATE, INTERVAL 1 YEAR), '%Y-01-01') AND p.start_date < DATE_FORMAT(CURRENT_DATE, '%Y-01-01')"; break;
}
} elseif ($date_preset === 'custom' && $start_from && $start_to) {
$query .= " AND p.start_date BETWEEN ? AND ?";
$params[] = $start_from;
$params[] = $start_to;
}
$query .= " ORDER BY p.created_at DESC";
$projects = db()->prepare($query);
$projects->execute($params);
$projectList = $projects->fetchAll();
$employees = db()->prepare("SELECT id, first_name, last_name FROM employees WHERE tenant_id = ? ORDER BY first_name, last_name");
$employees->execute([$tenant_id]);
$employeeList = $employees->fetchAll();
$pageTitle = "SR&ED Manager - Projects";
include __DIR__ . '/includes/header.php';
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Projects</h2>
<div class="d-flex gap-2">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProjectModal">+ New Project</button>
</div>
</div>
<?php if (isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
Project successfully created.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['archived'])): ?>
<div class="alert alert-info alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
Project successfully archived.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if (isset($_GET['unarchived'])): ?>
<div class="alert alert-success alert-dismissible fade show border-0 shadow-sm mb-4" role="alert">
Project successfully unarchived.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-bold">Search</label>
<div class="input-group input-group-sm">
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control" placeholder="Project name or code..." value="<?= htmlspecialchars($search) ?>">
</div>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold">Status</label>
<select name="status" class="form-select form-select-sm">
<option value="">All Statuses</option>
<option value="active" <?= $status_filter === 'active' ? 'selected' : '' ?>>Active</option>
<option value="on_hold" <?= $status_filter === 'on_hold' ? 'selected' : '' ?>>On Hold</option>
<option value="completed" <?= $status_filter === 'completed' ? 'selected' : '' ?>>Completed</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold">Start Date</label>
<div class="d-flex gap-2">
<select name="date_preset" class="form-select form-select-sm" onchange="handleDatePreset(this.value)" style="width: 140px;">
<option value="">Any Time</option>
<option value="this_month" <?= $date_preset === 'this_month' ? 'selected' : '' ?>>This Month</option>
<option value="this_year" <?= $date_preset === 'this_year' ? 'selected' : '' ?>>This Year</option>
<option value="custom" <?= $date_preset === 'custom' ? 'selected' : '' ?>>Custom...</option>
</select>
<div id="customDateRange" class="d-flex gap-1 <?= $date_preset === 'custom' ? '' : 'd-none' ?>">
<input type="date" name="start_from" class="form-control form-control-sm" value="<?= htmlspecialchars($start_from) ?>" placeholder="From">
<input type="date" name="start_to" class="form-control form-control-sm" value="<?= htmlspecialchars($start_to) ?>" placeholder="To">
</div>
</div>
</div>
<div class="col-md-2">
<div class="form-check form-switch mb-1">
<input class="form-check-input" type="checkbox" name="include_archived" id="includeArchived" value="1" <?= $include_archived ? 'checked' : '' ?>>
<label class="form-check-label small fw-bold" for="includeArchived">Archived</label>
</div>
</div>
<div class="col-md-2 text-end">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-sm btn-primary flex-grow-1">Filter</button>
<a href="projects.php" class="btn btn-sm btn-outline-secondary">Reset</a>
</div>
</div>
</form>
</div>
</div>
<div class="row position-relative">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Project Name</th>
<th>Owner</th>
<th>Type</th>
<th>Hours (Logged/Est)</th>
<th>Variance</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($projectList)): ?>
<tr><td colspan="7" class="text-center py-5 text-muted">No projects found.</td></tr>
<?php endif; ?>
<?php foreach ($projectList as $p):
$total_hours = (float)$p['total_hours'];
$est_hours = (float)$p['estimated_hours'];
$variance = $est_hours - $total_hours;
$is_over = $total_hours > $est_hours && $est_hours > 0;
?>
<tr>
<td>
<strong><a href="project_detail.php?id=<?= $p['id'] ?>" class="text-decoration-none text-dark"><?= htmlspecialchars($p['name']) ?></a></strong>
<?php if ($p['is_archived']): ?>
<span class="badge bg-secondary extra-small ms-1">Archived</span>
<?php endif; ?><br>
<code class="extra-small text-primary"><?= htmlspecialchars($p['code']) ?></code>
</td>
<td><small><?= htmlspecialchars($p['owner_name'] ?: 'Unassigned') ?></small></td>
<td><span class="badge bg-light text-dark border"><?= $p['type'] ?></span></td>
<td>
<span class="fw-bold <?= $is_over ? 'text-danger' : '' ?>"><?= number_format($total_hours, 1) ?></span>
<span class="text-muted">/ <?= number_format($est_hours, 1) ?></span>
</td>
<td>
<span class="fw-bold <?= $is_over ? 'text-danger' : 'text-success' ?>">
<?= ($variance >= 0 ? '+' : '') . number_format($variance, 1) ?>
</span>
</td>
<td><span class="status-badge status-<?= str_replace('_', '-', $p['status']) ?>"><?= ucfirst(str_replace('_', ' ', $p['status'])) ?></span></td>
<td class="text-end">
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">Actions</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="project_detail.php?id=<?= $p['id'] ?>">View Details</a></li>
<li><hr class="dropdown-divider"></li>
<?php if ($p['is_archived']): ?>
<li><a class="dropdown-item" href="projects.php?unarchive=<?= $p['id'] ?>" onclick="return confirm('Unarchive this project?')">Unarchive</a></li>
<?php else: ?>
<li><a class="dropdown-item text-danger" href="projects.php?archive=<?= $p['id'] ?>" onclick="return confirm('Are you sure you want to archive this project? Future hours and expenses will be limited.')">Archive</a></li>
<?php endif; ?>
</ul>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="addProjectModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title fw-bold">Add New Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="row">
<div class="col-md-8 mb-3">
<label class="form-label small fw-bold">Project Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold">Project Code</label>
<input type="text" name="code" class="form-control" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Project Owner</label>
<select name="owner_id" class="form-select">
<option value="">Select Owner...</option>
<?php foreach ($employeeList as $e): ?>
<option value="<?= $e['id'] ?>"><?= htmlspecialchars($e['first_name'] . ' ' . $e['last_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Project Type</label>
<select name="type" class="form-select">
<option value="Internal">Internal</option>
<option value="SRED">SR&ED</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Start Date</label>
<input type="date" name="start_date" class="form-control" value="<?= date('Y-m-d') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Estimated Hours</label>
<input type="number" name="estimated_hours" class="form-control" step="0.5" min="0" value="0">
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="submit" name="add_project" class="btn btn-primary px-4">Create Project</button>
</div>
</form>
</div>
</div>
</div>
<script>
function handleDatePreset(val) {
const custom = document.getElementById('customDateRange');
if (val === 'custom') {
custom.classList.remove('d-none');
} else {
custom.classList.add('d-none');
}
}
</script>
<?php include __DIR__ . '/includes/footer.php'; ?>