Add Personal Timesheet View to Me Tab on Dashboard - #469
Conversation
Step 1 of the redesign plan. Bottom nav tabs are now Dashboard, Huddle, Clock (center FAB), Tickets, Teams. Settings is still reachable via the avatar dropdown in the header, matching the prototype's priority order.
Step 2 of the redesign plan. Adds a CSS-only override that turns the @mieweb/ui Modal into a bottom-sheet on mobile: slides up from the bottom, full width, rounded top corners only, capped at 88dvh with internal scroll. Applies to every consumer of the shared Modal (tickets, teams, org, invite, confirm-delete, feedback, media, etc.) without forking the component. Desktop behavior is unchanged.
Step 3. Huddle now uses AppPage flush + fill so PostCards stretch to the viewport edges on mobile (no side gutter, no card radius). Tabs, composer, drafts, and search input keep px-4 mobile gutter so they still look inset. Desktop keeps the max-w-4xl reading column (Phase 2 will tighten this to max-w-2xl). No changes to composer logic or post rendering.
The quick-stats grid in DashboardPage.tsx already uses 'grid grid-cols-2 gap-3', which satisfies the mobile 2-column requirement from the redesign plan. Me/Team scope is controlled by the existing OrgTeamSwitcher / TeamContext, not a separate toggle inside the page, so no layout change is needed for step 4. Numbers populate correctly for both personal and team scopes.
Step 5. ClockPage was already a single-column stack (banner status -> plan/wrap-up composer -> primary action -> punch-clock display). On mobile the primary action row (Post plan and clock in / Post wrap-up and clock out / Clock in / Clock out / Break) now stacks vertically with each Button spanning w-full so the tap target reaches the full column width. Desktop keeps the horizontal flex-wrap row via sm:flex-row + sm:w-auto. Plan-first gate logic in useClockToggle is untouched -- all 11 tests in useClockToggle.test.ts still pass.
Replaces the anchored Dropdown popover with the shared Modal primitive: - Mobile: full-width bottom sheet (existing Modal CSS from step 2) - Desktop: centered dialog (existing Modal CSS from step 10) - Organization select + Team list with member counts + '+ New team', matching design/redesign-prototype/index.html's OrgTeamSwitcher exactly - Portals the Modal to document.body so it escapes AppHeader's backdrop-blur containing block (backdrop-filter creates a new containing block for position:fixed descendants, which was pinning the sheet near the header instead of the viewport) - Preserves existing features not in the prototype: org role labels, pending join-request badges, awaiting-approval section
- Header trigger now matches the prototype's stacked two-line layout: brand mark + bold team name on top, muted 'Org name ▾' below — replacing the single-line 'Org > Team' pill. - Replaced the large translateY(100%) slide-up keyframe with the prototype Modal's actual animation: a quiet fade + 4px settle (150ms ease-out). The prototype never really 'slides' a large distance — it docks the sheet to the bottom and just fades/settles it in, which is what read as clean instead of a 'jump'.
- AppPage gains an optional titleActions slot so pages can render controls on the same row as the page title (right-aligned), used here for the Me/Team segmented toggle from the redesign prototype. - DashboardPage: 'Me' (default) shows the signed-in user's own hours today, open/closed/high-priority tickets, and running timers; 'Team' shows the existing team-wide aggregates unchanged. - The toggle is always visible now (previously personal-team users had the team members / time-logged sections silently hidden via isPersonalTeam) — switching to Team on a personal team just shows a team of one instead of hiding the section.
The prototype's desktop sidebar renders the Organization select + Team list + '+ New team' inline, always visible — no header button/modal. Extracted that content into OrgTeamSwitcherPanel (shared with the mobile bottom sheet) and render it directly in Sidebar.tsx's brand block for the expanded desktop rail. The header pill + Modal in OrgTeamSwitcher is now md:hidden (mobile-only) since desktop no longer needs it — mobile behavior is unchanged.
…prototype" This reverts commit b7923fd.
… switcher - Teams page: replace tabs with always-visible Members/Pending sections - Add horizontal-scroll team pill switcher (Personal + selected team lead, rest follow) - Consolidate Rename/Delete/Copy/Share into Team Settings modal; keep code+copy under team name - Restore per-member 3-dot menu (Make/Remove Admin, Set Password, Remove Member) for all rows, including self (minus self-demote/remove) - Compact icon-only Create/Join Team buttons in page title actions - Move admin Timesheet view from Teams to Dashboard's Team tab with Overview/Timesheet toggle and deep-link support - Scope the mobile bottom-sheet Modal treatment to only the org/team switcher modal, restoring default centered dialogs everywhere else - Update e2e tests and notification deep links to match
Give the plan/wrap-up composer the same Photo/Video/Doc/Pulse/Ticket/ @Mention bar as the Huddle composer (extracted into shared ComposerAttachButtons/ComposerChips so both stay in sync), and match the redesign prototype's status card (bold elapsed-session timer instead of the wall clock) and Recent sessions list.
- Override @mieweb/ui's mobile full-screen defaults (min-h-dvh, rounded-none) so all modals render as centered popups on every screen size - Add horizontal margin on small screens so modals don't touch viewport edges - Move org-switcher bottom-sheet rules outside the 767px media query so the bottom-sheet treatment applies on all screen sizes (not just mobile)
Drop the avatar circle and nested flex wrapper from the expanded composer, trim its padding, and widen the feed's max-width from 2xl to 4xl so it isn't stranded in a narrow strip on wide dashboards.
Replace text "Copy" link with a tight code badge + copy + share icon button group so the invite code row stays uncluttered. Share uses the Web Share API with a clipboard fallback for desktop.
Match Clock's column width to Huddle's (max-w-2xl -> max-w-4xl). Also drop the opacity/saturate dimming on the bottom-nav Clock In/Out FAB when a plan or wrap-up is pending — it's a plain link to the clock page, not a disabled control, so it shouldn't look blocked.
The per-row "..." menu used the plain library Dropdown, which positions its menu absolutely inside the ticket list's overflow-y-scroll container. Per the CSS overflow spec, once one axis is scrollable the other axis clips too, so the menu was silently cut off for rows near the bottom of the list -- the same class of bug FilterDropdown already works around elsewhere in this file. Portal the menu to document.body with fixed coordinates computed from the trigger's rect, same pattern.
- Add meView state ('overview' | 'timesheet') to the Me tab
- Add Overview / Timesheet secondary toggle under Me tab
- Render AdminTimesheetPanel pre-loaded with the current user when meView is 'timesheet'
- Add userToTeamMember helper to convert TimecoreUser to TeamMember shape
The border above the like/comment/share row now stretches to the card's full corners (-mx-5/px-5 instead of -mx-1) instead of stopping short. Clicking the author avatar or name in the huddle feed now navigates to their profile page.
Meteor.users has no idGeneration override, so its _id is the default Random.id() format (17-char alphanumeric) — not the 24-char hex ObjectId the /app/profile/:id route's regex expected. Raw userIds without a known username (e.g. huddle post authors) were falling through to the username branch and failing lookup ('Unknown user'), including when clicking your own profile from the Huddle feed. Broadened the ID regex to also match Meteor's default 17-char user id format.
Bumped the dashboard's Recent Activity feed cap from 6 to 10 posts; older activity remains reachable via 'View all' on the Huddle page.
Addresses Copilot review comment on PR #469: the .org-switcher-modal overrides (align-self:end, width:100%, top-only rounding) applied unconditionally, forcing a bottom sheet on desktop too. This contradicted OrgTeamSwitcher's own doc comment and the redesign spec (bottom sheet on mobile, centered dialog on desktop). Wrapped the overrides in the same 767px mobile breakpoint used elsewhere in this file.
design/redesign-prototype/index.html was a static mockup used as design reference during the redesign work — not needed in the shipped PR.
- AppHeader: hide the clock-page shortcut on /app/clock so its 'Clock in' aria-label doesn't collide with the ClockPage's own Clock In button (Playwright strict-mode fail).
- ClockPage e2e page object: .clock-banner → .clock-status; status text 'On shift' → 'Clocked in'; plan gate text 'Write a plan before starting this session' → 'Plan before you clock in' — the underlying DOM was renamed during the redesign.
- Team switcher tests (team-qr-share, team-auto-accept, org-invite-by-email, timesheet-calc): getByRole('menuitem', {teamName}) → getByRole('dialog').getByRole('button', {teamName}); the switcher renders a modal of plain buttons now, not a menu.
- team-qr-share: 'Share team QR code' button lives inside Team Settings; the main teams page opens the share modal via 'Share team invite link'.
- teams: 'Copy' button is now 'Copy team code'; disambiguate the created team header by asserting the h3 CardTitle instead of unscoped getByText.
- dashboard: scope sidebar nav-item assertions to the 'Main navigation' nav, since the dashboard body now has its own Timesheet/Team tabs.
- timesheet-calc admin view: scope the 'Timesheet' tab click to <main> to skip the sidebar's identically-named nav button.
- playwright.config: bump retries 1→2 and per-test timeout 30s→45s; grant clipboard-read/write permission so copy-link flows can actually write to the clipboard (previously NotAllowedError).
- loginAs: after waitForURL('**/dashboard') also await waitForLoadState('networkidle') so the post-login session/orgs/teams fanout is settled before the test navigates — otherwise the next page.goto() occasionally races the session-cookie handshake and bounces back to the login screen.
- realtime specs (media-library, messages, notifications, team-members, timesheet): give the login toHaveURL check an explicit 15s timeout — the default 5s occasionally clips when the backend is warming up under sequential load.
- notifications:52, channels:140, pulsevault:348, notifications:28: test.setTimeout(60000) — these are the four historically flaky tests; 45s occasionally isn't enough late in the suite.
- profile-routing selectTestTeam: after page.reload(), wait for networkidle first, then require the Teams heading with a 20s timeout — the reload path is where the auth cookie sometimes hadn't propagated, dropping us on the login screen.
jlocala1
left a comment
There was a problem hiding this comment.
Went through the whole thing and clicked around the preview deploy. The redesign looks good and the commit list in the description made this way easier to follow than a 48 commit PR has any right to be. Couple of things I ran into.
The big one is the notification URLs, left that inline on notify-core.js. Short version is the old links already out there stop working and fail silently. One thing I couldn't check is how many old notifications are actually sitting around, so if those get cleaned up regularly this might be smaller than it looks.
Second thing: there are two personal timesheets now and they don't match. /app/timesheet is still routed and still linked from the sidebar, the More sheet and the command palette. I compared them on the preview with the same account. The old page has a Break Hours card and an Add Entry button. The new Me > Timesheet has neither, and it also shows a Member dropdown where the only option is yourself, since it's reusing AdminTimesheetPanel with a one element member list. So someone who finds the dashboard one first can't log a manual entry and doesn't see their break hours. Which one is meant to be the real one going forward? If it's the dashboard, the panel probably wants a way to hide the member select and the missing pieces ported over.
Also worth knowing, and not your fault: Playwright Checks and Backend Checks are both if: false in checks.yml, so the e2e changes in here and the retries and timeout bumps in playwright.config.ts never actually ran. The green checks are only lint, typecheck and build. Did you get a manual pass over the suite locally?
Two minor ones I didn't bother with inline. The styles.css modal overrides reach into @mieweb/ui internals (data-slot, data-state) with !important across every modal in the app, so a library bump could break it with nothing failing to catch it, might be worth a Modal variant upstream instead. And the More sheet in BottomNav has aria-modal="true" but focus stays on the page behind it when it opens, so keyboard users tab in from the top. If I'm already gone when these are sorted, feel free to dismiss this rather than waiting on me.
| date, | ||
| userId: actorUserId, | ||
| url: `/app/teams?tab=timesheet&memberId=${actorUserId}&teamId=${teamId}`, | ||
| url: `/app/dashboard?tab=timesheet&memberId=${actorUserId}&teamId=${teamId}`, |
There was a problem hiding this comment.
This URL gets saved into the notification document when it's created, it isn't built at render time, so everything already in the database still points at /app/teams?tab=timesheet&memberId=...&teamId=.... I tried that old URL on the preview as an admin and it just drops you on the Teams member roster with no timesheet and no error, it silently strips the query params. Same for any push notification already sitting on someone's phone. Could /app/teams forward tab=timesheet over to the dashboard, or is migrating the existing rows easier? Same line in timers.js.
There was a problem hiding this comment.
Fixed in both directions. notify-core.js and timers.js now write /app/dashboard?tab=timesheet&... for all new notifications. For existing rows already in the database pointing at /app/teams?tab=timesheet, TeamsPage now detects tab=timesheet in the query string and immediately navigate()s to /app/dashboard?tab=timesheet&memberId=...&teamId=... before stripping the params — so those old links land on the right view instead of dropping silently.
| setTeamView('timesheet'); | ||
| } | ||
| if (memberId) setInitialMemberId(memberId); | ||
| if (teamId && teams.some((t) => t.id === teamId)) setSelectedTeamId(teamId); |
There was a problem hiding this comment.
Question more than anything since I couldn't test it. teams from useTeam() is scopedTeams, filtered by selectedOrgId. So if the notification is for a team in a different org than whatever's currently selected, the match fails, canViewTimesheet stays false, the fallback effect resets teamView to overview, and replaceState has already wiped the params so there's no second chance. The comment in timesheet-calculation-fixes.spec.ts sounds like the same thing and works around it by switching teams by hand. I couldn't set up a second org on the preview to check. Have you run into that?
There was a problem hiding this comment.
Good catch. Fixed by exposing allTeams (the unscoped array, before org filtering) from TeamContext alongside the existing scoped teams. The deep-link effect now checks allTeams when the teamId isn't found in the current org's teams. If the team is found there, it calls setSelectedOrgId(team.orgId) first, then setSelectedTeamId(teamId), so the org switch happens in the same React batch and the auto-correction effect sees the team as in-scope.
| {/* ── Me Timesheet view ────────────────────────────────────────────── */} | ||
| {tab === 'me' && meView === 'timesheet' && user && selectedTeamId && ( | ||
| <AdminTimesheetPanel | ||
| members={[userToTeamMember(user)]} |
There was a problem hiding this comment.
Small one. This builds a new array every render, and TeamContext runs setInterval(..., 1000) on currentTime, so the dashboard re-renders once a second and the panel's auto-select effect and memberOptions memo re-run every tick. A useMemo keyed on user?.id would settle it.
There was a problem hiding this comment.
No longer applicable. The Me → Timesheet view now renders with no props rather than <AdminTimesheetPanel members={[userToTeamMember(user)]} ...>, so the array isn't built at all on the Me tab. The memberOptions memo in AdminTimesheetPanel only runs when it's shown on the Team tab, where it receives the full pre-fetched teamMembers list.
Removes the decorative ClockStrand from the desktop header middle section. The header now shows only the timer pill and action buttons when clocked in.
…e ambient background CSS Restores the original translateX-only keyframe. Removes all ambient-bg orb rules that were added for the background color experiment.
Removes the app-bg class and initAmbientBg hook call from AppLayout. Removes AmbientBgRow toggle from Settings Appearance section and deletes useAmbientBg.ts.
Removes the ClockStrand from the timer pill so mobile shows only the HH:MM:SS digits. Also removes the now-unused ClockStrand import.
Reduces gap, card padding, and timer font size on small screens. Hides the decorative wave strand on mobile. Shrinks the composer heading on mobile. All layout values restore to their original sizes at md breakpoint.
After updateSettings saves requirePlanForClock: true, fan-out a notification to every team member (except the admin who made the change) via createNotification, which persists to the inbox and fires a push.
Initialises teamFilter from selectedTeamId so the list opens pre-filtered to the active team. A useEffect also follows the team switcher so changing teams in the header updates the filter automatically.
…reak, useWorkspaceGreeting
…BottomNav focus trap, styles.css comment - TeamsPage: forward old /app/teams?tab=timesheet notification URLs to /app/dashboard - TeamContext: expose allTeams (unscoped) alongside scoped teams - DashboardPage: switch org when deep-link teamId belongs to a different org - BottomNav More sheet: move focus in on open, trap Tab/Shift+Tab, restore focus to trigger on close - styles.css: document data-slot/data-state internals dependency with TODO to fix upstream
…k active tickets - Remove Me/Team scoping from the Dashboard entirely - Single Overview/Timesheet toggle in the content area (no more two-toggle confusion) - Timesheet tab shows AdminTimesheetPanel for admins, PersonalTimesheetPanel for others - Stats cards always show team data - Active ticket rows are now clickable buttons that navigate to /app/tickets/:id
…ew/Timesheet - Me/Team pill toggle back in the title bar - Stats cards (Hours, Open tickets, Closed today, High priority) scope to Me or Team - Team Members and Time logged today sections only appear on Team tab - Single Overview/Timesheet toggle in content (no duplicate toggles) - Active ticket rows remain clickable, navigating to /app/tickets/:id
Oh. I see it. Are there not test roles for auto logging in? Every test environment should have a drop-down role that we can just login automatically with a single click |
Overview
This branch implements the full mobile/desktop UI redesign for TimeHuddle — bringing the app to parity with the redesign prototype across layout, navigation, and interaction patterns — plus several quality-of-life fixes and feature additions on top.
Changes by Commit
Navigation & Shell
OrgTeamSwitcher
Dashboard
Clock Page
Huddle
Tickets
Teams Page
Modals & Toasts
New Features
AdminTimesheetPanelDocs
design/Acceptance Criteria
npm run typecheckandnpm run lintpass clean