287 lines
9.5 KiB
TypeScript
287 lines
9.5 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|||
|
|
import { Search, Bell, Clock, LogOut, Home, ArrowLeft } from 'lucide-react';
|
||
|
|
import { useNavigate } from 'react-router-dom';
|
||
|
|
import { logout } from '../utils/auth';
|
||
|
|
|
||
|
|
export const TopBar: React.FC = () => {
|
||
|
|
const [time, setTime] = useState(new Date());
|
||
|
|
const navigate = useNavigate();
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const timer = setInterval(() => setTime(new Date()), 1000);
|
||
|
|
return () => clearInterval(timer);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const handleLogout = () => {
|
||
|
|
logout();
|
||
|
|
};
|
||
|
|
|
||
|
|
const user = React.useMemo(() => {
|
||
|
|
try {
|
||
|
|
const stored = localStorage.getItem('teleems_user');
|
||
|
|
if (!stored || stored === 'undefined' || stored === 'null') return {};
|
||
|
|
const parsed = JSON.parse(stored);
|
||
|
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
||
|
|
} catch {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const displayName = String(user.username || 'Admin');
|
||
|
|
const rawRole = Array.isArray(user.roles) ? (user.roles[0] || 'Administrator') : 'Administrator';
|
||
|
|
// Shorten long role names for the header
|
||
|
|
const roleLabel = rawRole
|
||
|
|
.replace(/_/g, ' ')
|
||
|
|
.replace('CURESELECT ADMIN', 'CS ADMIN')
|
||
|
|
.replace('HOSPITAL ADMIN', 'H. ADMIN')
|
||
|
|
.replace('FLEET OPERATOR', 'FLEET OPS')
|
||
|
|
.replace('STATION INCHARGE', 'STATION IC');
|
||
|
|
const initials = displayName.substring(0, 2).toUpperCase() || 'AD';
|
||
|
|
|
||
|
|
const formattedTime = time.toLocaleTimeString('en-US', {
|
||
|
|
hour12: false,
|
||
|
|
timeZone: 'Asia/Kolkata',
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit',
|
||
|
|
second: '2-digit'
|
||
|
|
});
|
||
|
|
const tzLabel = 'IST';
|
||
|
|
|
||
|
|
return (
|
||
|
|
<header
|
||
|
|
className="glass"
|
||
|
|
style={{
|
||
|
|
height: 'var(--topbar-height)',
|
||
|
|
margin: '16px 16px 0 16px',
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'space-between',
|
||
|
|
padding: '0 20px',
|
||
|
|
gap: '16px',
|
||
|
|
border: '1px solid var(--card-border)',
|
||
|
|
background: 'var(--glass-bg)',
|
||
|
|
zIndex: 900,
|
||
|
|
borderRadius: '12px',
|
||
|
|
flexShrink: 0,
|
||
|
|
minWidth: 0,
|
||
|
|
overflow: 'hidden',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{/* ── LEFT: Search + status ────────────────────────────────── */}
|
||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
||
|
|
{/* Global Navigation Controls */}
|
||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
|
||
|
|
{/* Back Navigation Button */}
|
||
|
|
<button
|
||
|
|
onClick={() => navigate(-1)}
|
||
|
|
className="hover-glow"
|
||
|
|
style={{
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
gap: '6px',
|
||
|
|
background: 'rgba(0, 0, 0, 0.03)',
|
||
|
|
border: '1px solid rgba(0, 0, 0, 0.08)',
|
||
|
|
borderRadius: '8px',
|
||
|
|
padding: '8px 14px',
|
||
|
|
color: 'var(--text-primary)',
|
||
|
|
cursor: 'pointer',
|
||
|
|
transition: 'all 0.2s ease',
|
||
|
|
fontWeight: 700,
|
||
|
|
fontSize: '0.8rem'
|
||
|
|
}}
|
||
|
|
title="Go to Previous Screen"
|
||
|
|
>
|
||
|
|
<ArrowLeft size={16} />
|
||
|
|
<span>BACK</span>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{/* Home Navigation Button */}
|
||
|
|
<button
|
||
|
|
onClick={() => navigate('/')}
|
||
|
|
className="hover-glow"
|
||
|
|
style={{
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
gap: '6px',
|
||
|
|
background: 'rgba(0, 209, 255, 0.1)',
|
||
|
|
border: '1px solid rgba(0, 209, 255, 0.3)',
|
||
|
|
borderRadius: '8px',
|
||
|
|
padding: '8px 14px',
|
||
|
|
color: 'var(--accent-cyan)',
|
||
|
|
cursor: 'pointer',
|
||
|
|
transition: 'all 0.2s ease',
|
||
|
|
fontWeight: 700,
|
||
|
|
fontSize: '0.8rem'
|
||
|
|
}}
|
||
|
|
title="Return to Dashboard"
|
||
|
|
>
|
||
|
|
<Home size={16} />
|
||
|
|
<span>HOME</span>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Search bar */}
|
||
|
|
<div style={{ position: 'relative', flexShrink: 0, width: 'clamp(160px, 22vw, 320px)' }}>
|
||
|
|
<Search
|
||
|
|
size={16}
|
||
|
|
style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)', pointerEvents: 'none' }}
|
||
|
|
/>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
placeholder="Search operators, hospitals, incidents..."
|
||
|
|
style={{
|
||
|
|
width: '100%',
|
||
|
|
background: 'rgba(0, 0, 0, 0.03)',
|
||
|
|
border: '1px solid var(--card-border)',
|
||
|
|
borderRadius: '8px',
|
||
|
|
padding: '8px 10px 8px 36px',
|
||
|
|
color: 'var(--text-primary)',
|
||
|
|
fontSize: '0.8rem',
|
||
|
|
outline: 'none',
|
||
|
|
whiteSpace: 'nowrap',
|
||
|
|
overflow: 'hidden',
|
||
|
|
textOverflow: 'ellipsis',
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Status pill — flexible, shrinks if needed */}
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
gap: '7px',
|
||
|
|
background: 'rgba(0, 255, 136, 0.08)',
|
||
|
|
padding: '5px 11px',
|
||
|
|
borderRadius: '20px',
|
||
|
|
border: '1px solid rgba(0, 255, 136, 0.18)',
|
||
|
|
flexShrink: 1,
|
||
|
|
minWidth: 0,
|
||
|
|
overflow: 'hidden',
|
||
|
|
maxWidth: '260px',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div style={{ width: '7px', height: '7px', background: 'var(--accent-green)', borderRadius: '50%', boxShadow: '0 0 6px var(--accent-green)', flexShrink: 0 }} />
|
||
|
|
<span style={{ fontSize: '0.68rem', fontWeight: 700, color: 'var(--accent-green)', letterSpacing: '0.03em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||
|
|
SYSTEM CONTROL PLANE HEALTHY
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* ── RIGHT: Clock + Bell + Profile ───────────────────────── */}
|
||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '20px', flexShrink: 0 }}>
|
||
|
|
{/* Clock */}
|
||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', color: 'var(--text-secondary)', flexShrink: 0 }}>
|
||
|
|
<Clock size={15} />
|
||
|
|
<span
|
||
|
|
className="mono"
|
||
|
|
style={{ fontSize: '0.92rem', color: 'var(--text-primary)', fontWeight: 600, whiteSpace: 'nowrap' }}
|
||
|
|
>
|
||
|
|
{formattedTime}{' '}
|
||
|
|
<span style={{ fontSize: '0.65rem', color: 'var(--text-secondary)' }}>{tzLabel}</span>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Bell */}
|
||
|
|
<div style={{ position: 'relative', cursor: 'pointer', flexShrink: 0 }}>
|
||
|
|
<Bell size={18} style={{ color: 'var(--text-secondary)' }} />
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
position: 'absolute',
|
||
|
|
top: '-4px',
|
||
|
|
right: '-5px',
|
||
|
|
width: '15px',
|
||
|
|
height: '15px',
|
||
|
|
background: 'var(--alert-red)',
|
||
|
|
borderRadius: '50%',
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
fontSize: '0.6rem',
|
||
|
|
fontWeight: 700,
|
||
|
|
color: '#fff',
|
||
|
|
boxShadow: '0 0 8px var(--alert-red)',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
3
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Divider */}
|
||
|
|
<div style={{ height: '20px', width: '1px', background: 'var(--card-border)', flexShrink: 0 }} />
|
||
|
|
|
||
|
|
{/* Profile */}
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
gap: '10px',
|
||
|
|
cursor: 'pointer',
|
||
|
|
padding: '4px 8px',
|
||
|
|
borderRadius: '8px',
|
||
|
|
transition: 'background 0.2s',
|
||
|
|
border: '1px solid transparent',
|
||
|
|
flexShrink: 0,
|
||
|
|
maxWidth: '220px',
|
||
|
|
}}
|
||
|
|
className="hover-glow"
|
||
|
|
onClick={handleLogout}
|
||
|
|
title="Click to logout"
|
||
|
|
>
|
||
|
|
{/* Avatar */}
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
width: '30px',
|
||
|
|
height: '30px',
|
||
|
|
borderRadius: '50%',
|
||
|
|
background: 'linear-gradient(135deg, var(--accent-cyan), #0066ff)',
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
color: 'white',
|
||
|
|
fontWeight: 700,
|
||
|
|
fontSize: '0.75rem',
|
||
|
|
flexShrink: 0,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{initials}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Name + Role */}
|
||
|
|
<div style={{ display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}>
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
fontSize: '0.82rem',
|
||
|
|
fontWeight: 700,
|
||
|
|
color: 'var(--text-primary)',
|
||
|
|
whiteSpace: 'nowrap',
|
||
|
|
overflow: 'hidden',
|
||
|
|
textOverflow: 'ellipsis',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{displayName}
|
||
|
|
</span>
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
fontSize: '0.65rem',
|
||
|
|
color: 'var(--accent-cyan)',
|
||
|
|
whiteSpace: 'nowrap',
|
||
|
|
overflow: 'hidden',
|
||
|
|
textOverflow: 'ellipsis',
|
||
|
|
textTransform: 'uppercase',
|
||
|
|
letterSpacing: '0.03em',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{roleLabel} • Logout
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<LogOut size={14} style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
);
|
||
|
|
};
|