// ============ TaskQuest — Header & Notifications ============ const { useState, useEffect, useRef } = React; const { ROLES: R2, PROJECTS: P2, CONSULTANTS: C2 } = window.TQ; function Dropdown({ trigger, children, width, align = "left" }) { const [open, setOpen] = useState(false); const ref = useRef(null); useClickOutside(ref, () => setOpen(false), open); return (
{trigger(open, () => setOpen(o => !o))} {open && (
setOpen(false)}> {children}
)}
); } function Header({ role, setRole, project, setProject, theme, toggleTheme, authUser, onLogout, canSwitchRole = true, notifications, onOpenNotif, notifOpen, notifPanel }) { const r = R2[role]; const isDM = r.type === "dm"; // PMs/consultants are scoped to the projects they're assigned to; DMs span all. const myProjs = window.TQ.projectsOf(r).map(k => P2[k]).filter(Boolean); const projOptions = isDM ? [{ key: "all", name: "All Projects", color: "#8b7cf6" }, ...Object.values(P2)] : (myProjs.length ? myProjs : Object.values(P2)); const curProj = project === "all" ? { name: "All Projects", color: "#8b7cf6" } : (P2[project] || { name: "—", color: "#878b94" }); const acct = R2[authUser] || r; const acctUsername = (R2[authUser] && R2[authUser].username) || ""; return (
TaskQuest
{/* Project selector — managers only (consultants see their own board) */} {(r.type === "pm" || r.type === "dm") && ( ( )}>
Projects
{projOptions.map(p => ( ))}
)}
{/* Role switcher — prototype only (offline preview / admin impersonation) */} {canSwitchRole && ( ( )}>
View as · prototype
{Object.values(R2).slice().sort((a, b) => { const ord = { dm: 0, pm: 1, consultant: 2, admin: 3 }; return (ord[a.type] - ord[b.type]) || a.name.localeCompare(b.name); }).map(p => ( ))}
)} {/* Signed-in identity (locked to the authenticated user) */} {r.short} {r.type === "consultant" ? "Consultant" : r.label}
{/* Theme toggle */} {/* Notifications */}
{notifOpen && notifPanel}
( )}>
{acct.name}
@{acctUsername}
); } function NotificationsPanel({ notifications, onClose, onMarkAll, onClickNotif }) { const ref = useRef(null); useClickOutside(ref, onClose, true); const iconFor = { date_request: { ic: Ic.Calendar, c: "#f2c94c" }, date_approved: { ic: Ic.CheckCircle, c: "#4cb782" }, approved: { ic: Ic.CheckCircle, c: "#4cb782" }, done: { ic: Ic.Flag, c: "#8b7cf6" }, comment: { ic: Ic.Msg, c: "#4ea7fc" }, bonus: { ic: Ic.Gift, c: "#e0902b" }, rejected: { ic: Ic.X, c: "#eb5757" }, }; return (
Notifications {notifications.some(n => !n.read) && }
{notifications.length === 0 &&
You're all caught up.
} {notifications.map(n => { const meta = iconFor[n.type] || { ic: Ic.Bell, c: "var(--muted)" }; return (
onClickNotif(n)}>
{React.createElement(meta.ic, { size: 15 })}
{n.time}{n.project ? " · " + (P2[n.project] ? P2[n.project].name : "") : ""}
); })}
); } window.Header = Header; window.NotificationsPanel = NotificationsPanel; window.Dropdown = Dropdown;