Skip to content

AdminCustomersPage: founder view of all teams + tier/promo actions#45

Merged
mastermanas805 merged 1 commit into
mainfrom
feat/admin-customers-ui-fresh
May 13, 2026
Merged

AdminCustomersPage: founder view of all teams + tier/promo actions#45
mastermanas805 merged 1 commit into
mainfrom
feat/admin-customers-ui-fresh

Conversation

@mastermanas805
Copy link
Copy Markdown
Member

Summary

  • New /app/admin/customers page — gated on ctx.me.is_platform_admin (new field on AuthMeResponse). Non-admin users redirect to root (404-equivalent, not 403), so the route's existence isn't leaked. Sidebar link is also conditional.
  • Table with sortable headers (default MRR desc), filter pills (All / Anonymous / Free / Hobby / Pro / Team), and a search box.
  • Row click opens CustomerDetailDrawer — a right-side slide-in with four tabs (Overview / Resources / Activity / Promos) so the operator never loses list context.
  • Two write actions live in the drawer:
    • Promote / demote tier — typed PROMOTE or DEMOTE confirmation, derived from the tier rank delta. Reason field is required and lands in the audit log.
    • Issue promo — kind (percent_off / first_month_free / amount_off), value, applies_to, valid_for_days. On success, the modal flips to "Issued" state with a copy button on the generated code.

Backend contract assumed (Track A)

The dashboard talks to four new endpoints. Where Track A's shape differs from these assumptions, the adapter in src/api/index.ts is the single place to fix:

  • GET /api/v1/admin/customers?q=&tier=&sort_by=&limit=&offset={ ok, customers: AdminCustomerSummary[], total }
  • GET /api/v1/admin/customers/:team_id{ ok, team, users, resources, audit_log[], deploys, subscription, promos? }
  • POST /api/v1/admin/customers/:team_id/tier{ tier, reason }{ ok, team }
  • POST /api/v1/admin/customers/:team_id/promo{ kind, value, applies_to, valid_for_days }{ ok, code, expires_at }
  • /auth/me carries a new is_platform_admin?: boolean flag.

Fields where I made an assumption Track A might not match exactly:

  • AdminCustomerSummary.mrr_monthly / mrr_yearly — assumed INR paise (×100), formatted via Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' }). If Track A returns rupees or USD cents, only formatINRCompact in AdminCustomersPage.tsx and formatINR in the drawer need to change.
  • AdminCustomerSummary.name — marked optional because some old teams have no display name; the drawer falls back to team.display_name / team.name / summary.name in that order.
  • AdminCustomerDetailResponse.promos is optional — the page degrades to an empty list when the field isn't present.
  • subscription may be null for unpaid teams (rendered as "none").
  • Sort is currently client-side (result set is small per the brief; Track A clamps to 200). The sort_by query param is sent but the page re-sorts locally for instant header-click feedback.

Test plan

  • npm test — 365 passed / 3 skipped (was 342, +23 new in AdminCustomersPage.test.tsx)
  • npm run build — clean; page lazy-chunked to ~23 KB (gzip 6.4 KB)
  • Manual smoke once Track A is live: log in as a platform admin, verify the link appears, page renders, drawer opens, tier change + promo modals POST and refetch.
  • Verify non-admin users 404 on the deep link (currently mocked in tests; need to confirm with a real non-admin /auth/me response).

🤖 Generated with Claude Code

New /app/admin/customers console for platform admins. Gated on the new
is_platform_admin flag on AuthMeResponse — non-admin users redirect to
root (404-equivalent) instead of seeing a 403, and the sidebar link is
suppressed entirely.

Page surfaces:
- Filter pills (All / Anonymous / Free / Hobby / Pro / Team) + search
- Sortable table: Email · Tier · MRR · Storage · Deploys · Last active · Signed up
- Row click opens CustomerDetailDrawer (right-side slide-in)
- Drawer tabs: Overview / Resources / Activity / Promos
- "Issue promo" modal — kind, value, applies_to, valid_for_days; surfaces
  the generated code with a Copy button
- "Promote / demote tier" modal — requires typed PROMOTE / DEMOTE
  confirmation, computed from current → target tier rank

Backend contract assumed (Track A):
- GET  /api/v1/admin/customers
- GET  /api/v1/admin/customers/:team_id
- POST /api/v1/admin/customers/:team_id/tier
- POST /api/v1/admin/customers/:team_id/promo

23 new tests covering route gating, sorting, filtering, drawer tabs,
promo issuance, and tier-change confirmation. Full suite: 365 passed,
3 skipped (was 342 + 23 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit 9d6609a into main May 13, 2026
1 of 2 checks passed
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.

1 participant