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: axios with a base instance that injects x-tenant-id and Authorization headers

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:

  1. Personal details (name, email, roll number, batch, major, minor, category)
  2. Education (can add multiple entries — school, undergrad, any further)
  3. Work experience (each entry: company, role, dates, bullet points, proof document upload)
  4. Projects (title, description, link, bullets)
  5. Skills (categories, proficiency)
  6. Achievements / extracurriculars
  7. 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:

  1. Profile — full profile view, edit inline
  2. Resumes — list all resumes, view/download each
  3. Applications — all applications across all cycles, status timeline
  4. Verifications — verification history per section
  5. Events — attendance history
  6. Dossier — external data pulled from tenant's configured external API (shown as raw structured data)
  7. 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:

  1. Overview — config, enrollment toggle, stats
  2. Students — enrolled students (same as student table but scoped to cycle), bulk enroll button
  3. Jobs — list of jobs in this cycle
  4. Applications — all applications across all jobs in this cycle, cross-job view
  5. 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/OR toggle 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