true, 'secure' => !empty($_SERVER['HTTPS']) || $forwardedHttps, 'samesite' => 'Lax', 'path' => '/']); session_start(); function render_header(string $title): void { $user = current_user(); echo '' . e($title) . ' · JOBcard'; if ($user) { echo '
'; } else { echo '
'; } } function render_footer(): void { $user = current_user(); echo '
' . ($user ? '
' : '') . ''; } $route = scalar_input($_GET['route'] ?? null, current_user() ? 'dashboard' : 'login'); if ($route === 'logout') { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); exit('Logout requires POST'); } verify_csrf(); if (current_user()) audit('logout', 'user', (int)current_user()['id']); $_SESSION = []; session_destroy(); header('Location: /?route=login'); exit; } if ($route === 'login') { if (current_user()) { header('Location: /?route=dashboard'); exit; } $error = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { verify_csrf(); $stmt = db()->prepare('SELECT u.*, r.name AS role_name FROM users u JOIN roles r ON r.id = u.role_id WHERE u.email = :email LIMIT 1'); $stmt->execute(['email' => strtolower(trim(scalar_input($_POST['email'] ?? null))) ]); $user = $stmt->fetch(); if (!$user || !$user['is_active'] || !password_verify(scalar_input($_POST['password'] ?? null), $user['password_hash'])) { $error = 'The email or password is incorrect.'; } else { session_regenerate_id(true); $_SESSION['user_id'] = (int)$user['id']; $_SESSION['csrf'] = bin2hex(random_bytes(32)); db()->prepare('UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = :id')->execute(['id' => $user['id']]); audit('login_success', 'user', (int)$user['id']); header('Location: /?route=dashboard'); exit; } } render_header('Sign in'); ?>

JOBcard

Sign in to the support workspace.

Dashboard

Your operational overview.

New jobcards
0
Open jobcards
0
Hours this week
0.0
SLA warnings
0

Foundation ready

Authentication, role-aware navigation, CSRF protection, password hashing and audit logging are active. Client and jobcard modules will populate this dashboard in the next increments.

'clients.view','jobcards'=>'jobcards.view','reports'=>'reports.view','users'=>'users.manage','audit'=>'audit.view']; if ($route === 'jobcards') { require_permission('jobcards.view'); $errors = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { require_permission('jobcards.manage'); verify_csrf(); $command = (new \App\Domain\Jobcard\JobcardWorkflow())->validateCommand($_POST); $clientId = $command['client_id']; $workRequested = $command['work_requested']; $priority = $command['priority']; $errors = array_values($command['errors']); if (!$errors) { $clientCheck = db()->prepare("SELECT id FROM clients WHERE id = :id AND status = 'active'"); $clientCheck->execute(['id' => $clientId]); if (!$clientCheck->fetchColumn()) $errors[] = 'The selected client is not active or does not exist.'; } if (!$errors) { $year = (int)date('Y'); $pdo = db(); try { $pdo->beginTransaction(); $sequenceStmt = $pdo->prepare('INSERT INTO jobcard_sequences (sequence_year, next_sequence) VALUES (:year, 2) ON DUPLICATE KEY UPDATE next_sequence = next_sequence + 1'); $sequenceStmt->execute(['year' => $year]); $sequenceStmt = $pdo->prepare('SELECT next_sequence - 1 FROM jobcard_sequences WHERE sequence_year = :year FOR UPDATE'); $sequenceStmt->execute(['year' => $year]); $sequence = (int)$sequenceStmt->fetchColumn(); $reference = \App\Domain\Jobcard\JobcardReference::generate($sequence, $year); $stmt = $pdo->prepare('INSERT INTO jobcards (reference_no, client_id, created_by, priority, status, work_requested) VALUES (:reference, :client, :created_by, :priority, \'new\', :requested)'); $stmt->execute(['reference' => $reference, 'client' => $clientId, 'created_by' => $user['id'], 'priority' => $priority, 'requested' => $workRequested]); $jobcardId = (int)$pdo->lastInsertId(); audit('jobcard_created', 'jobcard', $jobcardId, ['reference_no' => $reference]); $pdo->commit(); header('Location: /?route=jobcards&created=1'); exit; } catch (Throwable $exception) { if ($pdo->inTransaction()) $pdo->rollBack(); $errors[] = 'The jobcard could not be created. Please try again.'; } } } $clients = db()->query("SELECT id, name FROM clients WHERE status = 'active' ORDER BY name")->fetchAll(); $jobcards = db()->query('SELECT j.reference_no, j.priority, j.status, j.work_requested, j.created_at, c.name AS client_name FROM jobcards j JOIN clients c ON c.id = j.client_id ORDER BY j.created_at DESC LIMIT 100')->fetchAll(); render_header('Jobcards'); echo '

Jobcards

Track requested work and operational status.

'; if (can('jobcards.manage')) echo ''; echo '
'; if (isset($_GET['created'])) echo '
Jobcard created successfully.
'; if ($errors) echo '
' . e(implode(' ', $errors)) . '
'; if (can('jobcards.manage')) { echo '

Create jobcard

'; } echo '
'; if (!$jobcards) echo ''; foreach ($jobcards as $jobcard) echo ''; echo '
ReferenceClientPriorityStatusWork requestedCreated
No jobcards found.
' . e($jobcard['reference_no']) . '' . e($jobcard['client_name']) . '' . e(ucfirst($jobcard['priority'])) . '' . e(ucwords(str_replace('_', ' ', $jobcard['status']))) . '' . e($jobcard['work_requested']) . '' . e($jobcard['created_at']) . '
'; render_footer(); exit; } if ($route === 'client') { require_permission('clients.view'); $clientId = filter_var(scalar_input($_GET['id'] ?? null), FILTER_VALIDATE_INT); if (!$clientId) { http_response_code(400); exit('Invalid client'); } $stmt = db()->prepare('SELECT * FROM clients WHERE id = :id'); $stmt->execute(['id' => $clientId]); $client = $stmt->fetch(); if (!$client) { http_response_code(404); exit('Client not found'); } $contactErrors = []; $contactOld = ['name' => '', 'email' => '', 'phone' => '', 'is_primary' => false]; if ($_SERVER['REQUEST_METHOD'] === 'POST') { require_permission('clients.manage'); verify_csrf(); $contact = validate_client_contact($_POST); $contactOld = $contact; $contactErrors = $contact['errors']; if ($contactErrors === []) { $pdo = db(); try { $pdo->beginTransaction(); if ($contact['is_primary']) { $pdo->prepare('UPDATE client_contacts SET is_primary = 0 WHERE client_id = :client')->execute(['client' => $clientId]); } $contactInsert = $pdo->prepare('INSERT INTO client_contacts (client_id, name, email, phone, is_primary) VALUES (:client, :name, :email, :phone, :primary)'); $contactInsert->execute(['client' => $clientId, 'name' => $contact['name'], 'email' => $contact['email'], 'phone' => $contact['phone'], 'primary' => $contact['is_primary'] ? 1 : 0]); $contactId = (int)$pdo->lastInsertId(); audit('client_contact_created', 'client_contact', $contactId, ['client_id' => $clientId]); $pdo->commit(); header('Location: /?route=client&id=' . $clientId . '&contact_created=1'); exit; } catch (Throwable $exception) { if ($pdo->inTransaction()) $pdo->rollBack(); $contactErrors[] = 'The contact could not be created. Please try again.'; } } } $contactsStmt = db()->prepare('SELECT name, email, phone, is_primary, notes FROM client_contacts WHERE client_id = :id ORDER BY is_primary DESC, name'); $contactsStmt->execute(['id' => $clientId]); $contacts = $contactsStmt->fetchAll(); render_header('Client details'); echo '
← Back to clients

' . e($client['name']) . '

Client profile and support contacts.

' . e(ucfirst($client['status'])) . '
' . (isset($_GET['contact_created']) ? '
Contact added successfully.
' : '') . ($contactErrors ? '
' . e(implode(' ', $contactErrors)) . '
' : '') . '

Support information

Email
' . e((string)($client['support_email'] ?? '—')) . '
Phone
' . e((string)($client['support_phone'] ?? '—')) . '
Preferred method
' . e((string)($client['preferred_contact_method'] ?? '—')) . '
Address
' . nl2br(e((string)($client['physical_address'] ?? '—'))) . '

Contacts

'; if (!$contacts) echo '

No contacts recorded.

'; foreach ($contacts as $contact) echo '
' . e($contact['name']) . ($contact['is_primary'] ? ' Primary' : '') . '
' . e((string)($contact['email'] ?? '')) . ' ' . e((string)($contact['phone'] ?? '')) . '
'; if (can('clients.manage')) echo '

Add contact

'; echo '
'; render_footer(); exit; } if ($route === 'clients') { require_permission('clients.view'); $errors = []; $old = ['name' => '', 'status' => 'active']; if ($_SERVER['REQUEST_METHOD'] === 'POST') { require_permission('clients.manage'); verify_csrf(); $validated = validate_client($_POST); $old = $validated; $errors = $validated['errors']; if ($errors === []) { $stmt = db()->prepare('INSERT INTO clients (name, status, created_by) VALUES (:name, :status, :created_by)'); $stmt->execute(['name' => $validated['name'], 'status' => $validated['status'], 'created_by' => $user['id']]); $clientId = (int)db()->lastInsertId(); audit('client_created', 'client', $clientId, ['name' => $validated['name']]); header('Location: /?route=clients&created=1'); exit; } } $search = trim(scalar_input($_GET['q'] ?? null)); $stmt = db()->prepare('SELECT id, name, status, support_email, support_phone, created_at FROM clients WHERE (:search = \'\' OR name LIKE :like_name OR support_email LIKE :like_email) ORDER BY name LIMIT 100'); $stmt->execute(['search' => $search, 'like_name' => "%{$search}%", 'like_email' => "%{$search}%"]); $clients = $stmt->fetchAll(); render_header('Clients'); echo '

Clients

Manage client records and support contacts.

'; if (can('clients.manage')) echo ''; echo '
'; if (isset($_GET['created'])) echo '
Client created successfully.
'; if (can('clients.manage')) { echo '

Create client

' . (isset($errors['name']) ? '
' . e($errors['name']) . '
' : '') . '
'; } echo '
'; if (!$clients) echo ''; foreach ($clients as $client) echo ''; echo '
ClientStatusSupport emailPhone
No clients found.
' . e($client['name']) . '' . e(ucfirst($client['status'])) . '' . e((string)($client['support_email'] ?? '—')) . '' . e((string)($client['support_phone'] ?? '—')) . '
'; render_footer(); exit; } if ($route === 'reports') { require_permission('reports.view'); $format = scalar_input($_GET['format'] ?? null); if ($format === 'csv') require_permission('reports.export'); $reportRows = db()->query('SELECT c.id AS client_id, c.name AS client_name, COUNT(DISTINCT j.id) AS jobcards, COALESCE(SUM(te.hours), 0) AS hours FROM clients c LEFT JOIN jobcards j ON j.client_id = c.id LEFT JOIN time_entries te ON te.jobcard_id = j.id GROUP BY c.id, c.name ORDER BY c.name')->fetchAll(); $rows = array_map(static fn (array $row): array => [$row['client_name'], (int)$row['jobcards'], round((float)$row['hours'], 2)], $reportRows); if ($format === 'csv') { $csv = (new CsvExporter())->export(['Client', 'Jobcards', 'Hours'], $rows, true); header('Content-Type: text/csv; charset=UTF-8'); header('Content-Disposition: attachment; filename="hours-per-client.csv"'); header('Cache-Control: no-store'); echo $csv; exit; } render_header('Reports'); echo '

Reports

Internal hours summary by client.

'; if (can('reports.export')) echo 'Export CSV'; echo '
'; if (!$reportRows) echo ''; foreach ($reportRows as $row) echo ''; echo '
ClientJobcardsHours
No report data available.
' . e($row['client_name']) . '' . (int)$row['jobcards'] . '' . e(number_format((float)$row['hours'], 2)) . '
'; render_footer(); exit; } if (isset($permissionByRoute[$route])) { require_permission($permissionByRoute[$route]); render_header(ucfirst($route)); ?>

This module is scaffolded for the next implementation phase.

The route is permission-protected and ready for its domain workflow.
Page not found.