269 lines
14 KiB
PHP
269 lines
14 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/app.php';
|
|
|
|
ensure_request_table();
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['action'] ?? '') === 'logout') {
|
|
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
|
flash('danger', 'Your session expired. Please try again.');
|
|
} else {
|
|
logout_current_user();
|
|
flash('primary', 'Signed out successfully.');
|
|
}
|
|
redirect('index.php');
|
|
}
|
|
|
|
$currentUser = require_auth();
|
|
$flash = consume_flash();
|
|
$requestId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
|
$request = $requestId > 0 ? fetch_request_by_id($requestId) : null;
|
|
$decisionError = null;
|
|
|
|
if (!$request) {
|
|
http_response_code(404);
|
|
render_head('Request not found', 'The requested workflow record could not be located.');
|
|
?>
|
|
<body>
|
|
<?php render_nav($currentUser, 'dashboard'); ?>
|
|
<main class="app-shell">
|
|
<section class="container py-5">
|
|
<div class="shell-card narrow-panel">
|
|
<div class="section-kicker">Not found</div>
|
|
<h1 class="section-title">Request record not found.</h1>
|
|
<p class="section-copy">The request may have been removed or the link is incomplete.</p>
|
|
<a href="index.php#requests" class="btn btn-primary">Back to request list</a>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<?php render_footer(); ?>
|
|
<?php render_scripts(); ?>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
if (!can_view_request($currentUser, $request)) {
|
|
http_response_code(403);
|
|
render_head('Access denied', 'You do not have permission to view this request.');
|
|
?>
|
|
<body>
|
|
<?php render_nav($currentUser, 'dashboard'); ?>
|
|
<main class="app-shell">
|
|
<section class="container py-5">
|
|
<div class="shell-card narrow-panel">
|
|
<div class="section-kicker">Access denied</div>
|
|
<h1 class="section-title">You cannot open this request.</h1>
|
|
<p class="section-copy">Employees only see their own requests, while approvers only see requests inside their department.</p>
|
|
<a href="index.php#requests" class="btn btn-primary">Back to request list</a>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<?php render_footer(); ?>
|
|
<?php render_scripts(); ?>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!verify_csrf($_POST['csrf_token'] ?? null)) {
|
|
flash('danger', 'Your session expired. Please refresh and try again.');
|
|
redirect('request.php?id=' . $requestId);
|
|
}
|
|
|
|
$action = trim((string) ($_POST['action'] ?? ''));
|
|
if ($action === 'approve_request') {
|
|
$result = apply_request_decision($request, $currentUser, (string) ($_POST['decision'] ?? ''), (string) ($_POST['comment'] ?? ''));
|
|
if (!empty($result['success'])) {
|
|
flash('success', $result['message']);
|
|
redirect('request.php?id=' . $requestId);
|
|
}
|
|
|
|
$decisionError = $result['message'] ?? 'Unable to update this request.';
|
|
$request = fetch_request_by_id($requestId);
|
|
}
|
|
}
|
|
|
|
$request = fetch_request_by_id($requestId);
|
|
$canApprove = can_approve_request($currentUser, $request);
|
|
$auditTrail = array_reverse(decode_audit((string) $request['audit_trail']));
|
|
$workflow = workflow_levels();
|
|
|
|
render_head(
|
|
$request['request_code'] . ' · ' . $request['title'],
|
|
'Request detail with status tracking, approval controls, and audit trail for departmental workflow.'
|
|
);
|
|
?>
|
|
<body>
|
|
<?php render_nav($currentUser, 'dashboard'); ?>
|
|
<?php render_flash_toast($flash); ?>
|
|
|
|
<main class="app-shell">
|
|
<section class="container py-4">
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
|
<div>
|
|
<a href="index.php#requests" class="text-decoration-none small text-muted">← Back to request list</a>
|
|
<h1 class="detail-title mt-2 mb-2"><?= e($request['title']) ?></h1>
|
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
|
<span class="badge text-bg-light border"><?= e($request['request_code']) ?></span>
|
|
<span class="badge <?= e(status_badge_class($request['status'])) ?>"><?= e($request['status']) ?></span>
|
|
<span class="badge <?= e(priority_badge_class($request['priority'])) ?>"><?= e($request['priority']) ?></span>
|
|
</div>
|
|
</div>
|
|
<div class="text-md-end detail-summary-text">
|
|
<div class="text-muted small">Requester</div>
|
|
<div class="fw-semibold"><?= e($request['requester_name']) ?></div>
|
|
<div class="text-muted small"><?= e($request['department']) ?> · <?= e($request['request_type']) ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="shell-card mb-4">
|
|
<div class="section-kicker">Approval progress</div>
|
|
<div class="progress-route">
|
|
<?php foreach ($workflow as $level => $definition): ?>
|
|
<?php
|
|
$state = 'upcoming';
|
|
if ($request['status'] === 'Rejected' && (int) $request['current_stage'] === (int) $level) {
|
|
$state = 'rejected';
|
|
} elseif ($request['status'] === 'Approved' || (int) $request['current_stage'] > (int) $level) {
|
|
$state = 'done';
|
|
} elseif ((int) $request['current_stage'] === (int) $level && str_starts_with((string) $request['status'], 'Pending')) {
|
|
$state = 'current';
|
|
}
|
|
?>
|
|
<div class="progress-step <?= e('state-' . $state) ?>">
|
|
<span class="progress-index"><?= (int) $level ?></span>
|
|
<div>
|
|
<div class="fw-semibold"><?= e($definition['label']) ?></div>
|
|
<div class="text-muted small"><?= $level === 3 ? 'Global approval desk' : e($request['department']) . ' approver' ?></div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 align-items-start">
|
|
<div class="col-xl-7">
|
|
<div class="shell-card mb-4">
|
|
<div class="section-kicker">Request detail</div>
|
|
<div class="detail-grid">
|
|
<div>
|
|
<span class="detail-label">Request type</span>
|
|
<div class="detail-value"><?= e($request['request_type']) ?></div>
|
|
</div>
|
|
<div>
|
|
<span class="detail-label">Department</span>
|
|
<div class="detail-value"><?= e($request['department']) ?></div>
|
|
</div>
|
|
<div>
|
|
<span class="detail-label">Amount</span>
|
|
<div class="detail-value"><?= e(format_money($request['amount'])) ?></div>
|
|
</div>
|
|
<div>
|
|
<span class="detail-label">Needed by</span>
|
|
<div class="detail-value"><?= e(format_date($request['needed_by'])) ?></div>
|
|
</div>
|
|
<div>
|
|
<span class="detail-label">Created</span>
|
|
<div class="detail-value"><?= e(format_datetime($request['created_at'])) ?></div>
|
|
</div>
|
|
<div>
|
|
<span class="detail-label">Last updated</span>
|
|
<div class="detail-value"><?= e(format_datetime($request['updated_at'])) ?></div>
|
|
</div>
|
|
</div>
|
|
<hr class="my-4">
|
|
<span class="detail-label">Business justification</span>
|
|
<p class="detail-paragraph mb-0"><?= nl2br(e($request['justification'])) ?></p>
|
|
</div>
|
|
|
|
<div class="shell-card">
|
|
<div class="section-kicker">Audit trail</div>
|
|
<h2 class="section-title mb-3">Every action on this request</h2>
|
|
<div class="timeline-list">
|
|
<?php foreach ($auditTrail as $entry): ?>
|
|
<div class="timeline-item">
|
|
<div class="timeline-marker"></div>
|
|
<div>
|
|
<div class="d-flex flex-column flex-md-row justify-content-md-between gap-1">
|
|
<div class="fw-semibold"><?= e($entry['action'] ?? 'Update') ?> · <?= e($entry['actor'] ?? 'System') ?></div>
|
|
<div class="text-muted small"><?= e(format_datetime($entry['timestamp'] ?? '')) ?></div>
|
|
</div>
|
|
<div class="text-muted small mb-2"><?= e($entry['role'] ?? 'Workflow') ?></div>
|
|
<?php if (!empty($entry['comment'])): ?>
|
|
<p class="mb-0 detail-paragraph"><?= nl2br(e($entry['comment'])) ?></p>
|
|
<?php else: ?>
|
|
<p class="mb-0 text-muted small">No additional comment recorded.</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-5">
|
|
<div class="shell-card mb-4">
|
|
<div class="section-kicker">Current status</div>
|
|
<h2 class="section-title mb-3">Workflow snapshot</h2>
|
|
<div class="surface-panel compact-panel">
|
|
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
|
<div>
|
|
<div class="surface-panel-title">Next expected step</div>
|
|
<div class="fw-semibold"><?= str_starts_with((string) $request['status'], 'Pending') ? e(level_label((int) $request['current_stage'])) : e($request['status']) ?></div>
|
|
</div>
|
|
<span class="badge <?= e(status_badge_class($request['status'])) ?>"><?= e($request['status']) ?></span>
|
|
</div>
|
|
<div class="small text-muted">Latest note</div>
|
|
<p class="mb-0 detail-paragraph"><?= e((string) ($request['last_comment'] ?: 'No comment recorded yet.')) ?></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="shell-card mb-4">
|
|
<div class="section-kicker">Access rules</div>
|
|
<h2 class="section-title mb-3">Who can act next</h2>
|
|
<ul class="rule-list mb-0">
|
|
<li>Employees can submit and monitor their own requests.</li>
|
|
<li>Supervisors and HODs approve only inside their department.</li>
|
|
<li>Admin / Finance sees every request and closes level 3 approvals.</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="shell-card">
|
|
<div class="section-kicker">Decision</div>
|
|
<h2 class="section-title mb-3">Approve or reject</h2>
|
|
<?php if (!$canApprove): ?>
|
|
<div class="empty-state">
|
|
<strong>No action required from you.</strong>
|
|
<p class="mb-0">This request is either already closed or currently assigned to another approval level.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<?php if ($decisionError): ?>
|
|
<div class="alert alert-danger" role="alert"><?= e($decisionError) ?></div>
|
|
<?php endif; ?>
|
|
<form method="post" class="vstack gap-3">
|
|
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
|
|
<input type="hidden" name="action" value="approve_request">
|
|
<div>
|
|
<label class="form-label" for="comment">Approval note</label>
|
|
<textarea class="form-control" id="comment" name="comment" rows="5" maxlength="600" data-char-count-target="#comment-count" placeholder="Add context for the next approver or explain a rejection."></textarea>
|
|
<div class="d-flex justify-content-between mt-2 small text-muted">
|
|
<span>Notes are optional for approval, but required for rejection.</span>
|
|
<span id="comment-count">0 characters</span>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex flex-column flex-sm-row gap-2">
|
|
<button type="submit" name="decision" value="approve" class="btn btn-primary flex-fill">Approve and continue</button>
|
|
<button type="submit" name="decision" value="reject" class="btn btn-outline-danger flex-fill">Reject request</button>
|
|
</div>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<?php render_footer(); ?>
|
|
<?php render_scripts(); ?>
|