Skip to content

Feature #92: Member role pages and role-aware foundation#98

Open
FadyGergesRezk wants to merge 8 commits into
mainfrom
client/roles-ui-evolution
Open

Feature #92: Member role pages and role-aware foundation#98
FadyGergesRezk wants to merge 8 commits into
mainfrom
client/roles-ui-evolution

Conversation

@FadyGergesRezk

@FadyGergesRezk FadyGergesRezk commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Why

Builds the Member-role UI and the role-aware foundation the other role PRs (#93/#94/#95) will extend. Replaces pages that read raw fixtures inline with a proper three-layer flow (TanStack query → selector/view-model → zustand UI store), adds a runtime mock switch so the app runs fully on fixtures today and flips to live with one env var, and ships the dashboard, role-filtered sidebar, and Roost branding.

⚠️ For the server team — fixtures ARE the proposed contract

The fixtures in web-client/src/mocks/fixtures/ are not throwaway test data — they are how we believe the server responses should be shaped. The client is typed against them, so they double as a contract proposal. Where a fixture's shape differs from what a service returns today, that difference is a request to the server team, not an accident. Please read the highlights below and comment in this PR if you disagree.

Going live is designed to be a one-line swap: every read query is mockOr(mockFn, liveFn) and the live axios call is already written, just gated behind VITE_USE_MOCKS. git grep mockOr is the full inventory.

New things that need your agreement (object to any of these)

  • Foreign keys come back resolved, not as bare ids. Read models return MemberRef { id, first_name, last_name } and TeamRef { id, name } instead of uuid strings — e.g. Team.trainers/trainees, Sport.directors, Feedback.member/creator, Transaction.member/creator, Event.attendees/creator. Write DTOs (*Create/*PartialUpdate) stay bare ids. This removes the need for client-side id→name resolution.
  • New endpoint GET /members/dashboard returning a role-scoped superset object ({ member, events, feedback, balance, report, sports, teams }) — sections by entitlement, not a {role,data} discriminator. The server identifies the caller from the token (no /me). The dashboard page reads this verbatim and derives nothing client-side.
  • Server-side scoping is the source of truth. Each list endpoint returns only what the caller may see (e.g. GET /feedback: members see feedback about themselves, trainers see what they authored, directors see none, admins see all). The mock layer (src/mocks/scope.ts) emulates this exactly so the demo shows real role differences — but the real enforcement must live on the server.
  • Feedback.rating — a nullable 0–10 integer on Feedback + FeedbackSummary (not in the live schema yet). UI hides the rating column when absent.
  • Sport keyed by name (its PK / path param); Team.sport is a sport name. Event.sports_linked stays string[] of names.
  • Money is integer amount_cents, no currency field — client displays EUR; negative = charge, positive = payment. Overdue/Clear is derived client-side from balance sign.

What changed

  • Foundation — role + sub from the token via a single getCurrentUser() resolver; roleLabel() (member → "Member"); VITE_USE_MOCKS switch + dev VITE_MOCK_PERSONA to view the app as any role.
  • Mock layer — typed fixtures for all services + pure, unit-tested role-scoping helpers (scope.ts) + per-persona dashboard fixture.
  • Per-page three-layer flowapi/queries.ts (cache) → model/use*ViewModel.ts (joins/derivations) → model/*UiStore.ts (UI-only state). Pages consume only the view-model + store; no page imports fixtures.
  • Pages — Events (attended/missed/upcoming, no RSVP), Feedback (+ stat cards), Payments (+ paid-in/charged totals, recorded-by), Teams/Organization, Development report (safe markdown renderer, no dangerouslySetInnerHTML), Members, and a composed Dashboard on the index route.
  • Shell & brand — role-filtered sidebar, theme picker, and Roost name/icons/tab title.
  • Toolingreact-day-picker for the calendar; dev Vite proxy rewrite; .env.development.example.

Notes

  • The diff is large (~23k insertions) but most of it is generated fixture data under src/mocks/fixtures/ — review effort should focus on src/types.ts, src/mocks/scope.ts, the model/ view-models, and the contract highlights above.
  • Runs on mocks by default in dev (VITE_USE_MOCKS=true). Copy .env.development.example.env.development. To run against live services set VITE_USE_MOCKS=false.
  • Services that are still hello-world (Events, Finance, Letters, Helper report, the dashboard endpoint) are served entirely by mocks today; going live is a per-query mockOr swap.

Testing

pnpm -C web-client lint, typecheck, build, and test (50 passing) all green. Scope helpers have unit coverage across all four roles.

Closes #92

Summary by CodeRabbit

  • New Features
    • Added a new dashboard landing page with summary cards and role-based sections.
    • Introduced richer list views for members, payments, events, and feedback with search, filters, sorting, and detail panels.
    • Updated navigation and page branding to better match Roost.
  • Bug Fixes
    • Improved loading, empty, and error states across key pages.
    • Added safer report rendering and better date/amount formatting.
    • Fixed local development behavior with clearer mock and environment support.

@FadyGergesRezk FadyGergesRezk self-assigned this Jun 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a mock-data infrastructure (persona-based fixtures, role-based scoping, a mock/live switch) and rebuilds the web client's dashboard, members, feedback, payments, sport-events, organization, and helper pages from placeholder "hello" screens into fully functional, filterable, role-aware views backed by shared UI primitives and view-model hooks.

Changes

Web Client Feature Buildout

Layer / File(s) Summary
Core types, identity & mock switch
web-client/src/types.ts, web-client/src/vite-env.d.ts, web-client/src/mocks/mockSwitch.ts, web-client/src/mocks/personas.ts, web-client/src/features/auth/currentUser.ts, web-client/src/features/auth/useAuth.ts
Introduces ref-based domain types, role labeling, env typings, USE_MOCKS/mockOr, mock personas, and getCurrentUser() wired into useAuth.
Role-based scoping
web-client/src/mocks/scope.ts, web-client/src/mocks/scope.test.ts
Adds and tests scope functions filtering feedback, transactions, balances, events, members, and reports by role.
Mock fixtures
web-client/src/mocks/fixtures/*
Adds seeded fixture datasets for dashboard, finance, organization, and reports, plus a barrel re-export module.
Formatting utilities
web-client/src/lib/format.ts
Adds Intl-based formatters for currency, dates, times, and durations.
Shared UI primitives
web-client/src/components/ui/*
Adds Badge, Calendar, DataTable, DateRangeFilter, PageHeader, Popover, Select, StatCard, TableToolbar, and sidebar tooltip/collapsed-icon styling updates.
App shell & routing
web-client/src/app/layout/AppShell.tsx, web-client/src/app/router/routes.tsx
Adds role-filtered, icon-based sidebar nav and theme selection; changes the index route to render DashboardPage.
Dashboard feature
web-client/src/app/pages/api/dashboardQueries.ts, .../model/useDashboardViewModel.ts, .../DashboardPage.tsx
Adds mock-aware dashboard query, role-based section builders, and the full dashboard UI.
Members feature
web-client/src/features/members/*
Adds mock-aware member queries, filters store, view-model, and filterable members table page.
Feedback feature
web-client/src/features/feedback/*
Adds mock-aware feedback queries, filters store, view-model, and filterable feedback table/detail page.
Payments feature
web-client/src/features/payments/*
Adds mock-aware transaction queries, filters store, view-model, and filterable payments table page.
Sport-events feature
web-client/src/features/sport-events/*
Renames query keys to eventKeys, adds mock-aware queries, attendance-based view-model with tests, and full events table/detail page.
Organization feature
web-client/src/features/organization/*
Adds mock-aware sports/teams queries, teams/roster view-model, and full organization roster page.
Helper report & letters mocking
web-client/src/features/helper/*, web-client/src/features/letters/api/queries.ts
Adds mock-aware report query, report view-model, safe markdown renderer with tests, redesigned helper page, and mock switch wiring for letters.
Project configuration
.gitignore, web-client/.env.development.example, web-client/index.html, web-client/package.json, web-client/src/index.css, web-client/vite.config.ts
Updates ignore rules, env docs, page metadata, react-day-picker dependency, theme CSS, and dev proxy rewrite logic.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Page as Feature Page
  participant ViewModel as useXViewModel
  participant Query as useXQuery
  participant MockOr as mockOr
  participant Fixtures as Fixtures/Scope
  participant Client as API Client
  Page->>ViewModel: request view
  ViewModel->>Query: fetch data
  Query->>MockOr: choose mock or live
  MockOr->>Fixtures: scopeX(getCurrentUser())
  MockOr->>Client: GET endpoint (fallback)
  Fixtures-->>Query: scoped fixture rows
  Client-->>Query: live API rows
  Query-->>ViewModel: raw data
  ViewModel-->>Page: filtered/sorted view + stats
Loading

Estimated code review effort

Estimated code review effort: 5 (Critical) | ~120 minutes

Related Issues: Not specified in provided context.
Related PRs: Not specified in provided context.
Suggested labels: frontend, web-client, feature, mocks
Suggested reviewers: Not specified in provided context.

Poem:
A rabbit hopped through mock and store,
Fixtures piled high, and personas galore,
Dashboards bloomed with stats anew,
Tables filtered, badges too,
Scopes kept watch on who sees what—
Roost's web client grew a lot!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: role-aware member pages and foundational frontend updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch client/roles-ui-evolution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

if (!found) throw new Error('Sport not found')
return Promise.resolve(found)
},
() => organizationClient.get<Sport>(`/sports/${name}`).then(r => r.data),

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web-client/src/index.css (1)

65-73: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Verify dark-mode contrast for --primary-foreground.

--primary keeps the same light/bright hue (oklch(0.768 0.233 130.85)) in dark mode, but --primary-foreground is now set to white (oklch(1 0 0)) — likely low contrast on that light background. The parallel --sidebar-primary-foreground for the identical --sidebar-primary color uses a dark value (oklch(0.274 0.072 132.109), line 93) instead, suggesting the correct foreground for this hue is dark, not white.

💡 Possible fix
   --primary: oklch(0.768 0.233 130.85);
-  --primary-foreground: oklch(1 0 0);
+  --primary-foreground: oklch(0.274 0.072 132.109);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/index.css` around lines 65 - 73, The dark-mode
`--primary-foreground` value in the `.dark` theme block appears to have poor
contrast against the unchanged bright `--primary` color. Update the
`--primary-foreground` token in `web-client/src/index.css` to match the darker
contrast approach already used by `--sidebar-primary-foreground`, and keep the
change scoped to the `.dark` theme variables so the primary button/text styling
remains readable.
web-client/src/features/auth/useAuth.ts (1)

1-20: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the stale snapshot block

useAuth.ts still references KeycloakTokenParsed without importing it, and the local user: AuthUser literal is dead code now; it also no longer satisfies AuthUser because id is missing. Keep only the getCurrentUser() path here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/features/auth/useAuth.ts` around lines 1 - 20, useAuth
currently keeps a stale token snapshot path that is no longer needed and
introduces type issues: it references KeycloakTokenParsed without importing it,
defines an unused local user literal, and that literal does not satisfy AuthUser
because id is missing. Remove the AuthTokenSnapshot type and the parsed/user
construction from useAuth, and keep the return value based only on
getCurrentUser() plus logout so the hook stays consistent with the current auth
source.
🧹 Nitpick comments (9)
web-client/src/features/sport-events/model/useEventsViewModel.ts (1)

79-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated default filters — reuse the single source of truth.

This inline default duplicates defaultFilters already defined in eventsUiStore.ts. If the two literals ever drift, buildEventsView's standalone default (used in tests/without a store) would silently diverge from the store's default.

♻️ Suggested fix
-import { useEventsUiStore } from './eventsUiStore'
+import { defaultEventsFilters, useEventsUiStore } from './eventsUiStore'
...
-  filters: EventsFilters = {
-    search: '',
-    status: 'all',
-    fromDate: '',
-    toDate: '',
-    sort: 'date-asc',
-  },
+  filters: EventsFilters = defaultEventsFilters,

(export defaultFilters as defaultEventsFilters from eventsUiStore.ts.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/features/sport-events/model/useEventsViewModel.ts` around
lines 79 - 90, The default filters in buildEventsView are duplicated and can
drift from the store’s single source of truth. Update buildEventsView in
useEventsViewModel.ts to use the shared default exported from eventsUiStore.ts
(prefer the suggested defaultEventsFilters export) instead of an inline object
literal, so tests and standalone usage stay aligned with the store’s
defaultFilters.
web-client/src/features/sport-events/api/queries.ts (1)

10-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused legacy aliases sportEventsKeys, useSportEvents, and useSportEvent; nothing in the codebase references them anymore, so they can be dropped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/features/sport-events/api/queries.ts` around lines 10 - 18,
The module still exports unused legacy aliases that should be removed. Delete
the `sportEventsKeys` re-export from `eventKeys` in this file, and also remove
the unused `useSportEvents` and `useSportEvent` symbols wherever they are
defined so only the current `eventKeys` API remains. Keep the existing
`eventKeys` object intact and update any nearby exports or references to match
the trimmed public surface.
web-client/src/features/organization/pages/OrganizationPage.tsx (1)

26-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding aria-expanded/aria-hidden to the sport toggle for screen readers.

The sport toggle button (SportSection, Line 168-172) conveys expand/collapse state only visually via the rotating ChevronRight icon. Adding aria-expanded={expanded} on the button and aria-hidden="true" on the decorative chevron would make the disclosure state perceivable to assistive tech.

♿ Proposed tweak
       <button
         type="button"
         onClick={onToggle}
+        aria-expanded={expanded}
         className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-surface-sunken"
       >
         <ChevronRight
+          aria-hidden="true"
           className={`size-4 shrink-0 text-text-tertiary transition-transform ${
             expanded ? 'rotate-90' : ''
           }`}
         />

Also applies to: 151-223

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/features/organization/pages/OrganizationPage.tsx` around lines
26 - 104, The sport disclosure toggle in SportSection only communicates expanded
state visually, so update the button to expose its state to assistive tech by
adding aria-expanded based on the expanded prop and marking the ChevronRight
icon as aria-hidden because it is decorative. Make the change in the
SportSection toggle handler/UI used by OrganizationPage so screen readers can
perceive the expand/collapse state correctly.
web-client/src/app/pages/DashboardPage.tsx (1)

59-157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer the exported view-model interfaces over NonNullable<ReturnType<...>>.

AdminCountsSection, BalanceCard, EventsCards, and FeedbackStat all derive their prop types via NonNullable<ReturnType<typeof useDashboardViewModel>['view']['X']>, even though useDashboardViewModel.ts already exports clean named interfaces (DashboardAdminCountsSection, DashboardBalanceSection, DashboardEventsSection, DashboardFeedbackSection) for exactly this purpose.

♻️ Proposed refactor
 import {
+  type DashboardAdminCountsSection,
+  type DashboardBalanceSection,
+  type DashboardEventsSection,
   type DashboardFeedbackItem,
+  type DashboardFeedbackSection,
   type DashboardSectionState,
   type DashboardSportSection,
   useDashboardViewModel,
 } from './model/useDashboardViewModel'
@@
-  counts: NonNullable<ReturnType<typeof useDashboardViewModel>['view']['adminCounts']>
+  counts: DashboardAdminCountsSection
@@
-  balance: NonNullable<ReturnType<typeof useDashboardViewModel>['view']['myBalance']>
+  balance?: DashboardBalanceSection
@@
-  events: NonNullable<ReturnType<typeof useDashboardViewModel>['view']['myEvents']>
+  events: DashboardEventsSection
@@
-  feedback: NonNullable<ReturnType<typeof useDashboardViewModel>['view']['myFeedback']>
+  feedback: DashboardFeedbackSection
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/app/pages/DashboardPage.tsx` around lines 59 - 157, The
section prop types are inferred indirectly with NonNullable<ReturnType<typeof
useDashboardViewModel>['view'][...]> instead of using the exported view-model
interfaces. Update AdminCountsSection, BalanceCard, EventsCards, and
FeedbackStat to import and use the named types from useDashboardViewModel.ts
(DashboardAdminCountsSection, DashboardBalanceSection, DashboardEventsSection,
DashboardFeedbackSection) so the component props stay explicit and aligned with
the view-model contract.
web-client/src/features/feedback/pages/FeedbackPage.tsx (1)

293-306: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate Field/FieldLabel helpers across pages.

Per the provided graph context, near-identical Field/FieldLabel components are independently defined in SportEventsPage.tsx and OrganizationPage.tsx as well. Consider hoisting these into a shared UI primitive (e.g. components/ui/field.tsx) alongside the other new shared primitives added in this PR (Badge, StatCard, etc.) so styling stays in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/features/feedback/pages/FeedbackPage.tsx` around lines 293 -
306, The `Field` and `FieldLabel` helpers are duplicated across `FeedbackPage`,
`SportEventsPage`, and `OrganizationPage`, so consolidate them into a shared UI
primitive. Move the shared rendering/styling into a common component such as
`components/ui/field.tsx` alongside the other reusable primitives in this PR,
then update the page-level `Field`/`FieldLabel` usages to import the shared
version instead of redefining them locally.
web-client/src/features/payments/api/queries.ts (1)

37-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Repeated "find + scope + throw" mock-lookup pattern.

useMemberBalance, useTransaction here, and useFeedback in feedback/api/queries.ts all repeat the same shape: find a fixture by id, scope it for the current user, and throw a "not found" error if the scoped result is empty. Extracting a small shared helper (e.g. mockFindScopedOrThrow(fixtures, predicate, scopeFn, message)) would remove this duplication and centralize the not-found-error convention across features.

♻️ Example helper
// web-client/src/mocks/mockSwitch.ts
export function mockFindScopedOrThrow<T>(
  items: T[],
  predicate: (item: T) => boolean,
  scope: (items: T[]) => T[],
  notFoundMessage: string,
): Promise<T> {
  const found = items.find(predicate)
  const scoped = found ? scope([found]) : []
  if (!scoped[0]) return Promise.reject(new Error(notFoundMessage))
  return Promise.resolve(scoped[0])
}

Also applies to: 68-81

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/features/payments/api/queries.ts` around lines 37 - 53, The
`useMemberBalance` query repeats the same fixture lookup/scoping/not-found flow
used elsewhere, so extract it into a shared helper such as
`mockFindScopedOrThrow` in the mock utilities. Update `useMemberBalance` (and
the matching `useTransaction`/`useFeedback` call sites) to use that helper
instead of inlining `find`, `scopeBalances`, and the manual `throw new
Error('Balance not found')`, keeping the existing not-found message behavior
consistent.
web-client/src/mocks/scope.test.ts (1)

37-188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Most scope tests re-derive "expected" values using copies of the implementation under test.

memberTeamIds, trainerTeamIds, directorSports, sportTeamIds, teamMemberIds (lines 37-79) duplicate scope.ts's own private helpers verbatim, and the scopeFeedback/scopeTransactions/scopeBalances/scopeEvents/scopeMembers describe blocks build their expectations by calling these copies rather than asserting concrete member/team IDs. Since this is role-based data-visibility logic (who sees whose feedback/finances), a shared bug in the team/sport-membership derivation would reproduce identically in both the implementation and the test, and go undetected. The scopeMembers member/admin assertions (line 180, toEqual([users.member.id])) show the better pattern already in use — extending a couple of hardcoded, concrete-ID assertions per role to the other scope functions would meaningfully raise confidence in this RBAC-adjacent logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/mocks/scope.test.ts` around lines 37 - 188, The scope tests
are reusing copied helper logic instead of asserting concrete expected IDs, so
they can mirror the same bug as the implementation. Update the expectations in
scopeFeedback, scopeTransactions, scopeBalances, scopeEvents, and scopeMembers
to use explicit fixture IDs and hardcoded role-visible results, and remove or
minimize reliance on memberTeamIds, trainerTeamIds, directorSports,
sportTeamIds, and teamMemberIds copies of scope.ts helpers. Keep the existing
concrete pattern used in scopeMembers for users.member/users.admin and extend
that style to the other role-based assertions.
web-client/src/mocks/fixtures/organization.ts (1)

2122-2142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded TEAM_U16 can drift from fixture data.

TEAM_U16 is manually pinned to a team id rather than derived from myTeamFixtures, which is computed right below it and already encodes "teams the signed-in member belongs to." If teamFixtures/CURRENT_MEMBER_ID ever change, this constant can silently point to a team the member no longer belongs to. The name TEAM_U16 is also unrelated to the team's actual name ("Football Juniors"), which is confusing.

♻️ Suggested fix: derive from myTeamFixtures instead of hardcoding
-/** First team the signed-in member belongs to, for convenience. */
-export const TEAM_U16 = 'bbbbbbbb-0001-0000-9e37-000000009e37'
-
 /** name -> Sport. */
 export const sportsByName: Record<string, Sport> = Object.fromEntries(
   sportFixtures.map((s) => [s.name, s]),
 )
@@
 /** Teams the signed-in member belongs to (trainees are resolved MemberRefs). */
 export const myTeamFixtures: Team[] = teamFixtures.filter((t) =>
   t.trainees.some((m) => m.id === CURRENT_MEMBER_ID),
 )
+
+/** First team the signed-in member belongs to, for convenience. */
+export const TEAM_U16 = myTeamFixtures[0]?.id ?? ''
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/mocks/fixtures/organization.ts` around lines 2122 - 2142, The
hardcoded TEAM_U16 constant can drift from the fixture data and should not be
manually pinned; derive it from the already-computed myTeamFixtures (or the
relevant team lookup) so it always matches the signed-in member’s actual team.
Update the organization fixture in the area around TEAM_U16 and myTeamFixtures
so the identifier is sourced from the fixture list rather than a literal UUID,
and keep the reference aligned with the Football Juniors team data.
web-client/src/mocks/fixtures/report.ts (1)

2-7: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Inconsistent markdown formatting across report fixtures.

The Lena Roth entry (Line 3) uses proper markdown syntax (# Development report, ## Attendance, bold/italic), while the other three entries (Lines 4-6) use the same section titles as plain text with no #/## markers. Since reportTextById is rendered through a markdown renderer, these three reports will display as unstyled paragraphs instead of headed sections, unlike the first.

💚 Suggested fix: normalize headings for the remaining entries
-  "99999999-0013-0001-be1e-0000000bbe15": "Development report — Marie Wolf\n\nAttendance\nMarie has attended 10 of 14 sessions this term (71%).\n\nTechnical progress\nCoach feedback over the last month highlights steady improvement in core skills, with consistency under pressure noted as the main growth area.\n\nMatch involvement\nFeatured in recent fixtures with feedback trending positive.\n\nSuggested focus\nMaintain attendance momentum and dedicate warm-up drills to the highlighted growth area. Revisit in 4 weeks.",
+  "99999999-0013-0001-be1e-0000000bbe15": "# Development report — Marie Wolf\n\n## Attendance\nMarie has attended **10 of 14** sessions this term (71%).\n\n## Technical progress\nCoach feedback over the last month highlights steady improvement in core skills, with consistency under pressure noted as the main growth area.\n\n## Match involvement\nFeatured in recent fixtures with feedback trending positive.\n\n## Suggested focus\nMaintain attendance momentum and dedicate warm-up drills to the highlighted growth area. Revisit in **4 weeks**.",

(apply the same pattern to the remaining two entries)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/mocks/fixtures/report.ts` around lines 2 - 7, The report
fixtures in reportTextById are inconsistent because the Lena Roth entry uses
markdown headings while the other entries are plain text, so they won’t render
as headings in the markdown viewer. Update the remaining report strings in
reportTextById to match the markdown structure used by the existing styled
entry, including the main title and section headings in the same format. Keep
the content aligned across the report fixture entries so the renderer displays
them consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-client/src/app/pages/DashboardPage.tsx`:
- Around line 40-48: The DashboardPage rendering guard is using view.myBalance
instead of the loading/error state, which prevents BalanceCard from rendering
while data is still loading and can leave it unsafe when balance is undefined.
Update the conditional around BalanceCard in DashboardPage to gate on
states.myBalance so the card mounts for loading and error states, while still
passing the existing balance and state props through to BalanceCard.

In `@web-client/src/app/pages/model/useDashboardViewModel.ts`:
- Around line 86-87: The fixed DASHBOARD_NOW reference in useDashboardViewModel
is being applied unconditionally in buildEventsSection, which breaks
upcoming-event calculations in live mode. Update the logic around DASHBOARD_NOW
and buildEventsSection so the frozen date is only used when the mock data path
is enabled, and live mode uses the actual current time instead. Keep the
mock-vs-live decision tied to the existing env-switching flow in
useDashboardViewModel so the upcoming filter remains correct in both modes.
- Around line 221-226: `useDashboardViewModel` populates `myBalance` only when
`data?.balance` exists, unlike `myEvents` and `myFeedback`, so the balance card
disappears during loading/error states. Update the `myBalance` branch in
`buildDashboardViewModel` to always assign a balance view model placeholder when
`shouldShowBalance(user.role)` is true, and keep `states.myBalance` in sync with
loading state so `DashboardPage` can render the slot consistently. Use the
existing `buildBalanceSection` and `states.myBalance` pattern to mirror the
events/feedback behavior.

In `@web-client/src/components/ui/badge.tsx`:
- Line 1: BadgeProps uses React.ComponentProps but React is not imported in this
module. Update the imports in badge.tsx by adding a type-only React import, or
switch BadgeProps to use a direct ComponentProps import, so the type reference
is available where BadgeProps is defined.

In `@web-client/src/features/feedback/api/queries.ts`:
- Around line 36-51: The `useFeedback` detail query is missing the same cache
freshness window used by other mock-aware queries, so it refetches too often and
re-shows loading UI. Update `useFeedback` in `queries.ts` to include `staleTime:
30_000`, matching `useFeedbackList`, `useMemberBalance`, and `useTransaction`,
while keeping the existing `queryKey`, `queryFn`, and `enabled` behavior
unchanged.

In `@web-client/src/features/members/model/useMembersViewModel.ts`:
- Around line 29-46: The `memberTeamRows` helper is still letting `undefined`
sports flow into `MemberRow.sports`, which can later render an empty facet badge
instead of the intended empty state. Update the `sports` निर्माण in
`useMembersViewModel.ts` to filter out falsy `team.sport` values before
deduping/sorting, keeping it consistent with the `sportOptions` handling and
preventing `FacetBadges` from receiving blank labels.

In `@web-client/src/features/organization/api/queries.ts`:
- Around line 47-59: The useSport hook is using the wrong identifier throughout:
it accepts id but builds organizationKeys.sport, the mock lookup, the API URL,
and the enabled flag from name. Update useSport to consistently use the same
parameter name or swap all references to id in the hook, including the
sportsByName lookup and organizationClient.get call. If the identifier is used
in the request path, make sure it is safely encoded before interpolating it into
the /sports/... URL.

In `@web-client/src/features/payments/model/usePaymentsViewModel.ts`:
- Around line 14-23: `PaymentRow` is missing the stable transaction identifier,
which forces `PaymentsPage.tsx` to rely on an unstable composite key. Update the
`PaymentRow` interface and the `usePaymentsViewModel` mapping so each row
carries the underlying `Transaction.id`, similar to how `FeedbackRow` preserves
`id`. Then adjust `PaymentsPage.tsx` to key rows with that `id` instead of the
current date/description/index composite.

In `@web-client/src/features/sport-events/pages/SportEventsPage.tsx`:
- Around line 267-292: The FieldLabel and Field components in SportEventsPage
are duplicated verbatim in other detail pages, so consolidate them into a shared
UI primitive and reuse it here. Move the shared FieldLabel/Field implementation
into a common component such as a field UI module, then update SportEventsPage,
FeedbackPage, and OrganizationPage to import the shared version instead of
keeping local copies.

In `@web-client/src/types.ts`:
- Around line 95-102: The AuthUser contract is inconsistent: it declares roles[]
but all consumers and builders use role, so update AuthUser to include role:
Role (and remove or reconcile roles if it is unused) so tokenUser() and
mockPersona() in currentUser.ts type-check,
scopeFeedback/scopeTransactions/scopeBalances/scopeEvents/scopeMembers/scopeReport
in scope.ts can continue switching on user.role, AppShell.tsx can filter
NAV_ITEMS by user.role, and the AuthUser fixtures in scope.test.ts satisfy the
corrected shape.
- Around line 22-32: The EventSummary type currently intersects
S['EventSummary'] with a narrower attendees shape, which leaves the original
Reference[] requirement in place and causes schema mismatches. Update
EventSummary to omit attendees from S['EventSummary'] before reintroducing the
MemberRef[] version, and leave teams_linked as an added optional field only if
needed since it is not part of the generated summary schema. Use the existing
type aliases SportEvent and EventSummary as the locator when making this change.

In `@web-client/vite.config.ts`:
- Around line 21-32: The rewrite callback in the Vite proxy config does not
handle query-string-only service URLs like /api/v1/organization?active=true, so
those requests bypass the duplication logic and can 404 after Traefik strips the
prefix. Update the rewrite logic in the rewrite function to treat a ? after the
service name the same as a trailing slash or end-of-path, so the service segment
is duplicated before the query string. Keep the existing behavior for bare
service paths and preserve the current service list symbols used in the regex.

---

Outside diff comments:
In `@web-client/src/features/auth/useAuth.ts`:
- Around line 1-20: useAuth currently keeps a stale token snapshot path that is
no longer needed and introduces type issues: it references KeycloakTokenParsed
without importing it, defines an unused local user literal, and that literal
does not satisfy AuthUser because id is missing. Remove the AuthTokenSnapshot
type and the parsed/user construction from useAuth, and keep the return value
based only on getCurrentUser() plus logout so the hook stays consistent with the
current auth source.

In `@web-client/src/index.css`:
- Around line 65-73: The dark-mode `--primary-foreground` value in the `.dark`
theme block appears to have poor contrast against the unchanged bright
`--primary` color. Update the `--primary-foreground` token in
`web-client/src/index.css` to match the darker contrast approach already used by
`--sidebar-primary-foreground`, and keep the change scoped to the `.dark` theme
variables so the primary button/text styling remains readable.

---

Nitpick comments:
In `@web-client/src/app/pages/DashboardPage.tsx`:
- Around line 59-157: The section prop types are inferred indirectly with
NonNullable<ReturnType<typeof useDashboardViewModel>['view'][...]> instead of
using the exported view-model interfaces. Update AdminCountsSection,
BalanceCard, EventsCards, and FeedbackStat to import and use the named types
from useDashboardViewModel.ts (DashboardAdminCountsSection,
DashboardBalanceSection, DashboardEventsSection, DashboardFeedbackSection) so
the component props stay explicit and aligned with the view-model contract.

In `@web-client/src/features/feedback/pages/FeedbackPage.tsx`:
- Around line 293-306: The `Field` and `FieldLabel` helpers are duplicated
across `FeedbackPage`, `SportEventsPage`, and `OrganizationPage`, so consolidate
them into a shared UI primitive. Move the shared rendering/styling into a common
component such as `components/ui/field.tsx` alongside the other reusable
primitives in this PR, then update the page-level `Field`/`FieldLabel` usages to
import the shared version instead of redefining them locally.

In `@web-client/src/features/organization/pages/OrganizationPage.tsx`:
- Around line 26-104: The sport disclosure toggle in SportSection only
communicates expanded state visually, so update the button to expose its state
to assistive tech by adding aria-expanded based on the expanded prop and marking
the ChevronRight icon as aria-hidden because it is decorative. Make the change
in the SportSection toggle handler/UI used by OrganizationPage so screen readers
can perceive the expand/collapse state correctly.

In `@web-client/src/features/payments/api/queries.ts`:
- Around line 37-53: The `useMemberBalance` query repeats the same fixture
lookup/scoping/not-found flow used elsewhere, so extract it into a shared helper
such as `mockFindScopedOrThrow` in the mock utilities. Update `useMemberBalance`
(and the matching `useTransaction`/`useFeedback` call sites) to use that helper
instead of inlining `find`, `scopeBalances`, and the manual `throw new
Error('Balance not found')`, keeping the existing not-found message behavior
consistent.

In `@web-client/src/features/sport-events/api/queries.ts`:
- Around line 10-18: The module still exports unused legacy aliases that should
be removed. Delete the `sportEventsKeys` re-export from `eventKeys` in this
file, and also remove the unused `useSportEvents` and `useSportEvent` symbols
wherever they are defined so only the current `eventKeys` API remains. Keep the
existing `eventKeys` object intact and update any nearby exports or references
to match the trimmed public surface.

In `@web-client/src/features/sport-events/model/useEventsViewModel.ts`:
- Around line 79-90: The default filters in buildEventsView are duplicated and
can drift from the store’s single source of truth. Update buildEventsView in
useEventsViewModel.ts to use the shared default exported from eventsUiStore.ts
(prefer the suggested defaultEventsFilters export) instead of an inline object
literal, so tests and standalone usage stay aligned with the store’s
defaultFilters.

In `@web-client/src/mocks/fixtures/organization.ts`:
- Around line 2122-2142: The hardcoded TEAM_U16 constant can drift from the
fixture data and should not be manually pinned; derive it from the
already-computed myTeamFixtures (or the relevant team lookup) so it always
matches the signed-in member’s actual team. Update the organization fixture in
the area around TEAM_U16 and myTeamFixtures so the identifier is sourced from
the fixture list rather than a literal UUID, and keep the reference aligned with
the Football Juniors team data.

In `@web-client/src/mocks/fixtures/report.ts`:
- Around line 2-7: The report fixtures in reportTextById are inconsistent
because the Lena Roth entry uses markdown headings while the other entries are
plain text, so they won’t render as headings in the markdown viewer. Update the
remaining report strings in reportTextById to match the markdown structure used
by the existing styled entry, including the main title and section headings in
the same format. Keep the content aligned across the report fixture entries so
the renderer displays them consistently.

In `@web-client/src/mocks/scope.test.ts`:
- Around line 37-188: The scope tests are reusing copied helper logic instead of
asserting concrete expected IDs, so they can mirror the same bug as the
implementation. Update the expectations in scopeFeedback, scopeTransactions,
scopeBalances, scopeEvents, and scopeMembers to use explicit fixture IDs and
hardcoded role-visible results, and remove or minimize reliance on
memberTeamIds, trainerTeamIds, directorSports, sportTeamIds, and teamMemberIds
copies of scope.ts helpers. Keep the existing concrete pattern used in
scopeMembers for users.member/users.admin and extend that style to the other
role-based assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ba49071e-4c5b-4064-b082-a4f0b9572451

📥 Commits

Reviewing files that changed from the base of the PR and between 70055b1 and 865678d.

⛔ Files ignored due to path filters (4)
  • web-client/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • web-client/public/RoostFavIcon.svg is excluded by !**/*.svg
  • web-client/public/RoostIcon.svg is excluded by !**/*.svg
  • web-client/public/favicon.svg is excluded by !**/*.svg
📒 Files selected for processing (64)
  • .gitignore
  • web-client/.env.development.example
  • web-client/index.html
  • web-client/package.json
  • web-client/src/app/layout/AppShell.tsx
  • web-client/src/app/pages/DashboardPage.tsx
  • web-client/src/app/pages/api/dashboardQueries.ts
  • web-client/src/app/pages/model/useDashboardViewModel.ts
  • web-client/src/app/router/routes.tsx
  • web-client/src/components/ui/badge.tsx
  • web-client/src/components/ui/calendar.tsx
  • web-client/src/components/ui/data-table.tsx
  • web-client/src/components/ui/date-range-filter.tsx
  • web-client/src/components/ui/page-header.tsx
  • web-client/src/components/ui/popover.tsx
  • web-client/src/components/ui/select.tsx
  • web-client/src/components/ui/sidebar.tsx
  • web-client/src/components/ui/stat-card.tsx
  • web-client/src/components/ui/table-toolbar.tsx
  • web-client/src/features/auth/currentUser.ts
  • web-client/src/features/auth/useAuth.ts
  • web-client/src/features/feedback/api/queries.ts
  • web-client/src/features/feedback/model/feedbackUiStore.ts
  • web-client/src/features/feedback/model/useFeedbackViewModel.ts
  • web-client/src/features/feedback/pages/FeedbackPage.tsx
  • web-client/src/features/helper/api/queries.ts
  • web-client/src/features/helper/model/useReportViewModel.ts
  • web-client/src/features/helper/pages/HelperPage.tsx
  • web-client/src/features/helper/pages/ReportMarkdown.test.tsx
  • web-client/src/features/helper/pages/ReportMarkdown.tsx
  • web-client/src/features/letters/api/queries.ts
  • web-client/src/features/members/api/queries.ts
  • web-client/src/features/members/model/membersUiStore.ts
  • web-client/src/features/members/model/useMembersViewModel.ts
  • web-client/src/features/members/pages/MembersPage.tsx
  • web-client/src/features/organization/api/queries.ts
  • web-client/src/features/organization/model/useTeamsViewModel.ts
  • web-client/src/features/organization/pages/OrganizationPage.tsx
  • web-client/src/features/payments/api/queries.ts
  • web-client/src/features/payments/model/paymentsUiStore.ts
  • web-client/src/features/payments/model/usePaymentsViewModel.ts
  • web-client/src/features/payments/pages/PaymentsPage.tsx
  • web-client/src/features/sport-events/api/queries.ts
  • web-client/src/features/sport-events/model/eventsUiStore.ts
  • web-client/src/features/sport-events/model/useEventsViewModel.test.ts
  • web-client/src/features/sport-events/model/useEventsViewModel.ts
  • web-client/src/features/sport-events/pages/SportEventsPage.tsx
  • web-client/src/index.css
  • web-client/src/lib/format.ts
  • web-client/src/mocks/fixtures/dashboard.ts
  • web-client/src/mocks/fixtures/events.ts
  • web-client/src/mocks/fixtures/feedback.ts
  • web-client/src/mocks/fixtures/finance.ts
  • web-client/src/mocks/fixtures/index.ts
  • web-client/src/mocks/fixtures/members.ts
  • web-client/src/mocks/fixtures/organization.ts
  • web-client/src/mocks/fixtures/report.ts
  • web-client/src/mocks/mockSwitch.ts
  • web-client/src/mocks/personas.ts
  • web-client/src/mocks/scope.test.ts
  • web-client/src/mocks/scope.ts
  • web-client/src/types.ts
  • web-client/src/vite-env.d.ts
  • web-client/vite.config.ts

Comment on lines +40 to +48
{(view.myBalance || view.myEvents || view.myFeedback) && (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
{view.myBalance && <BalanceCard balance={view.myBalance} state={states.myBalance} />}
{view.myEvents && <EventsCards events={view.myEvents} state={states.myEvents} />}
{view.myFeedback && (
<FeedbackStat feedback={view.myFeedback} state={states.myFeedback} />
)}
</div>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Gate BalanceCard rendering on states.myBalance, not view.myBalance.

Because view.myBalance is only set once data has arrived (see companion comment in useDashboardViewModel.ts), the balance card doesn't render at all during loading (no skeleton) and could also crash if it ever needs to render with a state.error but balance undefined, since BalanceCard unconditionally accesses balance.balanceFormatted once past the isLoading check.

🐛 Proposed fix
-          {view.myBalance && <BalanceCard balance={view.myBalance} state={states.myBalance} />}
+          {states.myBalance && <BalanceCard balance={view.myBalance} state={states.myBalance} />}
 function BalanceCard({
   balance,
   state,
 }: {
-  balance: NonNullable<ReturnType<typeof useDashboardViewModel>['view']['myBalance']>
+  balance?: NonNullable<ReturnType<typeof useDashboardViewModel>['view']['myBalance']>
   state?: DashboardSectionState
 }) {
-  if (state?.isLoading) return <Skeleton className="h-32 border" />
+  if (state?.isLoading || !balance) return <Skeleton className="h-32 border" />

Also applies to: 85-102

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/app/pages/DashboardPage.tsx` around lines 40 - 48, The
DashboardPage rendering guard is using view.myBalance instead of the
loading/error state, which prevents BalanceCard from rendering while data is
still loading and can leave it unsafe when balance is undefined. Update the
conditional around BalanceCard in DashboardPage to gate on states.myBalance so
the card mounts for loading and error states, while still passing the existing
balance and state props through to BalanceCard.

Comment on lines +86 to +87
const DASHBOARD_NOW = new Date('2026-06-19T00:00:00Z')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Hardcoded reference date breaks "upcoming events" once live data is used.

DASHBOARD_NOW is frozen at 2026-06-19T00:00:00Z and used unconditionally in buildEventsSection regardless of mock vs. live mode. Once the app is switched to the real API (the PR's stated goal: swap mocked→live by one env var), "upcoming" will be computed against a permanently stale reference date instead of the actual current time — real events before that frozen date will incorrectly show as upcoming, and events shortly after it will incorrectly be excluded once real "now" passes it.

🐛 Proposed fix
-const DASHBOARD_NOW = new Date('2026-06-19T00:00:00Z')
-
 // "Recent" = latest N by created_at, no time window (decision 2026-06-26).
 const RECENT_FEEDBACK_COUNT = 3
@@
 function buildEventsSection(events: EventSummary[]): DashboardEventsSection {
+  const now = new Date()
   const upcoming = events
-    .filter((event) => new Date(event.start_time) >= DASHBOARD_NOW)
+    .filter((event) => new Date(event.start_time) >= now)
     .toSorted((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime())

If a frozen "now" is genuinely needed for deterministic mock-mode demos, gate it behind the mock switch instead of applying it universally.

Also applies to: 116-126

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/app/pages/model/useDashboardViewModel.ts` around lines 86 -
87, The fixed DASHBOARD_NOW reference in useDashboardViewModel is being applied
unconditionally in buildEventsSection, which breaks upcoming-event calculations
in live mode. Update the logic around DASHBOARD_NOW and buildEventsSection so
the frozen date is only used when the mock data path is enabled, and live mode
uses the actual current time instead. Keep the mock-vs-live decision tied to the
existing env-switching flow in useDashboardViewModel so the upcoming filter
remains correct in both modes.

Comment on lines +221 to +226
if (shouldShowBalance(user.role) && data?.balance) {
view.myBalance = buildBalanceSection(data.balance)
states.myBalance = state
} else if (shouldShowBalance(user.role)) {
states.myBalance = state
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

myBalance populated inconsistently with myEvents/myFeedback, causing a missing loading skeleton.

Unlike myEvents/myFeedback, which are always populated via data?.x ?? [] regardless of load state, view.myBalance is only set when data?.balance is truthy. During loading (and on error), view.myBalance stays undefined, so DashboardPage's {view.myBalance && <BalanceCard .../>} never renders — the balance stat card slot is entirely absent (no skeleton) until data arrives, causing a layout shift, while the events/feedback cards correctly render their skeletons.

♻️ Suggested fix (pairs with a DashboardPage.tsx change to gate rendering on `states.myBalance` instead of `view.myBalance`)
-    if (shouldShowBalance(user.role) && data?.balance) {
-      view.myBalance = buildBalanceSection(data.balance)
-      states.myBalance = state
-    } else if (shouldShowBalance(user.role)) {
-      states.myBalance = state
-    }
+    if (shouldShowBalance(user.role)) {
+      view.myBalance = data?.balance ? buildBalanceSection(data.balance) : undefined
+      states.myBalance = state
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/app/pages/model/useDashboardViewModel.ts` around lines 221 -
226, `useDashboardViewModel` populates `myBalance` only when `data?.balance`
exists, unlike `myEvents` and `myFeedback`, so the balance card disappears
during loading/error states. Update the `myBalance` branch in
`buildDashboardViewModel` to always assign a balance view model placeholder when
`shouldShowBalance(user.role)` is true, and keep `states.myBalance` in sync with
loading state so `DashboardPage` can render the slot consistently. Use the
existing `buildBalanceSection` and `states.myBalance` pattern to mirror the
events/feedback behavior.

@@ -0,0 +1,36 @@
import { cn } from '@/lib/utils'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check tsconfig for allowUmdGlobalAccess and confirm React import convention across UI components.
fd -e json tsconfig -x cat {}
rg -n "allowUmdGlobalAccess" .
rg -n "^import \* as React|^import type React|^import React" web-client/src/components/ui/

Repository: AET-DevOps26/team-devoops

Length of output: 2518


Import React before using React.ComponentProps

BadgeProps references React.ComponentProps<'span'>, but this file only imports cn. Add import type * as React from 'react' (or switch to a direct ComponentProps import) so the type reference is valid in this module.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/components/ui/badge.tsx` at line 1, BadgeProps uses
React.ComponentProps but React is not imported in this module. Update the
imports in badge.tsx by adding a type-only React import, or switch BadgeProps to
use a direct ComponentProps import, so the type reference is available where
BadgeProps is defined.

Comment on lines 36 to 51
export function useFeedback(id: string) {
return useQuery<Feedback>({
queryKey: feedbackKeys.detail(id),
queryFn: () => feedbackClient.get<Feedback>(`/${id}`).then(r => r.data),
queryFn: () =>
mockOr(
() => {
const found = feedbackDetailsById[id]
const scoped = found ? scopeFeedback([found], getCurrentUser()) : []
if (!scoped[0]) throw new Error('Feedback not found')
return Promise.resolve(scoped[0])
},
() => feedbackClient.get<Feedback>(`/${id}`).then(r => r.data),
),
enabled: !!id,
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Missing staleTime on useFeedback detail query.

Every other single-record mock-aware query added in this PR (useMemberBalance, useTransaction in payments/api/queries.ts) sets staleTime: 30_000, and useFeedbackList in this same file does too. useFeedback is the outlier, so opening the same feedback detail sheet repeatedly will refetch (and briefly show the skeleton) every time instead of using cached data.

🩹 Proposed fix
 export function useFeedback(id: string) {
   return useQuery<Feedback>({
     queryKey: feedbackKeys.detail(id),
+    staleTime: 30_000,
     queryFn: () =>
       mockOr(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function useFeedback(id: string) {
return useQuery<Feedback>({
queryKey: feedbackKeys.detail(id),
queryFn: () => feedbackClient.get<Feedback>(`/${id}`).then(r => r.data),
queryFn: () =>
mockOr(
() => {
const found = feedbackDetailsById[id]
const scoped = found ? scopeFeedback([found], getCurrentUser()) : []
if (!scoped[0]) throw new Error('Feedback not found')
return Promise.resolve(scoped[0])
},
() => feedbackClient.get<Feedback>(`/${id}`).then(r => r.data),
),
enabled: !!id,
})
}
export function useFeedback(id: string) {
return useQuery<Feedback>({
queryKey: feedbackKeys.detail(id),
staleTime: 30_000,
queryFn: () =>
mockOr(
() => {
const found = feedbackDetailsById[id]
const scoped = found ? scopeFeedback([found], getCurrentUser()) : []
if (!scoped[0]) throw new Error('Feedback not found')
return Promise.resolve(scoped[0])
},
() => feedbackClient.get<Feedback>(`/${id}`).then(r => r.data),
),
enabled: !!id,
})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/features/feedback/api/queries.ts` around lines 36 - 51, The
`useFeedback` detail query is missing the same cache freshness window used by
other mock-aware queries, so it refetches too often and re-shows loading UI.
Update `useFeedback` in `queries.ts` to include `staleTime: 30_000`, matching
`useFeedbackList`, `useMemberBalance`, and `useTransaction`, while keeping the
existing `queryKey`, `queryFn`, and `enabled` behavior unchanged.

Comment on lines +14 to +23
export interface PaymentRow {
createdAt: string
date: string
title: string
rawDescription: string
description: string
amountFormatted: string
kind: PaymentKind
creatorName: string
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

PaymentRow drops the transaction id.

Unlike FeedbackRow (which keeps id: feedback.id), PaymentRow never carries the underlying Transaction.id through. As a result, PaymentsPage.tsx has to key its table rows with a fragile composite of date/description/array-index instead of a stable identifier — reordering rows via sort/filter can cause React to misattribute row identity across renders.

🩹 Proposed fix
 export interface PaymentRow {
+  id: string
   createdAt: string
   date: string
   title: string
     rows: sortPaymentRows(filterPaymentRows(memberTransactions.map((transaction) => ({
+      id: transaction.id,
       createdAt: transaction.created_at,
       date: formatDateShort(transaction.created_at),

PaymentsPage.tsx can then use key={transaction.id} instead of the composite key.

Also applies to: 102-113

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/features/payments/model/usePaymentsViewModel.ts` around lines
14 - 23, `PaymentRow` is missing the stable transaction identifier, which forces
`PaymentsPage.tsx` to rely on an unstable composite key. Update the `PaymentRow`
interface and the `usePaymentsViewModel` mapping so each row carries the
underlying `Transaction.id`, similar to how `FeedbackRow` preserves `id`. Then
adjust `PaymentsPage.tsx` to key rows with that `id` instead of the current
date/description/index composite.

Comment on lines +267 to 292
function EventsTableSkeleton() {
return (
<div className="border bg-card p-5">
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, index) => (
<Skeleton key={index} className="h-9 w-full" />
))}
</div>
</div>
)
}

function FieldLabel({ children }: { children: React.ReactNode }) {
return (
<p className="text-caption uppercase tracking-[0.1em] text-text-tertiary">{children}</p>
)
}

function Field({ label, value }: { label: string; value: string }) {
return (
<div>
<h1 className="font-display text-display-lg uppercase tracking-wide">Events</h1>
{hello && <p className="mt-2 text-sm text-muted-foreground">{hello}</p>}
<FieldLabel>{label}</FieldLabel>
<p className="mt-0.5 text-body-sm">{value}</p>
</div>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

FieldLabel/Field are duplicated verbatim across pages.

The same FieldLabel/Field implementations also appear in FeedbackPage.tsx and OrganizationPage.tsx (per cross-file evidence). Extracting these into a shared UI primitive (e.g. components/ui/field.tsx) would avoid three copies drifting apart as detail views evolve.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/features/sport-events/pages/SportEventsPage.tsx` around lines
267 - 292, The FieldLabel and Field components in SportEventsPage are duplicated
verbatim in other detail pages, so consolidate them into a shared UI primitive
and reuse it here. Move the shared FieldLabel/Field implementation into a common
component such as a field UI module, then update SportEventsPage, FeedbackPage,
and OrganizationPage to import the shared version instead of keeping local
copies.

Comment thread web-client/src/types.ts
Comment on lines +22 to 32
export type SportEvent = Omit<S['Event'], 'attendees' | 'creator' | 'teams_linked'> & {
attendees?: MemberRef[]
creator: MemberRef
teams_linked?: TeamRef[]
}
export type EventSummary = S['EventSummary'] & {
attendees?: MemberRef[]
teams_linked?: TeamRef[]
}
export type EventCreate = S['EventCreate']
export type EventPartialUpdate = S['EventPartialUpdate']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the generated OpenAPI schema type for EventSummary's attendees/teams_linked shape.
rg -n "EventSummary" --type=ts -g '!node_modules' -B2 -A10

Repository: AET-DevOps26/team-devoops

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== top-level files =="
git ls-files | sed -n '1,200p'

echo
echo "== likely schema/type files =="
fd -H -I -t f 'types.ts|schema|openapi|swagger|generated|client' . 2>/dev/null | sed -n '1,200p'

echo
echo "== search EventSummary and related fields =="
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
  "EventSummary|teams_linked|attendees|type S =|export type S|interface S" .

echo
echo "== inspect web-client/src/types.ts =="
sed -n '1,120p' web-client/src/types.ts

Repository: AET-DevOps26/team-devoops

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== api/openapi.yaml: EventSummary schema block =="
sed -n '1941,1995p' api/openapi.yaml

echo
echo "== web-client/src/types.ts =="
sed -n '1,80p' web-client/src/types.ts

echo
echo "== generated EventSummary models for comparison =="
sed -n '1,260p' services/spring-event/src/generated/java/tum/devoops/eventservice/model/EventSummary.java

Repository: AET-DevOps26/team-devoops

Length of output: 9275


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== api/openapi.yaml: Reference schema block =="
sed -n '1880,1940p' api/openapi.yaml

echo
echo "== web-client generated schema typings for Reference/EventSummary =="
rg -n --hidden --glob 'web-client/src/**' --glob '!**/node_modules/**' \
  "Reference:|EventSummary:|MemberRef|TeamRef" web-client/src

echo
echo "== web-client api schema type excerpts =="
sed -n '1,260p' web-client/src/api/client.ts

Repository: AET-DevOps26/team-devoops

Length of output: 3781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== web-client/src/api.ts: Reference and EventSummary =="
sed -n '620,900p' web-client/src/api.ts

echo
echo "== api/openapi.yaml: Reference schema block =="
rg -n -A20 -B5 "^    Reference:" api/openapi.yaml

Repository: AET-DevOps26/team-devoops

Length of output: 12257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== web-client fixture EventSummary shape =="
rg -n -A12 -B4 '"teams_linked"|"attendees"|"EventSummary"' web-client/src/mocks/fixtures/events.ts | sed -n '1,220p'

echo
echo "== web-client scope test references =="
sed -n '1,160p' web-client/src/mocks/scope.test.ts

echo
echo "== any EventSummary consumer expecting teams_linked =="
rg -n --hidden --glob 'web-client/src/**' "teams_linked.*EventSummary|EventSummary.*teams_linked|type EventSummary|EventSummary\[" web-client/src

Repository: AET-DevOps26/team-devoops

Length of output: 13161


Omit attendees before overriding EventSummary

S['EventSummary'] only defines attendees?: Reference[]; intersecting it with MemberRef[] forces both shapes at once, so the current EventSummary fixtures/consumers don’t match the generated schema. teams_linked isn’t part of S['EventSummary'], so the fix only needs to omit attendees before re-adding the resolved ref shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/types.ts` around lines 22 - 32, The EventSummary type
currently intersects S['EventSummary'] with a narrower attendees shape, which
leaves the original Reference[] requirement in place and causes schema
mismatches. Update EventSummary to omit attendees from S['EventSummary'] before
reintroducing the MemberRef[] version, and leave teams_linked as an added
optional field only if needed since it is not part of the generated summary
schema. Use the existing type aliases SportEvent and EventSummary as the locator
when making this change.

Comment thread web-client/src/types.ts
Comment on lines 95 to 102
export interface AuthUser {
id: string
name: string
email: string
// Membership roles from the Keycloak `member_roles` claim — a subset of
// 'Trainee' | 'Coach' | 'Director' | 'Admin'.
roles: string[]
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

AuthUser is missing role: Role, but every consumer reads .role.

This interface declares roles: string[], yet:

  • currentUser.ts builds tokenUser()/mockPersona() results and returns { id, name, email, role: tokenRole(...) } (no roles field at all).
  • scope.ts switches on user.role in every exported scope function (scopeFeedback, scopeTransactions, scopeBalances, scopeEvents, scopeMembers, scopeReport).
  • scope.test.ts builds users via MOCK_PERSONAS... satisfies Record<string, AuthUser> and then calls scope functions expecting .role to exist.
  • AppShell.tsx filters nav items with NAV_ITEMS.filter((item) => item.roles.includes(user.role)).

A function/object literal checked against a declared type undergoes excess-property and missing-required-property checking, so tokenUser()'s return statement (missing roles, adding unknown role) and any direct AuthUser object literals would fail to type-check as written. This is a pervasive cross-file contract break, not a local defect.

🛠️ Proposed fix
 export interface AuthUser {
   id: string
   name: string
   email: string
-  // Membership roles from the Keycloak `member_roles` claim — a subset of
-  // 'Trainee' | 'Coach' | 'Director' | 'Admin'.
-  roles: string[]
+  role: Role
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface AuthUser {
id: string
name: string
email: string
// Membership roles from the Keycloak `member_roles` claim — a subset of
// 'Trainee' | 'Coach' | 'Director' | 'Admin'.
roles: string[]
}
export interface AuthUser {
id: string
name: string
email: string
role: Role
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/src/types.ts` around lines 95 - 102, The AuthUser contract is
inconsistent: it declares roles[] but all consumers and builders use role, so
update AuthUser to include role: Role (and remove or reconcile roles if it is
unused) so tokenUser() and mockPersona() in currentUser.ts type-check,
scopeFeedback/scopeTransactions/scopeBalances/scopeEvents/scopeMembers/scopeReport
in scope.ts can continue switching on user.role, AppShell.tsx can filter
NAV_ITEMS by user.role, and the AuthUser fixtures in scope.test.ts satisfy the
corrected shape.

Comment thread web-client/vite.config.ts
Comment on lines +21 to +32
// Traefik strips the full `/api/v1/<name>` prefix, so duplicate the service
// segment to survive it; also trim a bare trailing slash the services 404 on.
rewrite: (p) =>
p
.replace(
/^(\/api\/v1\/(?:organization|members|events|feedback|finance|letters|helper))\/(\?|$)/,
'$1$2',
)
.replace(
/^\/api\/v1\/(organization|members|events|feedback|finance|letters|helper)(\/|$)/,
'/api/v1/$1/$1$2',
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Query-string-only requests bypass the duplication rewrite.

For a bare service call like /api/v1/organization?active=true (no trailing slash before ?), neither regex matches: the first requires a literal / before ?/end, and the second requires / or end-of-string immediately after the service name — ? satisfies neither. The path is passed through unrewritten, so it won't survive Traefik's prefix-stripping as described in the comment, and any list/filter endpoint called this way (a very common REST pattern) will likely 404 against the real backend.

🐛 Proposed fix
-        // Traefik strips the full `/api/v1/<name>` prefix, so duplicate the service
-        // segment to survive it; also trim a bare trailing slash the services 404 on.
-        rewrite: (p) =>
-          p
-            .replace(
-              /^(\/api\/v1\/(?:organization|members|events|feedback|finance|letters|helper))\/(\?|$)/,
-              '$1$2',
-            )
-            .replace(
-              /^\/api\/v1\/(organization|members|events|feedback|finance|letters|helper)(\/|$)/,
-              '/api/v1/$1/$1$2',
-            ),
+        // Traefik strips the full `/api/v1/<name>` prefix, so duplicate the service
+        // segment to survive it; also trim a bare trailing slash the services 404 on.
+        // Query strings are split off first so this works regardless of a trailing slash.
+        rewrite: (p) => {
+          const services = 'organization|members|events|feedback|finance|letters|helper'
+          const [rawPath, query = ''] = p.split(/(?=\?)/)
+          const path = rawPath.replace(new RegExp(`^(/api/v1/(?:${services}))/$`), '$1')
+          return (
+            path.replace(new RegExp(`^/api/v1/(${services})(/|$)`), '/api/v1/$1/$1$2') + query
+          )
+        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Traefik strips the full `/api/v1/<name>` prefix, so duplicate the service
// segment to survive it; also trim a bare trailing slash the services 404 on.
rewrite: (p) =>
p
.replace(
/^(\/api\/v1\/(?:organization|members|events|feedback|finance|letters|helper))\/(\?|$)/,
'$1$2',
)
.replace(
/^\/api\/v1\/(organization|members|events|feedback|finance|letters|helper)(\/|$)/,
'/api/v1/$1/$1$2',
),
// Traefik strips the full `/api/v1/<name>` prefix, so duplicate the service
// segment to survive it; also trim a bare trailing slash the services 404 on.
// Query strings are split off first so this works regardless of a trailing slash.
rewrite: (p) => {
const services = 'organization|members|events|feedback|finance|letters|helper'
const [rawPath, query = ''] = p.split(/(?=\?)/)
const path = rawPath.replace(new RegExp(`^(/api/v1/(?:${services}))/$`), '$1')
return (
path.replace(new RegExp(`^/api/v1/(${services})(/|$)`), '/api/v1/$1/$1$2') + query
)
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-client/vite.config.ts` around lines 21 - 32, The rewrite callback in the
Vite proxy config does not handle query-string-only service URLs like
/api/v1/organization?active=true, so those requests bypass the duplication logic
and can 404 after Traefik strips the prefix. Update the rewrite logic in the
rewrite function to treat a ? after the service name the same as a trailing
slash or end-of-path, so the service segment is duplicated before the query
string. Keep the existing behavior for bare service paths and preserve the
current service list symbols used in the regex.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Member role — pages + role-aware foundation

2 participants