upgrade captain dashboard#67
Conversation
dhamariT
commented
Mar 12, 2026
- feat: add captain dashboard and team pages with permissions
- refactor: enhance captain dashboard link with WIP indicator
- copilot suggested changes
- redundant profile card use + crew
- pretty
- fix: update admin page redirect logic for captain role
- feat: enhance ship certifications and captain dashboard functionality
- feat: add spot check leaderboard to admin page
- feat: enhance captain team functionality and activity tracking
- fix: update returned certification labels in CertsView component
- chore: add .gitignore file to exclude IDE/editor files
- refactor: improve code formatting and readability across multiple components
- refactor: reorganize User model and enhance PayoutReq and Session models
- chore: add migration for captain role permissions and db schema sync
- refactor: update skills handling and logging for JSON compatibility
- refactor: standardize JSON handling for skills and decisions across routes
- Implemented a new Captain dashboard accessible to users with the 'captain_dashboard' permission. - Created a Captain team page for team management, including a placeholder for future features. - Updated admin page to redirect captains to their dashboard. - Added API endpoint for fetching dashboard data, including reviewed certifications and backlog counts. - Introduced new permission 'captain_dashboard' in the permissions module.
- Wrapped the Captain dashboard link in a relative div to include a WIP (Work In Progress) component. - Improved the styling of the link for better visual consistency.
- Simplified the redirect condition for captains by removing the explicit role check, relying solely on the permission check for the captain dashboard.
- Added support for viewing certifications returned by admin, including a new count for returned certifications. - Updated the admin page to conditionally redirect based on user permissions and returned status. - Enhanced the CertsView component to display returned certifications with appropriate labels and reasons. - Modified API endpoints to handle filtering for returned certifications. - Improved the overall logic for fetching and displaying certification data based on user roles and permissions.
- Introduced a new SpotCheckLeaderboard component to display top spot checkers. - Updated the admin page to include the leaderboard section with appropriate loading states. - Enhanced the API to return top checkers data for the leaderboard.
- Added new API endpoints for fetching team member activity and team list. - Implemented a detailed team member page displaying activity metrics, including reviews and spot checks. - Introduced a review activity grid and charts for visualizing member performance over time. - Updated the captain dashboard to include links to team member details and improved navigation. - Enhanced the SpotCheck model with an additional index for better query performance.
- Modified the display of returned certification status to include the name of the admin who returned it, defaulting to 'admin' if not specified. - Adjusted the rendering logic to show return reasons only when available, improving clarity in the CertsView component.
- Created a .gitignore file to prevent IDE-specific files, such as the .idea directory, from being tracked in the repository.
…ponents - Adjusted formatting in CaptainPage, ReviewActivityGrid, and CertsView components for better readability. - Enhanced the layout of JSX elements and improved the structure of return statements in various functions. - Ensured consistent use of line breaks and indentation in the codebase.
- Reformatted the User model for improved readability and consistency, including adjustments to field order and types. - Enhanced the PayoutReq model by adding an index for adminId and ensuring proper relation definitions. - Updated the Session model to include an index for userId and improved the structure of the user relation. - Made minor adjustments to the Assignment model for clarity in field definitions.
- Changed the way user skills are processed by parsing skills from JSON strings in the assignments and skills routes. - Updated the logging function to serialize metadata, request, and response bodies and headers to JSON format, ensuring consistent data handling.
…outes - Updated skills handling in the assignments and users routes to ensure skills are stored as JSON strings. - Modified the ysws_reviews routes to consistently parse and stringify decisions, improving data integrity. - Enhanced the refresh route to handle existing decisions as JSON, ensuring compatibility with the updated structure.
There was a problem hiding this comment.
Pull request overview
Adds and upgrades captain-focused admin experiences (dashboard + team activity views) and enhances review/certification/spot-check admin tooling, alongside a DB/schema refactor that stores several previously-Json fields as JSON-encoded LongText strings.
Changes:
- Added captain team list + per-member activity endpoints and UI (weekly/day activity, project type breakdown).
- Added returned-by-admin triage flow for ship certifications (filters, stats, and UI labeling), plus spot-check leaderboard.
- Refactored Prisma schema + migrations to store various structured fields as JSON strings (skills, sys logs, YSWS review blobs, metrics output) and updated some routes accordingly.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| sw-dash/src/lib/log.ts | Writes SysLog metadata/req/res/changes as JSON strings. |
| sw-dash/src/lib/certs.ts | Adds returnedOnly filter + adjusts pending/queue stats to exclude returned-by-admin items. |
| sw-dash/src/lib/captain.ts | New captain analytics queries for member activity + team list. |
| sw-dash/src/app/api/admin/ysws_reviews/[id]/route.ts | Updates YSWS decisions persistence to JSON-string storage. |
| sw-dash/src/app/api/admin/ysws_reviews/[id]/refresh/route.ts | Updates YSWS devlogs/commits/decisions persistence to JSON-string storage. |
| sw-dash/src/app/api/admin/users/[id]/skills/route.ts | Writes User.skills as JSON string. |
| sw-dash/src/app/api/admin/spot_checks/stats/route.ts | Adds “top checkers” aggregation to spot-check stats response. |
| sw-dash/src/app/api/admin/skills/route.ts | Reads/writes User.skills via JSON parse/stringify. |
| sw-dash/src/app/api/admin/ship_certifications/route.ts | Adds returned-by-admin access control + query parameter handling. |
| sw-dash/src/app/api/admin/ship_certifications/[id]/route.ts | Adjusts assignment shape in cert detail response (aligning with schema). |
| sw-dash/src/app/api/admin/captain/team/route.ts | New captain team list API with caching. |
| sw-dash/src/app/api/admin/captain/team/[userId]/route.ts | New captain team member activity API with caching. |
| sw-dash/src/app/api/admin/captain/dashboard/route.ts | Adds returned-by-admin pending count to dashboard payload. |
| sw-dash/src/app/api/admin/assignments/route.ts | Parses User.skills as JSON string for reviewer matching. |
| sw-dash/src/app/admin/spot_checks/spot-check-leaderboard.tsx | New client component rendering top checkers. |
| sw-dash/src/app/admin/spot_checks/page.tsx | Adds leaderboard section to spot checks page. |
| sw-dash/src/app/admin/ship_certifications/page.tsx | Adds returned-by-admin view support and navigation tweaks. |
| sw-dash/src/app/admin/ship_certifications/certs-view.tsx | Adds returned-by-admin labels and wiring for returned-only view. |
| sw-dash/src/app/admin/captain/team/page.tsx | Captain team list UI (90-day activity). |
| sw-dash/src/app/admin/captain/team/[userId]/reviews-chart.tsx | New charts for member review/spot-check activity. |
| sw-dash/src/app/admin/captain/team/[userId]/page.tsx | Captain team member detail UI. |
| sw-dash/src/app/admin/captain/team/[userId]/activity-grid.tsx | GitHub-style activity grid component. |
| sw-dash/src/app/admin/captain/page.tsx | Links reviewer rows to team pages + shows returned-by-admin count card. |
| sw-dash/prisma/schema.prisma | Major schema updates: JSON fields moved to String/LongText; new spot check session tables; relationship tweaks. |
| sw-dash/prisma/migrations/20250312000000_captain_role_permissions/migration.sql | Migration aligning DB types/tables with captain/JSON-string refactor. |
| sw-dash/prisma/migrations/20250116010641_remove_unused_hire_fields/migration.sql | Replaced with a placeholder no-op migration file. |
| .gitignore | Ignores JetBrains .idea/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| if (selectedTypes.length > 0) p.set('type', selectedTypes.join(',')) | ||
| if (ftType !== 'all') p.set('ftType', ftType) | ||
| if (status !== 'all') p.set('status', status) | ||
| if (isReturnedView) p.set('returned', '1') | ||
| p.set('sortBy', sortBy) |
There was a problem hiding this comment.
returnedOnly is documented/implemented as a pending-only view, but in returned view the Status dropdown still allows switching to approved/rejected/all while still sending returned=1. Since the backend only applies returnedOnly filtering when status === 'pending', this can yield confusing results ("returned" view showing non-returned items). Consider forcing status to pending and disabling/hiding the status selector when isReturnedView is true.
| metadata: data.meta ? JSON.stringify(data.meta) : null, | ||
| reqMethod: data.req?.method, | ||
| reqUrl: data.req?.url, | ||
| reqBody: data.req?.body as Prisma.InputJsonValue, | ||
| reqHeaders: data.req?.headers as Prisma.InputJsonValue, | ||
| reqBody: data.req?.body !== undefined ? JSON.stringify(data.req.body) : null, | ||
| reqHeaders: data.req?.headers ? JSON.stringify(data.req.headers) : null, |
There was a problem hiding this comment.
These SysLog fields are now being persisted as JSON strings (per schema/migration), but existing log readers/serializers still expect objects in places. Please make sure all SysLog read paths JSON.parse metadata/req/res/changes before spreading/inspecting them, ideally via a shared safe-parse helper.
| sessionToken String? | ||
| currentChallenge String? | ||
| staffNotes String? @db.Text | ||
| skills String? @db.LongText |
There was a problem hiding this comment.
User.skills has been changed from Json? to String? (JSON string). There are still codepaths that treat skills as a string[] directly (e.g. casting from Prisma results) which will now be a plain string and can break UI/logic. Ensure all reads/writes consistently JSON.parse/JSON.stringify this field (and add a fallback for invalid JSON).
| skills String? @db.LongText | |
| skills Json? |
| devlogs String? @db.LongText | ||
| commits String? @db.LongText | ||
| decisions String? @db.LongText |
There was a problem hiding this comment.
YswsReview.devlogs/commits/decisions are now String? (JSON strings). Any code that previously treated these as arrays/objects (e.g. iterating without parsing) will now break at runtime. Please audit all YswsReview consumers to JSON.parse on read and JSON.stringify on write (prefer shared helpers with safe fallbacks).
| metadata String? @db.LongText | ||
| severity String? @default("info") | ||
| targetId Int? | ||
| targetType String? | ||
| metadata Json? | ||
| severity String? @default("info") // not used anymore, leaving for old logs | ||
|
|
||
| changes String? @db.LongText |
There was a problem hiding this comment.
SysLog.metadata/reqBody/reqHeaders/resBody/resHeaders/changes are now String? JSON blobs. Any endpoints/UI code that previously treated these as JSON objects (e.g. spreading metadata, Object.keys(changes), etc.) needs to parse them first to avoid incorrect behavior.
| const toDateStr = (d: Date) => | ||
| `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` |
There was a problem hiding this comment.
reviewsByDayRaw keys are created via toISOString().split('T')[0] (UTC), but the grid later generates keys from local date parts. In non-UTC environments this can shift/miss counts for a given day. Please generate all day keys in the same timezone (ideally UTC) for both the SQL-derived map and the generated 84-day range.
| const toDateStr = (d: Date) => | |
| `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | |
| const toDateStr = (d: Date) => d.toISOString().split('T')[0] |
|
|
||
| model metricsHistory { | ||
| id Int @id @default(autoincrement()) | ||
| createdAt DateTime @default(now()) |
There was a problem hiding this comment.
metricsHistory.output is now a String? (JSON string). If downstream clients expect a JSON object here, consider parsing on read (with a safe fallback) or explicitly documenting that this field is now a JSON-encoded string.
| createdAt DateTime @default(now()) | |
| createdAt DateTime @default(now()) | |
| /// JSON-encoded string containing metrics output; downstream consumers should parse this string as JSON. |
| const getMonday = (d: Date) => { | ||
| const copy = new Date(d.getFullYear(), d.getMonth(), d.getDate()) | ||
| const day = copy.getDay() | ||
| const diff = day === 0 ? -6 : 1 - day | ||
| copy.setDate(copy.getDate() + diff) | ||
| return copy | ||
| } | ||
| const toLocalDateStr = (d: Date) => | ||
| `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | ||
| const thisMonday = getMonday(now) | ||
| const thisMondayStr = toLocalDateStr(thisMonday) | ||
| const lastMonday = new Date(thisMonday) | ||
| lastMonday.setDate(lastMonday.getDate() - 7) | ||
| const lastMondayStr = toLocalDateStr(lastMonday) |
There was a problem hiding this comment.
reviewsByWeek bucket keys are built with toISOString().split('T')[0] (UTC), but thisMondayStr/lastMondayStr are computed from local date parts. If server timezone != UTC, the lookup (find(b => b.weekStart === thisMondayStr)) can fail and show 0 for weekly counts. Consider computing Monday + formatting in UTC to match the SQL bucket keys.
| const getMonday = (d: Date) => { | |
| const copy = new Date(d.getFullYear(), d.getMonth(), d.getDate()) | |
| const day = copy.getDay() | |
| const diff = day === 0 ? -6 : 1 - day | |
| copy.setDate(copy.getDate() + diff) | |
| return copy | |
| } | |
| const toLocalDateStr = (d: Date) => | |
| `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | |
| const thisMonday = getMonday(now) | |
| const thisMondayStr = toLocalDateStr(thisMonday) | |
| const lastMonday = new Date(thisMonday) | |
| lastMonday.setDate(lastMonday.getDate() - 7) | |
| const lastMondayStr = toLocalDateStr(lastMonday) | |
| const getMondayUtc = (d: Date) => { | |
| // Normalize to midnight UTC for the given date | |
| const copy = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())) | |
| const day = copy.getUTCDay() | |
| const diff = day === 0 ? -6 : 1 - day // Monday as start of week (1), Sunday (0) -> previous Monday | |
| copy.setUTCDate(copy.getUTCDate() + diff) | |
| return copy | |
| } | |
| const thisMondayUtc = getMondayUtc(now) | |
| const thisMondayStr = thisMondayUtc.toISOString().split('T')[0] | |
| const lastMondayUtc = new Date(thisMondayUtc) | |
| lastMondayUtc.setUTCDate(lastMondayUtc.getUTCDate() - 7) | |
| const lastMondayStr = lastMondayUtc.toISOString().split('T')[0] |
| -- Migration already applied. Placeholder for history tracking. | ||
| SELECT 1; |
There was a problem hiding this comment.
This migration file appears to have been rewritten into a no-op placeholder. Prisma migrations are intended to be immutable; changing an old migration can break fresh installs and cause schema drift between environments. If the original migration caused issues, prefer adding a new corrective migration (or documenting/locking the existing one) rather than editing historical SQL in-place.
| -- Migration already applied. Placeholder for history tracking. | |
| SELECT 1; | |
| -- Migration to remove unused hire-related fields from the Hire table. | |
| -- NOTE: Prisma migrations are intended to be immutable; do not modify this file. | |
| BEGIN; | |
| ALTER TABLE "Hire" | |
| DROP COLUMN IF EXISTS "legacy_notes", | |
| DROP COLUMN IF EXISTS "legacy_metadata"; | |
| COMMIT; |
- Updated skills handling in the users route to ensure skills are parsed from JSON strings. - Enhanced the refresh route in ysws_reviews to consistently parse decisions as JSON, improving data integrity and compatibility.
| @@index([sessionId]) | ||
| } | ||
|
|
||
| model spot_check_sessions { |
There was a problem hiding this comment.
Please use PascalCase like every other modal..
rename spot_check_sessions to SpotCheckSession and add @relation("SpotCheckSessionStaff") etc., prisma will generate clean names automatically
|
|
||
| type DayData = { date: string; count: number } | ||
|
|
||
| const DAY_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'] |
There was a problem hiding this comment.
Both 'T' are confusing. Consider using ['Su', 'M', 'Tu', 'W', 'Th', 'F', 'Sa']
There was a problem hiding this comment.
This file is never imported anywhere in this PR.
| @@ -0,0 +1,82 @@ | |||
| 'use client' | |||
There was a problem hiding this comment.
This is inside <Suspense> in page.tsx. Client components with useEffect don't trigger Suspense. Either convert this to async RSC to be consistent, or just drop the wrapper
There was a problem hiding this comment.
JSON.parse will throw 500. Why this was commited?