458 lines
28 KiB
PHP
458 lines
28 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
require_once __DIR__ . '/app.php';
|
||
|
||
ensure_request_table();
|
||
|
||
$flash = consume_flash();
|
||
$currentUser = current_user();
|
||
$loginError = null;
|
||
$formErrors = [];
|
||
$oldForm = [
|
||
'request_type' => 'Purchase',
|
||
'title' => '',
|
||
'amount' => '',
|
||
'priority' => 'Standard',
|
||
'needed_by' => '',
|
||
'justification' => '',
|
||
];
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$action = trim((string) ($_POST['action'] ?? ''));
|
||
|
||
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
||
flash('danger', 'Your session expired. Please try again.');
|
||
redirect('index.php');
|
||
}
|
||
|
||
if ($action === 'login') {
|
||
$candidate = authenticate_demo_user((string) ($_POST['email'] ?? ''), (string) ($_POST['password'] ?? ''));
|
||
if ($candidate) {
|
||
set_current_user($candidate);
|
||
flash('success', 'Signed in as ' . $candidate['name'] . '.');
|
||
redirect('index.php');
|
||
}
|
||
|
||
$loginError = 'Use one of the demo accounts below and the default password Twende2026.';
|
||
} elseif ($action === 'logout') {
|
||
logout_current_user();
|
||
flash('primary', 'Signed out successfully.');
|
||
redirect('index.php');
|
||
} elseif ($action === 'create_request') {
|
||
$currentUser = require_auth();
|
||
if (!can_submit_requests($currentUser)) {
|
||
flash('warning', 'Admin / Finance accounts can review and approve requests, but submission is reserved for department staff in this first MVP slice.');
|
||
redirect('index.php#submit');
|
||
}
|
||
|
||
$result = create_request($currentUser, $_POST);
|
||
if (!empty($result['success'])) {
|
||
flash('success', 'Request ' . $result['request_code'] . ' submitted and routed to the supervisor queue.');
|
||
redirect('request.php?id=' . (int) $result['id']);
|
||
}
|
||
|
||
$formErrors = $result['errors'] ?? [];
|
||
$oldForm = array_merge($oldForm, $result['values'] ?? []);
|
||
}
|
||
}
|
||
|
||
$currentUser = current_user();
|
||
$metrics = $currentUser ? dashboard_metrics($currentUser) : null;
|
||
$visibleRequests = $currentUser ? fetch_visible_requests($currentUser, 12) : [];
|
||
$queueRequests = $currentUser ? fetch_queue_requests($currentUser, 6) : [];
|
||
$timelineSample = [
|
||
['step' => 'Level 1', 'label' => 'Supervisor review'],
|
||
['step' => 'Level 2', 'label' => 'Head of Department'],
|
||
['step' => 'Level 3', 'label' => 'Admin / Finance'],
|
||
];
|
||
|
||
render_head(
|
||
'Department Request & Approval Manager',
|
||
'Submit departmental requests, route them through supervisor and HOD review, and close them with admin/finance sign-off.'
|
||
);
|
||
?>
|
||
<body>
|
||
<?php render_nav($currentUser, 'dashboard'); ?>
|
||
<?php render_flash_toast($flash); ?>
|
||
|
||
<?php if (!$currentUser): ?>
|
||
<main class="public-shell">
|
||
<section class="container py-4 py-lg-5">
|
||
<div class="row g-4 align-items-stretch">
|
||
<div class="col-lg-7">
|
||
<div class="shell-card hero-panel h-100">
|
||
<div class="eyebrow">Internal workflow portal</div>
|
||
<h1 class="hero-title">Track departmental requests from submission to final decision.</h1>
|
||
<p class="hero-copy">This first MVP slice already covers the end-to-end flow: employees submit a request, supervisors and HODs review by department level, and Admin / Finance completes the final sign-off with a visible audit trail.</p>
|
||
<div class="route-band mt-4">
|
||
<?php foreach ($timelineSample as $sample): ?>
|
||
<div class="route-chip">
|
||
<span class="route-step"><?= e($sample['step']) ?></span>
|
||
<span class="route-label"><?= e($sample['label']) ?></span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<div class="row g-3 mt-4">
|
||
<div class="col-sm-4">
|
||
<div class="mini-stat">
|
||
<span class="mini-stat-label">Workflow</span>
|
||
<strong>Create → Approve → Track</strong>
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-4">
|
||
<div class="mini-stat">
|
||
<span class="mini-stat-label">Departments</span>
|
||
<strong>Operations + HR demos</strong>
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-4">
|
||
<div class="mini-stat">
|
||
<span class="mini-stat-label">Demo access</span>
|
||
<strong>Password: Twende2026</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="surface-panel mt-4">
|
||
<div class="surface-panel-title">What you can test immediately</div>
|
||
<ul class="feature-list mb-0">
|
||
<li>Submit a purchase, leave, access, travel, or maintenance request.</li>
|
||
<li>Switch roles using the demo accounts and approve at each department level.</li>
|
||
<li>Open a request detail page to review the full audit trail and latest note.</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-5">
|
||
<div class="shell-card auth-card h-100" id="login-panel">
|
||
<div class="section-kicker">Portal sign in</div>
|
||
<h2 class="section-title">Open the workflow dashboard</h2>
|
||
<p class="section-copy">Use a demo staff account to experience the employee, approver, and admin states.</p>
|
||
<?php if ($loginError): ?>
|
||
<div class="alert alert-danger small" role="alert"><?= e($loginError) ?></div>
|
||
<?php endif; ?>
|
||
<form method="post" class="vstack gap-3 mt-3">
|
||
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
|
||
<input type="hidden" name="action" value="login">
|
||
<div>
|
||
<label class="form-label" for="email">Email</label>
|
||
<input class="form-control" id="email" name="email" type="email" placeholder="mary.employee@company.local" required>
|
||
</div>
|
||
<div>
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<label class="form-label" for="password">Password</label>
|
||
<button type="button" class="btn btn-link btn-sm px-0" data-copy-password="Twende2026">Copy demo password</button>
|
||
</div>
|
||
<input class="form-control" id="password" name="password" type="password" placeholder="Twende2026" required>
|
||
</div>
|
||
<button class="btn btn-primary w-100" type="submit">Sign in</button>
|
||
</form>
|
||
<div class="table-responsive mt-4 demo-table-wrap">
|
||
<table class="table align-middle demo-table mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>User</th>
|
||
<th>Role</th>
|
||
<th>Department</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach (demo_users() as $demoUser): ?>
|
||
<tr>
|
||
<td>
|
||
<div class="fw-semibold"><?= e($demoUser['name']) ?></div>
|
||
<div class="text-muted small"><?= e($demoUser['email']) ?></div>
|
||
</td>
|
||
<td><?= e($demoUser['role_label']) ?></td>
|
||
<td><?= e($demoUser['department']) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
<?php else: ?>
|
||
<main class="app-shell">
|
||
<section class="container py-4" id="overview">
|
||
<div class="shell-card hero-panel dashboard-hero mb-4">
|
||
<div class="row g-4 align-items-start">
|
||
<div class="col-lg-8">
|
||
<div class="eyebrow">Signed in as <?= e($currentUser['role_label']) ?></div>
|
||
<h1 class="hero-title">Department request workflow, ready for daily use.</h1>
|
||
<p class="hero-copy">Submit requests, route them through the department chain, and keep every decision visible. The current policy is fixed to <strong>Supervisor → HOD → Admin / Finance</strong> for all departments in this first release.</p>
|
||
<div class="route-band mt-4">
|
||
<?php foreach ($timelineSample as $sample): ?>
|
||
<div class="route-chip">
|
||
<span class="route-step"><?= e($sample['step']) ?></span>
|
||
<span class="route-label"><?= e($sample['label']) ?></span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<div class="hero-actions mt-4">
|
||
<?php if (can_submit_requests($currentUser)): ?>
|
||
<a href="#submit" class="btn btn-primary">Create request</a>
|
||
<?php endif; ?>
|
||
<a href="#queue" class="btn btn-outline-secondary">View approval queue</a>
|
||
<a href="#requests" class="btn btn-outline-secondary">Open request list</a>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="surface-panel compact-panel">
|
||
<div class="surface-panel-title">Current profile</div>
|
||
<div class="profile-grid">
|
||
<div>
|
||
<span class="text-muted d-block small">Name</span>
|
||
<strong><?= e($currentUser['name']) ?></strong>
|
||
</div>
|
||
<div>
|
||
<span class="text-muted d-block small">Role</span>
|
||
<strong><?= e($currentUser['role_label']) ?></strong>
|
||
</div>
|
||
<div>
|
||
<span class="text-muted d-block small">Department</span>
|
||
<strong><?= e($currentUser['department']) ?></strong>
|
||
</div>
|
||
<div>
|
||
<span class="text-muted d-block small">Access</span>
|
||
<strong><?= ($currentUser['approval_level'] ?? 0) > 0 ? 'Review + approve' : 'Submit + track' ?></strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row g-3 mb-4">
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="shell-card metric-card h-100">
|
||
<span class="metric-label"><?= ($currentUser['approval_level'] ?? 0) > 0 || is_admin_finance($currentUser) ? 'Visible requests' : 'My requests' ?></span>
|
||
<strong class="metric-value"><?= (int) ($metrics['visible_total'] ?? 0) ?></strong>
|
||
<span class="metric-subtext">Requests in your current scope</span>
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="shell-card metric-card h-100">
|
||
<span class="metric-label">Open</span>
|
||
<strong class="metric-value"><?= (int) ($metrics['open'] ?? 0) ?></strong>
|
||
<span class="metric-subtext">Still moving through approval levels</span>
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="shell-card metric-card h-100">
|
||
<span class="metric-label">Awaiting my action</span>
|
||
<strong class="metric-value"><?= (int) ($metrics['awaiting_action'] ?? 0) ?></strong>
|
||
<span class="metric-subtext">Items currently assigned to you</span>
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="shell-card metric-card h-100">
|
||
<span class="metric-label">Approved</span>
|
||
<strong class="metric-value"><?= (int) ($metrics['approved'] ?? 0) ?></strong>
|
||
<span class="metric-subtext">Closed requests in your scope</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row g-4 align-items-start">
|
||
<div class="col-xl-7" id="submit">
|
||
<div class="shell-card h-100">
|
||
<div class="section-kicker">Create / input</div>
|
||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 mb-3">
|
||
<div>
|
||
<h2 class="section-title mb-1">Submit a new department request</h2>
|
||
<p class="section-copy mb-0">Capture the essentials once, then let the workflow route it automatically to each level.</p>
|
||
</div>
|
||
<?php if (!can_submit_requests($currentUser)): ?>
|
||
<span class="badge text-bg-light border">Submission disabled for Admin / Finance</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if (!can_submit_requests($currentUser)): ?>
|
||
<div class="surface-panel">
|
||
<div class="surface-panel-title">Why submission is hidden</div>
|
||
<p class="mb-0 text-muted">This MVP keeps Admin / Finance focused on final approvals and oversight. Use an employee, supervisor, or HOD account to create a request and then switch back here to complete level 3.</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<?php if ($formErrors): ?>
|
||
<div class="alert alert-danger" role="alert">
|
||
<div class="fw-semibold mb-2">Please fix the following before submitting:</div>
|
||
<ul class="mb-0 small ps-3">
|
||
<?php foreach ($formErrors as $error): ?>
|
||
<li><?= e($error) ?></li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</div>
|
||
<?php endif; ?>
|
||
<form method="post" class="row g-3">
|
||
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
|
||
<input type="hidden" name="action" value="create_request">
|
||
<div class="col-md-6">
|
||
<label class="form-label" for="request_type">Request type</label>
|
||
<select class="form-select <?= isset($formErrors['request_type']) ? 'is-invalid' : '' ?>" id="request_type" name="request_type" required>
|
||
<?php foreach (request_type_options() as $type): ?>
|
||
<option value="<?= e($type) ?>" <?= $oldForm['request_type'] === $type ? 'selected' : '' ?>><?= e($type) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<?php if (isset($formErrors['request_type'])): ?><div class="invalid-feedback"><?= e($formErrors['request_type']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label" for="priority">Priority</label>
|
||
<select class="form-select <?= isset($formErrors['priority']) ? 'is-invalid' : '' ?>" id="priority" name="priority" required>
|
||
<?php foreach (priority_options() as $priority): ?>
|
||
<option value="<?= e($priority) ?>" <?= $oldForm['priority'] === $priority ? 'selected' : '' ?>><?= e($priority) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<?php if (isset($formErrors['priority'])): ?><div class="invalid-feedback"><?= e($formErrors['priority']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label" for="title">Short title</label>
|
||
<input class="form-control <?= isset($formErrors['title']) ? 'is-invalid' : '' ?>" id="title" name="title" type="text" maxlength="140" value="<?= e($oldForm['title']) ?>" placeholder="e.g. Laptop replacement for field coordinator" required>
|
||
<?php if (isset($formErrors['title'])): ?><div class="invalid-feedback"><?= e($formErrors['title']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label" for="department_view">Department</label>
|
||
<input class="form-control" id="department_view" type="text" value="<?= e($currentUser['department']) ?>" readonly>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label" for="amount">Amount (optional)</label>
|
||
<input class="form-control <?= isset($formErrors['amount']) ? 'is-invalid' : '' ?>" id="amount" name="amount" type="text" inputmode="decimal" value="<?= e($oldForm['amount']) ?>" placeholder="120000">
|
||
<?php if (isset($formErrors['amount'])): ?><div class="invalid-feedback"><?= e($formErrors['amount']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label" for="needed_by">Needed by</label>
|
||
<input class="form-control <?= isset($formErrors['needed_by']) ? 'is-invalid' : '' ?>" id="needed_by" name="needed_by" type="date" value="<?= e($oldForm['needed_by']) ?>">
|
||
<?php if (isset($formErrors['needed_by'])): ?><div class="invalid-feedback"><?= e($formErrors['needed_by']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label" for="justification">Business justification</label>
|
||
<textarea class="form-control <?= isset($formErrors['justification']) ? 'is-invalid' : '' ?>" id="justification" name="justification" rows="5" maxlength="1000" data-char-count-target="#justification-count" required><?= e($oldForm['justification']) ?></textarea>
|
||
<div class="d-flex justify-content-between mt-2 small text-muted">
|
||
<span>Explain the request clearly so each approver can act without extra follow-up.</span>
|
||
<span id="justification-count">0 characters</span>
|
||
</div>
|
||
<?php if (isset($formErrors['justification'])): ?><div class="invalid-feedback d-block"><?= e($formErrors['justification']) ?></div><?php endif; ?>
|
||
</div>
|
||
<div class="col-12 d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mt-2">
|
||
<div class="small text-muted">Submission starts at supervisor review and automatically advances through HOD and Admin / Finance.</div>
|
||
<button class="btn btn-primary" type="submit">Submit request</button>
|
||
</div>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-xl-5" id="queue">
|
||
<div class="shell-card mb-4">
|
||
<div class="section-kicker">Approval queue</div>
|
||
<div class="d-flex justify-content-between align-items-start gap-2 mb-3">
|
||
<div>
|
||
<h2 class="section-title mb-1">Requests waiting on you</h2>
|
||
<p class="section-copy mb-0">Queue is filtered by your department and approval level.</p>
|
||
</div>
|
||
<span class="badge text-bg-light border"><?= (int) count($queueRequests) ?> active</span>
|
||
</div>
|
||
<?php if (!$queueRequests): ?>
|
||
<div class="empty-state">
|
||
<strong>No items in your queue.</strong>
|
||
<p class="mb-0">When a request reaches your level, it will appear here with a direct link to approve or reject.</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="queue-list">
|
||
<?php foreach ($queueRequests as $queued): ?>
|
||
<a class="queue-item" href="request.php?id=<?= (int) $queued['id'] ?>">
|
||
<div class="queue-item-top">
|
||
<span class="queue-code"><?= e($queued['request_code']) ?></span>
|
||
<span class="badge <?= e(status_badge_class($queued['status'])) ?>"><?= e($queued['status']) ?></span>
|
||
</div>
|
||
<div class="queue-title"><?= e($queued['title']) ?></div>
|
||
<div class="queue-meta"><?= e($queued['department']) ?> · <?= e($queued['request_type']) ?> · Requested by <?= e($queued['requester_name']) ?></div>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="shell-card">
|
||
<div class="section-kicker">Routing policy</div>
|
||
<h2 class="section-title">Current approval design</h2>
|
||
<p class="section-copy">All departments share the same three-step ladder in this first version. Admin can see every request; department approvers only see requests in their own department.</p>
|
||
<div class="policy-grid">
|
||
<?php foreach (workflow_levels() as $level => $definition): ?>
|
||
<div class="policy-row">
|
||
<span class="policy-step">Level <?= (int) $level ?></span>
|
||
<div>
|
||
<div class="fw-semibold"><?= e($definition['label']) ?></div>
|
||
<div class="text-muted small"><?= $level === 3 ? 'Applies across all departments' : 'Applies inside the requester’s department' ?></div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="shell-card mt-4" id="requests">
|
||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
|
||
<div>
|
||
<div class="section-kicker">Request list</div>
|
||
<h2 class="section-title mb-1"><?= ($currentUser['approval_level'] ?? 0) > 0 || is_admin_finance($currentUser) ? 'Requests in view' : 'My request history' ?></h2>
|
||
<p class="section-copy mb-0">Open a row to inspect the approval trail, current stage, and latest decision comment.</p>
|
||
</div>
|
||
<span class="text-muted small">Showing <?= (int) count($visibleRequests) ?> most recent records</span>
|
||
</div>
|
||
<?php if (!$visibleRequests): ?>
|
||
<div class="empty-state">
|
||
<strong>No requests yet.</strong>
|
||
<p class="mb-0">Create your first request above and it will appear here immediately.</p>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table align-middle request-table mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Request</th>
|
||
<th>Department</th>
|
||
<th>Requester</th>
|
||
<th>Status</th>
|
||
<th>Updated</th>
|
||
<th class="text-end">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($visibleRequests as $request): ?>
|
||
<tr>
|
||
<td>
|
||
<div class="fw-semibold"><?= e($request['title']) ?></div>
|
||
<div class="text-muted small"><?= e($request['request_code']) ?> · <?= e($request['request_type']) ?> · <?= format_money($request['amount']) ?></div>
|
||
</td>
|
||
<td><?= e($request['department']) ?></td>
|
||
<td>
|
||
<div><?= e($request['requester_name']) ?></div>
|
||
<div class="text-muted small"><?= e($request['requester_email']) ?></div>
|
||
</td>
|
||
<td>
|
||
<span class="badge <?= e(status_badge_class($request['status'])) ?>"><?= e($request['status']) ?></span>
|
||
<div class="text-muted small mt-1"><?= e(level_label((int) $request['current_stage'])) ?></div>
|
||
</td>
|
||
<td><?= e(format_datetime($request['updated_at'])) ?></td>
|
||
<td class="text-end">
|
||
<a class="btn btn-outline-secondary btn-sm" href="request.php?id=<?= (int) $request['id'] ?>">Open</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
<?php endif; ?>
|
||
|
||
<?php render_footer(); ?>
|
||
<?php render_scripts(); ?>
|