feat: Administrative Dashboard with Real-time Metrics#20
Conversation
|
@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. |
📝 WalkthroughWalkthroughAdds 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. ChangesAdmin dashboard
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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
ESLint install timed out. The project may have too many dependencies for the sandbox. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (37)
package.jsonsrc/app/(admin)/admin/page.tsxsrc/app/(auth)/login/page.tsxsrc/components/admin/AdminShell.tsxsrc/components/admin/DataTable.tsxsrc/components/admin/charts/GroupStatusChart.tsxsrc/components/admin/charts/SavingsGrowthChart.tsxsrc/components/admin/charts/TransactionVolumeChart.tsxsrc/components/admin/charts/UserGrowthChart.tsxsrc/components/admin/sections/ActivitySection.tsxsrc/components/admin/sections/ContractHealthSection.tsxsrc/components/admin/sections/GroupsSection.tsxsrc/components/admin/sections/OverviewSection.tsxsrc/components/admin/sections/TransactionsSection.tsxsrc/components/admin/sections/UsersSection.tsxsrc/components/admin/ui/MetricCard.tsxsrc/components/admin/ui/SectionCard.tsxsrc/components/admin/ui/StatusBadge.tsxsrc/lib/admin/mock-data.tssrc/lib/admin/table-utils.test.tssrc/lib/admin/table-utils.tssrc/lib/auth/actions.tssrc/lib/auth/constants.tssrc/lib/auth/dal.tssrc/lib/auth/mock-users.tssrc/lib/auth/roles.test.tssrc/lib/auth/roles.tssrc/lib/auth/session.tssrc/middleware.tssrc/proxy.tssrc/types/admin.tssrc/types/auth.tssrc/utils/formatters/address.tssrc/utils/formatters/currency.tssrc/utils/formatters/date.tsvitest.config.tsvitest.setup.ts
💤 Files with no reviewable changes (1)
- src/middleware.ts
| <SectionCard | ||
| title="Active Savings Groups" | ||
| description={`${groups.length} community savings groups`} | ||
| > | ||
| <DataTable | ||
| data={groups} | ||
| columns={columns} | ||
| searchPlaceholder="Search by group name…" | ||
| initialPageSize={8} |
There was a problem hiding this comment.
🎯 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.
| 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); | ||
| }); |
There was a problem hiding this comment.
🎯 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.
| const factor = direction === "desc" ? -1 : 1; | ||
| return [...rows].sort( | ||
| (a, b) => compareValues(accessor(a), accessor(b)) * factor, | ||
| ); |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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"); |
There was a problem hiding this comment.
🔒 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.
| 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; |
There was a problem hiding this comment.
🔒 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.
| export const formatAddress = (address: string, chars = 4): string => { | ||
| if (address.length <= chars * 2 + 1) return address; | ||
| return `${address.slice(0, chars)}...${address.slice(-chars)}`; |
There was a problem hiding this comment.
🎯 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.
| 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.
| export const formatCurrency = (amount: number, currency = "USD"): string => | ||
| new Intl.NumberFormat("en-US", { | ||
| style: "currency", | ||
| currency, | ||
| maximumFractionDigits: 2, | ||
| }).format(amount); |
There was a problem hiding this comment.
🎯 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 });
}
NODERepository: 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.
| 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.
|
Great work @Feyisara2108 . thank you for your contribution. |
Summary
Closes #13
Implements the Administrative Dashboard with real-time metrics. .
A guarded
/adminroute that renders only for authenttform metrics, complex data tables, charts, andactivity logs.What's included
Admin layout
/adminshell with sidebar + topbar and tabbed naviData tables (
@tanstack/react-table)DataTablewith **sorting, global filterinCharts (Recharts)
Security / authorization
requireAdmin) performs the secure server-side role check closeto the data, so admin UI never renders for non-admins.
proxy.ts(Next.js 16 renamed Middleware → Proxy).Testing
Notes for reviewers
getAdminDashboardData()(deterministic mock data) and a mock session provider. Wiring a real API/auth only touchessrc/lib/admin/mock-data.tsandsrc/lib/auth/session.ts— the returnshapes are the contract.
/login) provides one-click admin/member sessions so the authorization flow is demonstrable.currency/date/addressformatterart of this work.Verification
npm run lint— passes (0 errors)npm run typecheck— passesnpm run format— passesnpm run build— passesnpm test— 28 passingSummary by CodeRabbit