header.php
18.77 KB
<?php
require_once __DIR__ . '/../lib/feature_modules.php';
if (!isLoggedIn()) {
app_redirect('login');
}
$currentUser = getCurrentUser();
$companyName = 'Company Name';
try {
global $pdo;
$stmt = $pdo->query("SELECT value FROM system_info WHERE `key`='company_name'");
$r = $stmt->fetch();
if ($r && !empty($r['value'])) {
$companyName = $r['value'];
}
} catch (Throwable $e) {}
$logoAsset = function_exists('wp_logo_asset') ? wp_logo_asset() : null;
$logoSrc = $logoAsset['url'] ?? '';
$logoExists = ($logoSrc !== '');
$favAsset = function_exists('wp_favicon_asset') ? wp_favicon_asset(true) : null;
$faviconSrc = $favAsset['url'] ?? '';
$route = app_request_path();
$isAdminArea = (strpos($route, '/admin') === 0);
$isManagementArea = (strpos($route, '/management') === 0);
$isAdmin = isAdminRole();
$canManagement = ($isAdmin || hasPermission('management.access'));
$managementSubnav = [];
if ($canManagement) {
$managementSubnav[] = ['label' => 'Dashboard', 'path' => 'management', 'match' => ['/management']];
if ($isAdmin || hasPermission('vehicles.view')) {
$managementSubnav[] = ['label' => 'Vehicles', 'path' => 'management/vehicles', 'match' => ['/management/vehicles', '/management/vehicals']];
}
if ($isAdmin || hasAnyPermission(['rota.view','rota.edit'])) {
$managementSubnav[] = ['label' => 'Rota', 'path' => 'management/rota', 'match' => ['/management/rota']];
}
if ($isAdmin || hasPermission('clock.view_team')) {
$managementSubnav[] = ['label' => 'Clock Records', 'path' => 'management/clock', 'match' => ['/management/clock']];
}
if ($isAdmin || hasAnyPermission(['holidays.view','holidays.manage'])) {
$managementSubnav[] = ['label' => 'Holiday Requests', 'path' => 'management/holidays', 'match' => ['/management/holidays']];
}
if ($isAdmin || hasAnyPermission(['incidents.view','incidents.manage'])) {
$managementSubnav[] = ['label' => 'Incidents', 'path' => 'management/incidents', 'match' => ['/management/incidents']];
}
if ($isAdmin || hasAnyPermission(['contacts.view','contacts.manage'])) {
$managementSubnav[] = ['label' => 'Contacts', 'path' => 'management/contacts', 'match' => ['/management/contacts']];
}
}
$adminSubnav = [];
if ($isAdmin) {
$adminSubnav = [
['label' => 'Overview', 'path' => 'admin', 'match' => ['/admin']],
['label' => 'Staff Management', 'path' => 'admin/staff', 'match' => ['/admin/staff']],
['label' => 'Calendar Setup', 'path' => 'admin/calendar', 'match' => ['/admin/calendar']],
['label' => 'TruTrak Admin', 'path' => 'admin/trutrak', 'match' => ['/admin/trutrak']],
['label' => 'Exports', 'path' => 'admin/exports', 'match' => ['/admin/exports']],
['label' => 'Integrations', 'path' => 'admin/integrations', 'match' => ['/admin/integrations']],
['label' => 'Activity Logs', 'path' => 'admin/logs', 'match' => ['/admin/logs']],
['label' => 'Maintenance', 'path' => 'admin/maintenance', 'match' => ['/admin/maintenance']],
];
}
$v = APP_VERSION;
$clockNavStatus = wp_clock_available_for_current_user() ? wp_clock_status() : ['status' => 'not_available'];
$clockNavLabel = match ($clockNavStatus['status'] ?? 'not_available') {
'clocked_in' => 'End Shift / Status',
'shift_ended' => 'Shift Complete',
'not_clocked_in' => 'Start Shift',
default => 'Clock',
};
$alertsMenu = (hasPermission('alerts.view') || $isAdmin) ? wp_feature_fetch_user_alerts(6) : ['unread' => 0, 'items' => []];
$headerCanRota = (hasPermission('rota.view') || $isAdmin);
$headerMyRota = $headerCanRota ? wp_feature_fetch_rota_for_current_user(date('Y-m-d'), 12) : [];
$avatarLetter = strtoupper(substr((string)($currentUser['display_name'] ?? 'U'), 0, 1));
$extraHeaderCss = '<style>
.header-tools{display:flex;align-items:center;gap:10px;}
.header-drop-toggle{display:flex;align-items:center;gap:12px;border:1px solid var(--border);background:rgba(255,255,255,.02);border-radius:16px;padding:8px 12px;color:var(--text-main);text-decoration:none;}
.header-drop-toggle::after{display:none !important;}
.header-bell{position:relative;border:1px solid var(--border);background:rgba(255,255,255,.02);border-radius:14px;padding:8px 12px;color:var(--text-main);}
.header-badge{position:absolute;top:-6px;right:-6px;min-width:20px;height:20px;padding:0 6px;border-radius:999px;background:var(--accent);color:#111;font-size:.75rem;font-weight:800;display:flex;align-items:center;justify-content:center;border:2px solid var(--bg-main);}
.dropdown-menu.app-dropdown{min-width:320px;max-width:min(92vw,380px);background:#121212;border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:10px;box-shadow:0 20px 40px rgba(0,0,0,.35);}
.dropdown-menu.app-dropdown .dropdown-item{border-radius:10px;color:var(--text-main);padding:10px 12px;white-space:normal;}
.dropdown-menu.app-dropdown .dropdown-item:hover{background:rgba(255,255,255,.05);}
.dropdown-menu.app-dropdown .dropdown-divider{border-color:rgba(255,255,255,.08);}
.header-alert-item{display:block;padding:10px 12px;border-radius:12px;background:rgba(255,255,255,.02);margin-bottom:8px;color:var(--text-main);text-decoration:none;}
.header-alert-item.unread{border:1px solid rgba(255,140,26,.35);background:rgba(255,140,26,.08);}
.header-alert-title{display:flex;justify-content:space-between;gap:10px;font-weight:800;margin-bottom:4px;}
.header-alert-meta{font-size:.84rem;color:var(--text-muted);}
.profile-meta{display:flex;flex-direction:column;align-items:flex-end;line-height:1.2;}
body.sidebar-open{overflow:hidden;}@media (max-width:900px){.profile-meta{display:none}.dropdown-menu.app-dropdown{min-width:280px;max-width:92vw;}.header-tools{gap:8px;}.header-drop-toggle{padding:8px 10px;}}
</style>';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title><?= e($pageTitle ?? 'WorkersPanel') ?> - <?= e($companyName) ?></title>
<?php if ($faviconSrc !== ''): ?>
<link rel="icon" href="<?= e($faviconSrc) ?>">
<link rel="apple-touch-icon" href="<?= e($faviconSrc) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="<?= e(app_asset_url('css/bootstrap-dark-theme.css')) ?>?v=<?= e($v) ?>">
<link rel="stylesheet" href="<?= e(app_asset_url('css/variables.css')) ?>?v=<?= e($v) ?>">
<link rel="stylesheet" href="<?= e(app_asset_url('css/base.css')) ?>?v=<?= e($v) ?>">
<link rel="stylesheet" href="<?= e(app_asset_url('css/components.css')) ?>?v=<?= e($v) ?>">
<link rel="stylesheet" href="<?= e(app_asset_url('css/layout.css')) ?>?v=<?= e($v) ?>">
<link rel="stylesheet" href="<?= e(app_asset_url('css/desktop.css')) ?>?v=<?= e($v) ?>">
<link rel="stylesheet" href="<?= e(app_asset_url('css/modal.css')) ?>?v=<?= e($v) ?>">
<script src="<?= e(app_asset_url('js/modal.js')) ?>?v=<?= e($v) ?>" defer></script>
<script>
function wpSetSidebar(open) {
const sidebar = document.querySelector('.sidebar');
const backdrop = document.getElementById('sidebarBackdrop');
if (!sidebar) return;
sidebar.classList.toggle('open', !!open);
if (backdrop) backdrop.classList.toggle('open', !!open);
document.body.classList.toggle('sidebar-open', !!open);
}
function toggleMobileMenu() {
const sidebar = document.querySelector('.sidebar');
if (!sidebar) return;
wpSetSidebar(!sidebar.classList.contains('open'));
}
document.addEventListener('click', function(e) {
const sidebar = document.querySelector('.sidebar');
const toggle = document.getElementById('mobileMenuToggle');
const backdrop = document.getElementById('sidebarBackdrop');
if (!sidebar || !sidebar.classList.contains('open')) return;
if (backdrop && backdrop.contains(e.target)) {
wpSetSidebar(false);
return;
}
if (!sidebar.contains(e.target) && toggle && !toggle.contains(e.target)) {
wpSetSidebar(false);
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') wpSetSidebar(false);
});
window.addEventListener('resize', function() {
if (window.innerWidth > 900) wpSetSidebar(false);
});
function wpCleanupTransientUi() {
document.querySelectorAll('.wp-modal-overlay').forEach(function(el){ el.remove(); });
document.querySelectorAll('.modal.show').forEach(function(el){
el.classList.remove('show');
el.setAttribute('aria-hidden', 'true');
el.style.display = 'none';
});
document.querySelectorAll('.modal-backdrop').forEach(function(el){ el.remove(); });
document.body.classList.remove('modal-open');
document.body.style.removeProperty('padding-right');
document.body.style.removeProperty('overflow');
if (window.innerWidth > 900) {
wpSetSidebar(false);
}
}
document.addEventListener('DOMContentLoaded', wpCleanupTransientUi);
window.addEventListener('pageshow', wpCleanupTransientUi);
document.addEventListener('hidden.bs.modal', function() {
window.setTimeout(wpCleanupTransientUi, 0);
});
</script>
<?= $extraHeaderCss ?>
<?php if (!empty($extraHead)) echo $extraHead; ?>
</head>
<body>
<div class="app-container">
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<?php include __DIR__ . '/sidebar.php'; ?>
<div class="main-area">
<div class="page-header-simple">
<div class="header-left">
<?php if ($logoExists): ?>
<a href="<?= e(app_url()) ?>" class="header-logo-link" aria-label="Go to dashboard"><img src="<?= e($logoSrc) ?>" alt="<?= e($companyName) ?>" class="header-logo mobile-only"></a>
<span class="company-name desktop-only"><?= e($companyName) ?></span>
<span class="page-separator desktop-only">-</span>
<?php endif; ?>
<span class="page-name"><?= e($pageTitle ?? 'Dashboard') ?></span>
</div>
<div class="header-right header-tools">
<?php if (hasPermission('alerts.view') || $isAdmin): ?>
<div class="dropdown">
<button class="header-bell dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">๐
<?php if (($alertsMenu['unread'] ?? 0) > 0): ?><span class="header-badge"><?= (int)$alertsMenu['unread'] ?></span><?php endif; ?>
</button>
<div class="dropdown-menu dropdown-menu-end app-dropdown">
<div class="d-flex justify-content-between align-items-center px-2 pb-2">
<strong>Alerts</strong>
<?php if (hasPermission('alerts.send') || $isAdmin): ?><a class="btn btn-sm btn-secondary" href="<?= e(app_url('management/alerts')) ?>">Send</a><?php endif; ?>
</div>
<?php if (empty($alertsMenu['items'])): ?>
<div class="text-muted px-2 py-2">No active alerts right now.</div>
<?php else: ?>
<?php foreach ($alertsMenu['items'] as $alert): ?>
<a class="header-alert-item <?= empty($alert['is_read']) ? 'unread' : '' ?>" href="<?= e(app_url('management/alerts')) ?>">
<div class="header-alert-title"><span><?= e((string)$alert['title']) ?></span><span><?= e((string)$alert['priority']) ?></span></div>
<div class="text-muted" style="font-size:.9rem;"><?= e(mb_strimwidth((string)$alert['message'], 0, 110, 'โฆ')) ?></div>
<div class="header-alert-meta"><?= e((string)$alert['sent_by_name']) ?> ยท <?= e(date('d/m H:i', strtotime((string)$alert['sent_at']))) ?></div>
</a>
<?php endforeach; ?>
<?php endif; ?>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="<?= e(app_url('management/alerts')) ?>">Open alerts centre</a>
</div>
</div>
<?php endif; ?>
<div class="dropdown">
<button class="header-drop-toggle dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<div class="profile-meta">
<div class="user-name"><?= e($currentUser['display_name'] ?? 'User') ?></div>
<div class="user-role"><?= e(ucwords(str_replace('-', ' ', (string)($_SESSION['user_role'] ?? '')))) ?></div>
</div>
<div class="user-avatar"><?= e($avatarLetter) ?></div>
</button>
<div class="dropdown-menu dropdown-menu-end app-dropdown">
<div class="px-2 pb-2">
<div class="fw-bold"><?= e($currentUser['display_name'] ?? 'User') ?></div>
<div class="text-muted small"><?= e(ucwords(str_replace('-', ' ', (string)($_SESSION['user_role'] ?? '')))) ?></div>
</div>
<?php if (wp_clock_available_for_current_user()): ?>
<a class="dropdown-item" href="<?= e(app_url('clock')) ?>">โฑ๏ธ <?= e($clockNavLabel) ?></a>
<?php endif; ?>
<?php if ($headerCanRota): ?>
<button class="dropdown-item" type="button" data-wp-header-rota>๐๏ธ My Rota</button>
<?php endif; ?>
<a class="dropdown-item" href="<?= e(app_url('account')) ?>">๐ค Profile</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="<?= e(app_url('logout')) ?>">๐ช Logout</a>
</div>
</div>
<button class="mobile-menu-toggle" id="mobileMenuToggle" onclick="toggleMobileMenu()">โฐ</button>
</div>
</div>
<?php if ($isAdminArea && !empty($adminSubnav)): ?>
<div class="header-nav">
<?php foreach ($adminSubnav as $link): ?>
<?php $isActive = false; foreach ($link['match'] as $match) { if ($route === $match || str_starts_with($route, $match . '/')) { $isActive = true; break; } } ?>
<a href="<?= e(app_url($link['path'])) ?>" class="header-nav-item <?= $isActive ? 'active' : '' ?>"><?= e($link['label']) ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($isManagementArea && !empty($managementSubnav)): ?>
<div class="header-nav">
<?php foreach ($managementSubnav as $link): ?>
<?php $isActive = false; foreach ($link['match'] as $match) { if ($route === $match || str_starts_with($route, $match . '/')) { $isActive = true; break; } } ?>
<a href="<?= e(app_url($link['path'])) ?>" class="header-nav-item <?= $isActive ? 'active' : '' ?>"><?= e($link['label']) ?></a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($headerCanRota): ?>
<div class="wp-header-rota-template" hidden>
<template id="wpHeaderRotaTemplate">
<?php if (!$headerMyRota): ?>
<div class="text-muted">No rota entries are attached to your account yet.</div>
<?php else: ?>
<div style="display:grid;gap:12px;max-height:min(62vh,560px);overflow:auto;">
<?php foreach ($headerMyRota as $entry): ?>
<div style="border:1px solid var(--border);border-radius:14px;padding:12px;background:rgba(255,255,255,.02);">
<div class="d-flex justify-content-between gap-3 flex-wrap">
<strong><?= e((string)$entry['shift_date']) ?></strong>
<span class="text-muted"><?= e((string)($entry['role_label'] ?: 'Assigned')) ?></span>
</div>
<div style="margin-top:6px;font-weight:700;"><?= e((string)($entry['route_name'] ?: 'Route not set')) ?></div>
<div class="text-muted" style="margin-top:4px;"><?= e(trim((string)($entry['van_label'] ?: 'No van') . (($entry['notes'] ?? '') ? ' ยท ' . $entry['notes'] : ''))) ?></div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div style="display:flex;gap:10px;justify-content:flex-end;align-items:center;margin-top:18px;flex-wrap:wrap;">
<button type="button" class="btn btn-secondary" data-close-header-rota>Close</button>
<a href="<?= e(app_url('management/rota?mine=1')) ?>" class="btn btn-primary">Open full rota</a>
</div>
</template>
</div>
<script>
document.addEventListener('DOMContentLoaded', function(){
let headerRotaModal = null;
function closeHeaderRota(){ if (headerRotaModal) { headerRotaModal.close(); headerRotaModal = null; } }
document.addEventListener('click', function(ev){
const closeBtn = ev.target.closest('[data-close-header-rota]');
if (closeBtn) { ev.preventDefault(); closeHeaderRota(); return; }
const trigger = ev.target.closest('[data-wp-header-rota]');
if (!trigger) return;
ev.preventDefault();
const tpl = document.getElementById('wpHeaderRotaTemplate');
if (!tpl || typeof Modal === 'undefined') return;
closeHeaderRota();
headerRotaModal = new Modal({title:'My Rota', content:tpl.innerHTML, size:'large', buttons:[], onClose:function(){ headerRotaModal = null; }});
headerRotaModal.open();
});
});
</script>
<?php endif; ?>
<main class="main-content">