Service Spec: tenant-web
What this service is
The Next.js 14 frontend. Serves both the student-facing portal and the admin dashboard. Role-aware routing: the same app shows completely different views based on the authenticated user's role. For MVP, it hardcodes x-tenant-id: ashoka on every API call.
Tech stack
- Framework: Next.js 14 (App Router)
- Language: TypeScript (strict mode)
- Styling: Tailwind CSS
- UI components: shadcn/ui
- State: Zustand (global auth/user state), React Query (server state / API calls)
- Forms: React Hook Form + Zod
- Tables: TanStack Table v8
- Charts: Recharts
- File upload: react-dropzone
- QR:
qrcode.react(display),react-qr-reader(scan) - Date:
date-fns - HTTP client:
axioswith a base instance that injectsx-tenant-idandAuthorizationheaders
Folder structure
apps/tenant-web/
├── app/
│ ├── layout.tsx # root layout, providers
│ ├── (auth)/
│ │ ├── login/page.tsx # login page
│ │ └── layout.tsx # unauthed layout (no sidebar)
│ ├── (student)/
│ │ ├── layout.tsx # student sidebar + nav
│ │ ├── dashboard/page.tsx
│ │ ├── profile/page.tsx
│ │ ├── resumes/
│ │ │ ├── page.tsx # list resumes
│ │ │ └── [id]/page.tsx # resume editor
│ │ ├── jobs/
│ │ │ ├── page.tsx # job browser
│ │ │ └── [id]/page.tsx # job detail + apply
│ │ ├── applications/page.tsx
│ │ └── events/page.tsx
│ ├── (admin)/
│ │ ├── layout.tsx # admin sidebar + nav
│ │ ├── dashboard/page.tsx
│ │ ├── students/
│ │ │ ├── page.tsx # student table
│ │ │ └── [id]/page.tsx # student detail
│ │ ├── cycles/
│ │ │ ├── page.tsx
│ │ │ └── [id]/
│ │ │ ├── page.tsx # cycle overview
│ │ │ ├── jobs/page.tsx
│ │ │ └── applications/page.tsx
│ │ ├── events/page.tsx
│ │ ├── verifications/page.tsx # verifier view
│ │ └── intel/page.tsx # student intel / analytics
│ └── api/
│ └── auth/[...nextauth]/ # if using NextAuth, or custom route handlers
├── components/
│ ├── ui/ # shadcn components
│ ├── shared/
│ │ ├── DataTable.tsx # TanStack Table wrapper with pagination/sort/filter
│ │ ├── SegmentationBuilder.tsx # the query builder component
│ │ ├── FileUpload.tsx
│ │ ├── ProfileSection.tsx # reusable profile section with verification badge
│ │ └── StatusBadge.tsx
│ ├── student/
│ │ ├── ProfileEditor.tsx
│ │ ├── ResumeBuilder.tsx
│ │ ├── JobCard.tsx
│ │ └── ApplicationTracker.tsx
│ └── admin/
│ ├── StudentDetailPanel.tsx
│ ├── CycleConfig.tsx
│ ├── JobPostForm.tsx
│ ├── BulkEnrollModal.tsx
│ ├── VerificationQueue.tsx
│ └── IntelDashboard.tsx
├── lib/
│ ├── api.ts # axios instance (injects headers)
│ ├── auth.ts # token storage, refresh logic
│ ├── hooks/
│ │ ├── useCurrentUser.ts
│ │ ├── useStudents.ts # React Query hooks per domain
│ │ ├── useJobs.ts
│ │ └── ...
│ └── constants.ts # TENANT_SLUG = "ashoka"
├── middleware.ts # Next.js middleware: redirect unauthed users
└── ...Auth flow in the frontend
// lib/api.ts
import axios from 'axios';
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL, // http://api-gateway:3000/api
headers: {
'x-tenant-id': 'ashoka', // hardcoded for MVP
}
});
// Attach access token on every request
api.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Auto-refresh on 401 token_expired
api.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.data?.error === 'token_expired') {
const newToken = await refreshAccessToken();
error.config.headers.Authorization = `Bearer ${newToken}`;
return api.request(error.config);
}
return Promise.reject(error);
}
);Token storage: access token in memory (Zustand store), refresh token in httpOnly cookie (set by the server). On page refresh, immediately call /auth/refresh using the cookie to rehydrate the access token.
Next.js middleware (route protection)
// middleware.ts
export function middleware(req: NextRequest) {
const token = req.cookies.get('refresh_token');
const isAuthPage = req.nextUrl.pathname.startsWith('/login');
if (!token && !isAuthPage) {
return NextResponse.redirect(new URL('/login', req.url));
}
if (token && isAuthPage) {
return NextResponse.redirect(new URL('/dashboard', req.url));
}
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};Role-based redirect after login: student → /dashboard, admin → /admin/dashboard.
Page specs
Login page /login
Layout: Centered card, university logo, two tabs: "Google" and "Email OTP"
Google tab:
- Button: "Continue with Google"
- Calls
GET /api/auth/google/url→ redirects to Google - On callback: stores tokens, redirects to dashboard
Email OTP tab:
- Step 1: Email input + "Send OTP" button → calls
POST /api/auth/otp/request - Step 2: 6-digit OTP input (auto-focus, auto-submit on 6 digits) → calls
POST /api/auth/otp/verify - Shows countdown timer for OTP expiry (10 min), resend button after 60s
- On success: store tokens, redirect
Error states:
- Email not allowed by tenant rules → "Your email is not registered in this system. Contact your placement office."
- Invalid OTP → "Incorrect code. X attempts remaining."
- Expired OTP → "Code expired. Request a new one."
Student: Dashboard /dashboard
Components:
- Welcome card: name, batch, major
- Quick stats: applications submitted, offers received, resumes created, pending verifications
- Active cycles: list of cycles they're enrolled in with enrollment status
- Upcoming events: next 3 events (PPTs, tests) with date/company
- Recent jobs: 5 most recently posted jobs they're eligible for
- Deadline calendar: jobs closing in next 7 days
- Announcements: latest from admin
Student: Profile /profile
Layout: Sections accordion with verification badge per section
Sections:
- Personal details (name, email, roll number, batch, major, minor, category)
- Education (can add multiple entries — school, undergrad, any further)
- Work experience (each entry: company, role, dates, bullet points, proof document upload)
- Projects (title, description, link, bullets)
- Skills (categories, proficiency)
- Achievements / extracurriculars
- Courses / certifications
Per section:
- Edit button → inline editor
- Verification badge:
Unverified|Pending review|Verified|Rejected - Upload proof document (PDF/image) per experience/education entry
- "Request verification" button when section has unverified entries
Profile completeness: Progress bar at top showing % of required fields filled.
Student: Resume builder /resumes
List view: Grid of resume cards. Each shows: name, last edited, verification status summary, "Attach to job" quick action.
Create resume:
- "Create from profile" → snapshot current profile into a new resume
- Displays resume in a split-pane editor: left = section list, right = preview (rendered as a clean A4 layout)
Resume editor:
- Toggle sections on/off (education, experience, projects, skills, etc.)
- Reorder bullets within a section via drag-and-drop
- Edit individual bullet text → marks that bullet as "unverified edit" (shown in amber)
- "Request AI rewrite" → opens modal: select target job from dropdown, select tone (concise/detailed), submit → async, shows "AI is rewriting..." spinner, auto-refreshes when done
- "Request verification" → sends pending sections to verifier queue
AI variant badge: Resumes created by AI show "AI variant" badge. Auto-verified unless student edits bullets — edited bullets revert to "pending verification".
Student: Job browser /jobs
Layout: Left sidebar filters, main content job cards (list or grid toggle)
Filters:
- Sector (multi-select)
- Company (search)
- Job type (full-time / internship / PPO)
- CTC range (slider)
- Status (open/closed)
- Applied (yes/no)
- My eligibility (show only eligible)
Search: Full-text across job title + company name
Job card: Company logo, role title, location, CTC, deadline, sector badge, eligibility indicator (green tick / red cross with reason on hover)
Sorting: By deadline (default), by CTC, by company name, by date posted
Pagination: 20 per page, infinite scroll option
Student: Job detail /jobs/[id]
Sections:
- Company info (name, sector, about)
- Role details (title, location, type, CTC, description)
- Eligibility status (clear Y/N with reasons)
- PPT requirement (if required: shows which PPT event + attendance status)
- Application deadline + countdown
- "Apply" button (disabled if ineligible, shows reason)
Apply modal:
- Select resume (dropdown of own resumes, preview thumbnail)
- Cover letter (optional text area)
- Video upload (if required by job)
- Confirm + submit
- Confirmation screen with application ID
Student: Applications /applications
Table columns: Company, Role, Cycle, Submitted date, Status (badge), Resume used
Filters: Status, Cycle, Sector, Applied/Rejected/Offered
Segmentation shortcut: Quick filter presets like "Rejected in internship cycle" as saved queries
Status flow visible: Click a row → timeline showing status history
Student: Events /events
Layout: Calendar view (month) + list view toggle
Event card: Title, company (if PPT), date/time, location, attendance status badge, exception request button (if missed)
QR check-in: "Check in" button → opens camera to scan QR code shown on screen at the event venue
Admin: Student table /admin/students
This is the main power view.
Table:
- Configurable columns (drag to show/hide/reorder): name, email, batch, major, cgpa, c1–c5, verification status, cycle enrollments, applications count, offers count
- All columns sortable
- Search bar (name, email, roll number)
- Segmentation builder panel (collapsible) — build a query with AND/OR conditions → filters the table in real time
- Save current segmentation as a named filter
- Saved filters shown as quick-select chips above the table
Row actions: View, Edit, Deactivate, View resumes, Impersonate (super_admin only)
Bulk actions (on selection):
- Enroll in cycle
- Export selected as CSV
- Send notification
- Assign verifier
Export button: CSV or Excel with selected/configured columns
Admin: Student detail /admin/students/[id]
Tabs:
- Profile — full profile view, edit inline
- Resumes — list all resumes, view/download each
- Applications — all applications across all cycles, status timeline
- Verifications — verification history per section
- Events — attendance history
- Dossier — external data pulled from tenant's configured external API (shown as raw structured data)
- Activity log — all actions performed on this student's account
AI summary card: Sidebar widget showing AI-generated summary of the student (from ai-worker)
Admin: Cycles /admin/cycles
List: Table of cycles with name, batch, type, status, enrollment count, active jobs count
Cycle detail /admin/cycles/[id]:
Tabs:
- Overview — config, enrollment toggle, stats
- Students — enrolled students (same as student table but scoped to cycle), bulk enroll button
- Jobs — list of jobs in this cycle
- Applications — all applications across all jobs in this cycle, cross-job view
- Analytics — funnel chart (applied → shortlisted → offered → accepted), company breakdown, sector breakdown
Enroll modal:
- Tab 1: "By segmentation" — use segmentation builder to define who to enroll
- Tab 2: "Upload CSV" — upload a CSV of emails/roll numbers
Admin: Post job /admin/cycles/[id]/jobs/new
Multi-step form:
Step 1: Company + role
- Company (search from master list, or "Add new company")
- Role title, description (rich text editor)
- Location (text), sector (select), job type
Step 2: Compensation
- CTC or stipend amount + currency
- CTC breakdown (text)
Step 3: Eligibility
- Inherits cycle eligibility (shown as a summary)
- Add additional eligibility criteria using segmentation builder
- Preview: "X students currently match this eligibility"
Step 4: Application settings
- Require resume (always yes)
- Require cover letter (toggle)
- Require video (toggle)
- Application deadline (date picker)
- Open applications immediately (toggle)
Step 5: Review + publish (or save as draft)
Admin: Verifications /admin/verifications
For verifier role. Shows only students assigned to this verifier.
Queue view: List of pending verification requests, sorted by oldest first
Each item shows:
- Student name, section (e.g. "Experience: Internship at Google")
- Before/after diff if it's an edit
- Proof document (PDF viewer inline)
- Approve / Reject buttons
- Reject requires a comment (shown to student)
Admin: Student intel /admin/intel
Layout: Full-screen data explorer
Left panel: Column picker (drag on/off) + Segmentation builder
Main area: Data table (same as student table but wider, more columns)
Summary bar above table (from segmentation summary API):
- Total students matching
- AVG cgpa, MAX cgpa, MIN cgpa
- Breakdown by major (mini bar chart inline)
- Breakdown by c1/c2 status
Export: CSV / Excel of current view
Analytics sub-page /admin/intel/analytics:
- Placements by sector (donut chart)
- CTC distribution (histogram)
- Company placement count (bar chart)
- Year-over-year trend (line chart)
- Filter all by cycle
DataTable component
Shared component used on every list view. Props:
interface DataTableProps<T> {
columns: ColumnDef<T>[]; // TanStack Table column definitions
queryKey: string[]; // React Query cache key
fetchFn: (params: ListParams) => Promise<ListResponse<T>>;
defaultSort?: { field: string; direction: 'asc' | 'desc' };
searchPlaceholder?: string;
filters?: FilterConfig[]; // dropdown filters to show above table
bulkActions?: BulkAction[];
onRowClick?: (row: T) => void;
testId?: string; // data-testid prefix for Playwright
}Internally manages: page state, sort state, filter state, search debounce (300ms), selection state.
SegmentationBuilder component
interface SegmentationBuilderProps {
value: SegmentationDefinition;
onChange: (def: SegmentationDefinition) => void;
onEvaluate?: () => void; // triggers API call to evaluate
evaluationResult?: { count: number; summary: Record<string, any> };
availableFields: FieldConfig[]; // tenant-specific fields
mode: 'full' | 'compact'; // full for intel page, compact for job form
}UI breakdown:
- Group container with
AND/ORtoggle at group level - Each group has rows: [Field dropdown] [Operator dropdown] [Value input] [Delete]
- "Add condition" button per group
- "Add group" button at the top level
- Individual overrides section at the bottom: "Always include" and "Always exclude" student search inputs
- "Evaluate" button → calls
/segmentations/evaluate→ shows count + summary stats - "Save as filter" button → names and saves the definition
Test IDs (Playwright)
Every interactive element has a data-testid. Convention: [module]-[component]-[action]
Examples:
login-google-button
login-otp-email-input
login-otp-submit-button
profile-experience-edit-button
jobs-apply-button
jobs-filter-sector-select
applications-status-badge
admin-students-bulk-enroll-button
segmentation-add-condition-button
segmentation-evaluate-button