diff --git a/artifacts/tx-os/src/components/executive-meetings/analog-clock.tsx b/artifacts/tx-os/src/components/executive-meetings/analog-clock.tsx new file mode 100644 index 00000000..cdd736e5 --- /dev/null +++ b/artifacts/tx-os/src/components/executive-meetings/analog-clock.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from "react"; + +type AnalogClockProps = { + size?: number; + className?: string; +}; + +export function AnalogClock({ size = 44, className = "" }: AnalogClockProps) { + const [now, setNow] = useState(() => new Date()); + useEffect(() => { + const id = window.setInterval(() => setNow(new Date()), 1000); + return () => window.clearInterval(id); + }, []); + + const hours = now.getHours() % 12; + const minutes = now.getMinutes(); + const seconds = now.getSeconds(); + const hourAngle = (hours + minutes / 60) * 30; + const minuteAngle = (minutes + seconds / 60) * 6; + const secondAngle = seconds * 6; + + const cx = 50; + const cy = 50; + const navy = "#0B1E3F"; + const gold = "#C9A04C"; + + return ( + + + {Array.from({ length: 12 }).map((_, i) => { + const angle = (i * 30 * Math.PI) / 180; + const x1 = cx + Math.sin(angle) * 40; + const y1 = cy - Math.cos(angle) * 40; + const x2 = cx + Math.sin(angle) * 44; + const y2 = cy - Math.cos(angle) * 44; + return ( + + ); + })} + + + + + + + + + + + + + ); +} diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx index 2ca3c964..84e1acbe 100644 --- a/artifacts/tx-os/src/pages/executive-meetings.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -114,6 +114,8 @@ import { EditableCell } from "@/components/editable-cell"; // #486: reuse the same dialog the upcoming-alert pops, so postponing // from the schedule row quick-actions menu shares one implementation. import { PostponeDialog } from "@/components/executive-meetings/upcoming-meeting-alert"; +import { AnalogClock } from "@/components/executive-meetings/analog-clock"; +import { formatDate, formatWeekday } from "@/lib/i18n-format"; import { safeHtml, htmlToPlainText, wrapAsParagraph } from "@/lib/safe-html"; import { ALERT_COLOR_PRESETS, @@ -850,6 +852,27 @@ function ExecutiveMeetingsPageInner() { queryFn: () => apiJson("/api/executive-meetings/me"), }); + // #572: "President view" — a focused, presentation-style layout for users + // whose primary role is the CEO/president (`executive_ceo`) and who are + // NOT also wearing an admin hat (admin / office_manager). They get the + // schedule table only — no sub-nav tabs, no PDF shortcut, no audit log — + // plus an analog clock + weekday/date label in the toolbar. Their + // permissions on the backend are untouched; this is purely a UI gate. + const isPresidentView = useMemo(() => { + const roles = me?.roles ?? []; + if (!roles.includes("executive_ceo")) return false; + return !roles.some((r) => r === "admin" || r === "executive_office_manager"); + }, [me]); + + // Force the schedule section when the president lands on the page or + // their role flips on mid-session, so they never get stuck on a now- + // hidden tab. + useEffect(() => { + if (isPresidentView && section !== "schedule") { + setSection("schedule"); + } + }, [isPresidentView, section]); + const { data: fontResp } = useQuery({ queryKey: ["/api/executive-meetings/font-settings"], queryFn: () => apiJson("/api/executive-meetings/font-settings"), @@ -939,23 +962,25 @@ function ExecutiveMeetingsPageInner() {
- + {!isPresidentView && ( + + )}