276 lines
14 KiB
PHP
276 lines
14 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;
|
|
}
|
|
}
|
|
|
|
// Fetch Data
|
|
$search = $_GET['search'] ?? '';
|
|
$status_filter = $_GET['status'] ?? '';
|
|
$date_preset = $_GET['date_preset'] ?? '';
|
|
$start_from = $_GET['start_from'] ?? '';
|
|
$start_to = $_GET['start_to'] ?? '';
|
|
|
|
$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 ($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-outline-secondary" onclick="toggleFilters()"><i class="bi bi-funnel"></i> Filter</button>
|
|
<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; ?>
|
|
|
|
<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><?= htmlspecialchars($p['name']) ?></strong><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">
|
|
<button class="btn btn-sm btn-outline-secondary">Edit</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Sidebar (Absolute or slide-in) -->
|
|
<div class="filter-sidebar shadow-sm" id="projectFilterSidebar" style="display:none; position: absolute; top: 0; right: 0; z-index: 1050; width: 300px;">
|
|
<div class="card border-0">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<span class="fw-bold">FILTERS</span>
|
|
<button type="button" class="btn-close" onclick="toggleFilters()"></button>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="GET">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Search</label>
|
|
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($search) ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<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="mb-3">
|
|
<label class="form-label small fw-bold">Start Date</label>
|
|
<select name="date_preset" class="form-select form-select-sm" onchange="handleDatePreset(this.value)">
|
|
<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 Range...</option>
|
|
</select>
|
|
</div>
|
|
<div id="customDateRange" class="<?= $date_preset === 'custom' ? '' : 'd-none' ?>">
|
|
<div class="mb-2">
|
|
<label class="form-label extra-small text-muted">From</label>
|
|
<input type="date" name="start_from" class="form-control form-control-sm" value="<?= htmlspecialchars($start_from) ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label extra-small text-muted">To</label>
|
|
<input type="date" name="start_to" class="form-control form-control-sm" value="<?= htmlspecialchars($start_to) ?>">
|
|
</div>
|
|
</div>
|
|
<div class="d-grid gap-2">
|
|
<button type="submit" class="btn btn-sm btn-primary">Apply Filters</button>
|
|
<a href="projects.php" class="btn btn-sm btn-link text-decoration-none">Clear</a>
|
|
</div>
|
|
</form>
|
|
</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 toggleFilters() {
|
|
const sidebar = document.getElementById('projectFilterSidebar');
|
|
sidebar.style.display = sidebar.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
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'; ?>
|