# Leaderboard — the office TV board (Manage · CEO suite)

**Portal:** Manage · **Suite:** `ceo` · **Route:** `manage.ceo.leaderboard.*` (`/manage/ceo/leaderboard`) · **Nav:** Leaderboard (CEO sidebar) · **Permission:** `view-ceo-dashboard`

Part of the [CEO Dashboard](/docs/modules_handbook/manage/ceo-dashboard/readMe.md) suite. Built 2026-10-06 at the founder's request: a page opened **full screen on the office TV**, a leaderboard "to motivate everyone to call, Zoom and bring sales".

## What it does

**This week** — Monday 00:00 to Sunday 23:59 (`PeriodWindow::WEEK`) — for six salespeople fixed in code (`OfficeLeaderboard::PEOPLE`: Wai Kit, Zen, Dylan, Kexin, Shawn, Boon, by email):

- **The race** — one measure at a time, rotating every 9 s: phone call time · meaningful calls · Zoom time · leads engaged · sales brought · commission. The rows re-sort with a slide; the top three stand on a podium (🥇🥈🥉, a crown on 1st). A zero is never ranked; a podium place nobody has reached yet still stands, as a pulsing "?" with "Be #N!" — so a Monday morning, or a measure that is often 0 (reno closed), never leaves half the panel empty.
- **Spotlight** — one person at a time, every 7 s: commission **earned** and **in progress** this week, sales this week, and their whole **open pipeline** per project and stage.
- **Player cards** — every number for every person: calls, call time, meaningful, Zooms, Zoom time, leads (with medals, below).
- **The boss banner** (replaced the team-totals ticker, 2026-10-06) — the boss's cute avatar (`OfficeLeaderboard::BOSS`, Wai Kit) shakes beside a speech bubble that pops to a new line every 4 s: "Call Call Call!!", "Generate Sales!", "Wait what? Go Call!", "Do U Agree?", "Lets Fight Together!", "IPO!", "Ei, Why u guys so lao ya, see my sales" (with emoji; the list is `SLOGANS` in the page). On the right, **the race ends in** — days : hours : minutes : seconds to next Monday 00:00 (`week.ends_at`); the last 24 hours turn red and pulse.
- **Medals on the player cards** — every number that places 1st / 2nd / 3rd among the six gets 🥇🥈🥉 (a zero never places; a tie shares the place), and a card shows 🥇×N for how many measures that person leads.
- **Refresh** — every 15 minutes (`refreshSeconds`), with a countdown ring. Anything that went UP since the last read floats up as a "+N" bubble ("Kexin +2 calls"); a new sale rains confetti.
- **Cute avatars** — the boss uploads one picture per person (button in the header; JPG / PNG / WebP / animated GIF, 5 MB). Without one the board shows their profile photo, and a picture that fails to load falls back to coloured initials — never a broken image on the TV.
- **One screen, no scrolling** (2026-10-06): the page is exactly the viewport height — the podium sits BESIDE the ranking, every ranking row shares the height that is left, and the player cards are a compact 2-column list of 10 numbers. Built for a TV: no sidebar, controls fade out after 4 s without mouse movement, a full-screen button, the cursor hides, and everything respects `prefers-reduced-motion`.

## How each number is counted — [`OfficeLeaderboard`](/src/Ceo/Services/OfficeLeaderboard.php)

Sessions are the shared `ChannelLeaderboard::countableRows()` (a lead is linked, it is not a colleague, a Zoom actually happened).

- **Calls / call time** — ANSWERED phone calls only. A recording with no speech at all is a **no pick-up** (`CallRecording::isNoSpeechError()`, the weekly report's rule) and is counted on its own, never as a call.
- **Meaningful calls** — answered calls the AI read as a sales talk with an interested customer (`MeaningfulConversation`).
- **Zooms / Zoom time** — straight from `SalesEngagementReport` (the Sales Engagement page), so the TV and that page agree on how long a Zoom lasted.
- **Leads engaged** — different leads reached by an answered call or a Zoom.
- **Sales brought** — deals the person is on (`engagement_assignments`) whose booking is dated this week (`bookings.booking_date`). Lost / Swing deals are left out.
- **Commission** — each person's share by `Engagement::commissionShares()` of `Booking::commissionAt()` — the Sales team-performance rail's rule (`SalesProjectsController::teamPerformance()`). **Earned** = deals Converted this week (`engagements.won_at`); **in progress** = this week's bookings not yet Converted. RM (bookings carry no currency).
- **After sales (2026-10-06)** — 🔑 **rental keys**: owners' keys collected for rental this week (`rental_estimate_submissions.key_collected_at`, not Lost); 🛠️ **reno pipeline**: renovation jobs still being quoted today (Draft → Quotation sent); ✅ **reno closed**: jobs AWARDED this week (`renovation_jobs.awarded_at`, the owner committed; not Lost / Cancelled). Each credited to the record's `assigned_admin_id` (users.id); unassigned work only reaches `team` (on production 27 quoting jobs are unassigned, so they show on nobody's card). Rental keys and reno closed are race measures too.
- **Open pipeline** — every open deal the person is on today (not week-bound), per project and stage (`Engagement::COLUMN_STATUS` folds retired statuses).

Checked against production 2026-10-06 for the week Mon 28 Sep – Sun 4 Oct: the 7 open bookings' commission summed to ~RM 337k by hand against the board's RM 334k in-progress total (the difference is how the closing mode's role pools split each deal).

## Related files

- [OfficeLeaderboard](/src/Ceo/Services/OfficeLeaderboard.php) — `PEOPLE`, `AVATAR_COLLECTION` (`cute_avatar`, a `UserProfile` media collection beside the profile `avatar`), `people()`, `build()`
- [LeaderboardController](/app/Http/Controllers/Manage/Ceo/LeaderboardController.php) (`index` · `storeAvatar` · `destroyAvatar` — only for someone on the board, else 404) · [UploadAvatarRequest](/app/Http/Requests/Manage/Ceo/Leaderboard/UploadAvatarRequest.php)
- `resources/js/Pages/Manage/Ceo/Leaderboard/Index.vue` + `Partials/{PlayerAvatar,TweenNumber,AvatarManagerModal}.vue`; reuses [Confetti.vue](/resources/js/Components/Confetti.vue)
- [ManageLayout.vue](/resources/js/Layouts/ManageLayout.vue) (`ceoNav`: Leaderboard)
- Routes (`routes/web.php`, CEO group): `manage.ceo.leaderboard.index` · `.avatars.store` · `.avatars.destroy`
- Tests: [OfficeLeaderboardTest](/tests/Feature/Ceo/OfficeLeaderboardTest.php) (answered vs no pick-up vs meaningful; an avatar only for someone on the board)

## To change who is on the board

Edit `OfficeLeaderboard::PEOPLE` (emails, in display order). An email no longer on file is skipped.
