Skip to content

feat: Administrative Dashboard with Real-time Metrics#20

Open
Feyisara2108 wants to merge 23 commits into
Kolo-Org:mainfrom
Feyisara2108:feature/admin-dashboard
Open

feat: Administrative Dashboard with Real-time Metrics#20
Feyisara2108 wants to merge 23 commits into
Kolo-Org:mainfrom
Feyisara2108:feature/admin-dashboard

Conversation

@Feyisara2108

@Feyisara2108 Feyisara2108 commented Jul 21, 2026

Copy link
Copy Markdown

Summary

Closes #13

Implements the Administrative Dashboard with real-time metrics. .

A guarded /admin route that renders only for authenttform metrics, complex data tables, charts, andactivity logs.

What's included

Admin layout

  • /admin shell with sidebar + topbar and tabbed navi
  • Six sections: Overview, User Management, Savings Groups, Transaction Analytics, Smart Contract Health, Activity Log.

Data tables (@tanstack/react-table)

  • Reusable DataTable with **sorting, global filterin
  • Powers the User Management and Transaction Analytics views.

Charts (Recharts)

  • Total value saved (area), transaction volume (bar), y-status (donut).

Security / authorization

  • Follows the Next.js 16 authentication guide: a Data Access Layer (requireAdmin) performs the secure server-side role check close
    to the data, so admin UI never renders for non-admins.
  • Optimistic route pre-filter via proxy.ts (Next.js 16 renamed Middleware → Proxy).
  • Authorization logic verified end-to-end: no session r → forbidden; admin → dashboard.

Testing

  • Vitest set up (CI test job now runs instead of skipping).
  • Unit tests for the table **sorting, filtering, and paole predicates. 28 tests passing.

Notes for reviewers

  • There is no backend/auth service yet, so this ships getAdminDashboardData() (deterministic mock data) and a mock session provider. Wiring a real API/auth only touches src/lib/admin/mock-data.ts and src/lib/auth/session.ts — the return
    shapes are the contract.
  • A demo login (/login) provides one-click admin/member sessions so the authorization flow is demonstrable.
  • The shared currency / date / address formatterart of this work.

Verification

  • npm run lint — passes (0 errors)
  • npm run typecheck — passes
  • npm run format — passes
  • npm run build — passes
  • npm test — 28 passing

Summary by CodeRabbit

  • New Features
    • Added an admin dashboard with overview metrics, charts, user and group management, transaction analytics, contract health, and activity logs.
    • Added demo sign-in options for admin and member accounts, with protected route access and sign-out support.
    • Added searchable, sortable, paginated data tables.
    • Improved currency, date, number, and address formatting.
  • Bug Fixes
    • Unauthorized access to protected areas now redirects to login with the requested destination preserved.
  • Tests
    • Added automated coverage for authentication roles and table sorting, filtering, and pagination.

@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

@Feyisara2108 is attempting to deploy a commit to the omodunni241-4260's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a protected demo admin dashboard with typed mock data, charts, searchable tables, authentication actions, route guards, shared formatting/UI components, and Vitest configuration plus unit tests.

Changes

Admin dashboard

Layer / File(s) Summary
Authentication and route protection
src/lib/auth/*, src/proxy.ts, src/app/(auth)/login/page.tsx, src/app/(admin)/admin/page.tsx
Adds demo sessions, role checks, protected route redirects, admin authorization, login controls, and the server-rendered dashboard entry point.
Dashboard contracts and data
src/types/admin.ts, src/lib/admin/mock-data.ts
Defines dashboard domain types and assembles deterministic mock users, groups, transactions, contract health, activity, metrics, and chart data.
Table behavior and validation
src/lib/admin/table-utils.ts, src/lib/admin/table-utils.test.ts, src/components/admin/DataTable.tsx
Adds shared sorting, filtering, pagination, TanStack integration, and unit tests for table behavior.
Dashboard sections and presentation
src/components/admin/*, src/utils/formatters/*
Adds the tabbed admin shell, KPI cards, charts, tables, activity and contract sections, status badges, cards, and display formatters.
Test and build configuration
package.json, vitest.config.ts, vitest.setup.ts
Adds formatting, testing, typechecking scripts and the React/Vitest testing toolchain.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant Proxy
  participant AuthDAL
  participant Session
  participant AdminPage
  participant AdminShell
  Browser->>Proxy: Request /admin
  Proxy->>Browser: Redirect to /login if no session cookie
  Browser->>AuthDAL: Load protected admin page
  AuthDAL->>Session: Resolve session user
  AuthDAL->>AdminPage: Return authorized admin
  AdminPage->>AdminShell: Pass user and dashboard data
  AdminShell->>Browser: Render dashboard sections
Loading

Possibly related PRs

  • Kolo-Org/Frontend#19: Also changes the login page component and may overlap with the demo login implementation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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: an administrative dashboard with metrics and related UI work.
Linked Issues check ✅ Passed The PR implements the admin dashboard, tabbed sections, TanStack tables, authorization, and table tests required by issue #13.
Out of Scope Changes check ✅ Passed No clearly unrelated changes stand out; the added tooling, auth, charts, and utilities all support the dashboard work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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

@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: 7

🤖 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 `@src/components/admin/sections/GroupsSection.tsx`:
- Around line 75-83: Update the GroupsSection data flow so the “Active Savings
Groups” title matches its contents: filter groups to records with status ===
"active" before calculating the displayed count and passing data to DataTable.
Preserve the existing columns and table behavior for the filtered collection.

In `@src/lib/admin/table-utils.test.ts`:
- Around line 41-45: Extend the sorting tests around compareValues and sortRows
to cover descending order with nullish values, verifying they remain last after
direction inversion. Update sortRows so nullish comparisons are handled outside
the ascending/descending result negation, while non-null values continue to
reverse normally.

In `@src/lib/admin/table-utils.ts`:
- Around line 42-45: Update the sorting logic around compareValues and the
direction handling so nullish values remain at the end for both ascending and
descending sorts. Apply the descending reversal only to comparisons between
non-nullish values, while preserving the existing accessor-based sorting and
copied rows behavior.

In `@src/lib/auth/actions.ts`:
- Around line 17-30: Restrict signInAsAdmin and its predictable setSession-based
admin cookie issuance to non-production builds, and prevent the function from
creating or redirecting with an admin session in production. Before release,
replace the raw user ID cookie flow in setSession with credential verification
and an integrity-protected server-side session, while preserving the existing
demo behavior only for non-production environments.

In `@src/lib/auth/session.ts`:
- Around line 18-22: Replace the client-controlled ID lookup in readSessionUser
with server-verifiable session handling: validate a signed/encrypted session or
resolve an opaque random token through trusted server-side storage before
returning an AuthUser. Do not use the cookie value directly as a MOCK_USERS key,
and preserve the null result for missing or invalid sessions.

In `@src/utils/formatters/address.ts`:
- Around line 8-10: The formatAddress length guard currently expands some short
addresses because the ellipsis output is longer than the input. Update
formatAddress to leave the address unchanged unless its length exceeds the full
formatted output length, including both sliced portions and the ellipsis;
preserve the existing truncation behavior for longer addresses.

In `@src/utils/formatters/currency.ts`:
- Around line 7-12: Update formatCurrency to remove the fixed
maximumFractionDigits: 2 setting, allowing Intl.NumberFormat to preserve each
currency’s default minor-unit precision. Keep the existing locale, currency
option, and amount formatting behavior unchanged.
🪄 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: f0a4c13e-a7f0-480e-ad79-8d6ce254f395

📥 Commits

Reviewing files that changed from the base of the PR and between e36aca2 and e2600ac.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (37)
  • package.json
  • src/app/(admin)/admin/page.tsx
  • src/app/(auth)/login/page.tsx
  • src/components/admin/AdminShell.tsx
  • src/components/admin/DataTable.tsx
  • src/components/admin/charts/GroupStatusChart.tsx
  • src/components/admin/charts/SavingsGrowthChart.tsx
  • src/components/admin/charts/TransactionVolumeChart.tsx
  • src/components/admin/charts/UserGrowthChart.tsx
  • src/components/admin/sections/ActivitySection.tsx
  • src/components/admin/sections/ContractHealthSection.tsx
  • src/components/admin/sections/GroupsSection.tsx
  • src/components/admin/sections/OverviewSection.tsx
  • src/components/admin/sections/TransactionsSection.tsx
  • src/components/admin/sections/UsersSection.tsx
  • src/components/admin/ui/MetricCard.tsx
  • src/components/admin/ui/SectionCard.tsx
  • src/components/admin/ui/StatusBadge.tsx
  • src/lib/admin/mock-data.ts
  • src/lib/admin/table-utils.test.ts
  • src/lib/admin/table-utils.ts
  • src/lib/auth/actions.ts
  • src/lib/auth/constants.ts
  • src/lib/auth/dal.ts
  • src/lib/auth/mock-users.ts
  • src/lib/auth/roles.test.ts
  • src/lib/auth/roles.ts
  • src/lib/auth/session.ts
  • src/middleware.ts
  • src/proxy.ts
  • src/types/admin.ts
  • src/types/auth.ts
  • src/utils/formatters/address.ts
  • src/utils/formatters/currency.ts
  • src/utils/formatters/date.ts
  • vitest.config.ts
  • vitest.setup.ts
💤 Files with no reviewable changes (1)
  • src/middleware.ts

Comment on lines +75 to +83
<SectionCard
title="Active Savings Groups"
description={`${groups.length} community savings groups`}
>
<DataTable
data={groups}
columns={columns}
searchPlaceholder="Search by group name…"
initialPageSize={8}

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

Filter to active groups or rename this table.

This table includes forming, completed, and paused records, but its title and the PR objective say “Active Savings Groups.” Filter groups to status === "active" before calculating the count and passing data, or rename the section to “Savings Groups.”

🤖 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 `@src/components/admin/sections/GroupsSection.tsx` around lines 75 - 83, Update
the GroupsSection data flow so the “Active Savings Groups” title matches its
contents: filter groups to records with status === "active" before calculating
the displayed count and passing data to DataTable. Preserve the existing columns
and table behavior for the filtered collection.

Comment on lines +41 to +45
it("sorts nullish values last regardless of direction", () => {
expect(compareValues(null, 5)).toBeGreaterThan(0);
expect(compareValues(5, null)).toBeLessThan(0);
expect(compareValues(undefined, undefined)).toBe(0);
});

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

Exercise the claimed null-last behavior in descending sorts.

This calls compareValues, which has no direction. sortRows(..., "desc") negates its result, so nullish values sort first. Add a descending sortRows case and keep nullish ordering outside the direction inversion.

🤖 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 `@src/lib/admin/table-utils.test.ts` around lines 41 - 45, Extend the sorting
tests around compareValues and sortRows to cover descending order with nullish
values, verifying they remain last after direction inversion. Update sortRows so
nullish comparisons are handled outside the ascending/descending result
negation, while non-null values continue to reverse normally.

Comment on lines +42 to +45
const factor = direction === "desc" ? -1 : 1;
return [...rows].sort(
(a, b) => compareValues(accessor(a), accessor(b)) * factor,
);

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

Keep nullish values last when sorting descending.

Multiplying the full comparator by -1 moves null/undefined to the top for descending order, despite the documented “sorted last” contract.

Proposed fix
   const factor = direction === "desc" ? -1 : 1;
-  return [...rows].sort(
-    (a, b) => compareValues(accessor(a), accessor(b)) * factor,
-  );
+  return [...rows].sort((a, b) => {
+    const aValue = accessor(a);
+    const bValue = accessor(b);
+    const comparison = compareValues(aValue, bValue);
+
+    return aValue == null || bValue == null
+      ? comparison
+      : comparison * factor;
+  });
📝 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
const factor = direction === "desc" ? -1 : 1;
return [...rows].sort(
(a, b) => compareValues(accessor(a), accessor(b)) * factor,
);
const factor = direction === "desc" ? -1 : 1;
return [...rows].sort((a, b) => {
const aValue = accessor(a);
const bValue = accessor(b);
const comparison = compareValues(aValue, bValue);
return aValue == null || bValue == null
? comparison
: comparison * factor;
});
🤖 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 `@src/lib/admin/table-utils.ts` around lines 42 - 45, Update the sorting logic
around compareValues and the direction handling so nullish values remain at the
end for both ascending and descending sorts. Apply the descending reversal only
to comparisons between non-nullish values, while preserving the existing
accessor-based sorting and copied rows behavior.

Comment thread src/lib/auth/actions.ts
Comment on lines +17 to +30
async function setSession(userId: string): Promise<void> {
const store = await cookies();
store.set(SESSION_COOKIE, userId, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 7,
});
}

export async function signInAsAdmin(): Promise<void> {
await setSession(DEMO_ADMIN_ID);
redirect("/admin");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Do not expose an unauthenticated admin-session issuer.

signInAsAdmin needs no proof of identity and writes a predictable raw user ID. Any visitor can obtain or forge an admin session, defeating the /admin guard. Restrict demo auth to non-production builds and replace this with credential verification plus an integrity-protected server session before release.

🤖 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 `@src/lib/auth/actions.ts` around lines 17 - 30, Restrict signInAsAdmin and its
predictable setSession-based admin cookie issuance to non-production builds, and
prevent the function from creating or redirecting with an admin session in
production. Before release, replace the raw user ID cookie flow in setSession
with credential verification and an integrity-protected server-side session,
while preserving the existing demo behavior only for non-production
environments.

Comment thread src/lib/auth/session.ts
Comment on lines +18 to +22
export async function readSessionUser(): Promise<AuthUser | null> {
const store = await cookies();
const id = store.get(SESSION_COOKIE)?.value;
if (!id) return null;
return MOCK_USERS[id] ?? null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Do not trust a client-supplied user ID as the session.

Any client can set kolo_session=admin-1; this function will then authorize them as admin. Use a signed/encrypted session or an opaque random token resolved server-side. HttpOnly alone does not prevent users from replacing a cookie value.

🤖 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 `@src/lib/auth/session.ts` around lines 18 - 22, Replace the client-controlled
ID lookup in readSessionUser with server-verifiable session handling: validate a
signed/encrypted session or resolve an opaque random token through trusted
server-side storage before returning an AuthUser. Do not use the cookie value
directly as a MOCK_USERS key, and preserve the null result for missing or
invalid sessions.

Comment on lines +8 to +10
export const formatAddress = (address: string, chars = 4): string => {
if (address.length <= chars * 2 + 1) return address;
return `${address.slice(0, chars)}...${address.slice(-chars)}`;

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

Do not expand short addresses.

For chars = 4, a 10-character address becomes an 11-character display string. Keep strings unchanged unless they exceed the formatted output length.

Proposed fix
 export const formatAddress = (address: string, chars = 4): string => {
-  if (address.length <= chars * 2 + 1) return address;
+  if (address.length <= chars * 2 + 3) return address;
   return `${address.slice(0, chars)}...${address.slice(-chars)}`;
 };
📝 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 const formatAddress = (address: string, chars = 4): string => {
if (address.length <= chars * 2 + 1) return address;
return `${address.slice(0, chars)}...${address.slice(-chars)}`;
export const formatAddress = (address: string, chars = 4): string => {
if (address.length <= chars * 2 + 3) return address;
return `${address.slice(0, chars)}...${address.slice(-chars)}`;
};
🤖 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 `@src/utils/formatters/address.ts` around lines 8 - 10, The formatAddress
length guard currently expands some short addresses because the ellipsis output
is longer than the input. Update formatAddress to leave the address unchanged
unless its length exceeds the full formatted output length, including both
sliced portions and the ellipsis; preserve the existing truncation behavior for
longer addresses.

Comment on lines +7 to +12
export const formatCurrency = (amount: number, currency = "USD"): string =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency,
maximumFractionDigits: 2,
}).format(amount);

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
node <<'NODE'
for (const currency of ["USD", "JPY", "BHD"]) {
  const standard = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency,
  }).format(1.234);
  const forced = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency,
    maximumFractionDigits: 2,
  }).format(1.234);
  console.log({ currency, standard, forced });
}
NODE

Repository: Kolo-Org/Frontend

Length of output: 327


Preserve each currency’s minor-unit precision. maximumFractionDigits: 2 forces the wrong display for currencies like JPY and BHD; let Intl.NumberFormat use the currency’s default fraction digits unless callers can override it.

Proposed fix
 export const formatCurrency = (amount: number, currency = "USD"): string =>
   new Intl.NumberFormat("en-US", {
     style: "currency",
     currency,
-    maximumFractionDigits: 2,
   }).format(amount);
📝 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 const formatCurrency = (amount: number, currency = "USD"): string =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency,
maximumFractionDigits: 2,
}).format(amount);
export const formatCurrency = (amount: number, currency = "USD"): string =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(amount);
🤖 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 `@src/utils/formatters/currency.ts` around lines 7 - 12, Update formatCurrency
to remove the fixed maximumFractionDigits: 2 setting, allowing Intl.NumberFormat
to preserve each currency’s default minor-unit precision. Keep the existing
locale, currency option, and amount formatting behavior unchanged.

@Queenode

Copy link
Copy Markdown
Contributor

Great work @Feyisara2108 . thank you for your contribution.

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.

[UI/UX] Build Administrative Dashboard with Real-time Metrics

2 participants