implement TeleEMS platform architecture with centralized API client and master data management system
This commit is contained in:
286
src/components/TopBar.tsx
Normal file
286
src/components/TopBar.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user