#572 president-only focused view for executive meetings

User asked that when the President (`executive_ceo` role) logs into
the Meetings page he sees a clean "presentation" layout: just the
schedule table, an analog clock plus the weekday/date of the
schedule, and nothing else. The Manage / Notifications / Audit /
PDF tabs and the PDF-export shortcut button are noisy for him and
must disappear — but his backend permissions stay intact.

Changes (frontend only — backend role gates untouched):
- New component `components/executive-meetings/analog-clock.tsx`:
  small SVG analog clock (hour/minute/second hands, navy + gold),
  ticks every second, ~44px default.
- `pages/executive-meetings.tsx`:
  - Derived `isPresidentView` from the `/me` roles: true iff
    `executive_ceo` is present AND the user is NOT also `admin`
    or `executive_office_manager` (admins keep full UI).
  - Effect forces `section = "schedule"` whenever
    `isPresidentView` becomes true, so the president can never
    land on a now-hidden tab.
  - Hides the header PDF-shortcut button and the entire SECTIONS
    tab list when `isPresidentView`. The `#em-toolbar-portal` is
    kept so the date picker still mounts.
  - `ScheduleSection` accepts two new optional props:
    `isPresidentView` and `lang`. When `isPresidentView` is on,
    the toolbar renders the AnalogClock next to a label showing
    `formatWeekday(date, lang, "long")` and `formatDate(date, lang)`
    (e.g. "Sunday · May 17, 2026" / "الأحد · 17 مايو 2026"). The
    label updates immediately when the date input changes.
  - Edit-mode toggle stays gated on `canMutate`, so the president
    (who has canMutate) still gets the inline-edit pencil if he
    wants it.

Out of scope: backend role changes, applying the focused view to
other roles, manual presentation-mode toggle. Verified via tsc
(only the pre-existing `use-push-subscription` error remains).
This commit is contained in:
riyadhafraa
2026-05-17 14:32:19 +00:00
parent d002670e4e
commit 27384d019a
2 changed files with 128 additions and 11 deletions
@@ -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<Date>(() => 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 (
<svg
width={size}
height={size}
viewBox="0 0 100 100"
className={className}
role="img"
aria-label="analog clock"
data-testid="em-analog-clock"
>
<circle cx={cx} cy={cy} r="46" fill="#ffffff" stroke={navy} strokeWidth="3" />
{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 (
<line
key={i}
x1={x1}
y1={y1}
x2={x2}
y2={y2}
stroke={navy}
strokeWidth={i % 3 === 0 ? 2.5 : 1.5}
strokeLinecap="round"
/>
);
})}
<g transform={`rotate(${hourAngle} ${cx} ${cy})`}>
<line x1={cx} y1={cy + 6} x2={cx} y2={cy - 24} stroke={navy} strokeWidth="4" strokeLinecap="round" />
</g>
<g transform={`rotate(${minuteAngle} ${cx} ${cy})`}>
<line x1={cx} y1={cy + 8} x2={cx} y2={cy - 34} stroke={navy} strokeWidth="2.75" strokeLinecap="round" />
</g>
<g transform={`rotate(${secondAngle} ${cx} ${cy})`}>
<line x1={cx} y1={cy + 10} x2={cx} y2={cy - 38} stroke={gold} strokeWidth="1.5" strokeLinecap="round" />
</g>
<circle cx={cx} cy={cy} r="3" fill={navy} />
<circle cx={cx} cy={cy} r="1.25" fill={gold} />
</svg>
);
}