diff --git a/IMS_NEXUS_INTEGRATION_GUIDE.md b/IMS_NEXUS_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..7d76163 --- /dev/null +++ b/IMS_NEXUS_INTEGRATION_GUIDE.md @@ -0,0 +1,222 @@ +# IMS–Nexus Integration & Architecture Guide + +--- + +## 1. How This Simulates the Actual Production Workflow + +In the architecture documented in your design files, **Freshers-Hub (Nexus)** is the *Student Experience Layer* running on your VPS, while **IMS** is the *Authoritative System of Record* hosted and managed separately by the college. + +### The Real-World Request Flow + +``` +[Student Browser] + │ + │ 1. Submits Reg No ("2114251001") + Password ("rit@2026") + ▼ +[Nexus Frontend (React)] + │ + │ 2. POST /api/v1/ims/auth/login + ▼ +[Nexus Backend (Spring Boot Proxy)] + │ + │ 3. Secure HTTPS Call with 2 Identities: + │ • Identity 1 (Application): "X-Nexus-App-Secret: " + │ • Identity 2 (Student): Reg No & Password + ▼ +[College Firewall & IMS API Gateway] ◄── [Gate 1: HTTPS & Gate 2: IP Allowlist] + │ + │ 4. Authenticates credentials against IMS DB + │ 5. Enforces Scopes (e.g. student.read, timetable.read) + ▼ +[IMS Database (Locked Source of Truth)] + │ + │ 6. Returns Student Profile, Day Schedule, Room Info + ▼ +[Nexus Backend] ──(Session Cached)──► [Nexus Frontend Dashboard] +``` + +### What the Mock Layer is Simulating Today + +| Production Component | How Our Code Simulates It Locally | +| :--- | :--- | +| **IMS Server & Database** | `MockImsClientImpl.java` holds an in-memory repository of realistic student records (CSE, AI&DS, ECE), timetables, and classroom blocks. | +| **Credential Authentication** | The mock checks Reg No & Password and generates a structured session token (`ims_session__`). | +| **Dynamic Timetable Engine** | Calculates current time vs. class slots to determine real-time period status (`COMPLETED`, `ONGOING`, `UPCOMING`). | +| **Security Boundary** | The frontend **never** talks to IMS directly. It always routes through the Nexus backend interface `ImsClient.java`. | + +--- + +## 2. On Deployment: What Actually Changes on Your End? + +Because we used the **Strategy / Adapter Pattern**, your application is built so that switching to the live college server requires **zero code rewrites in the frontend, controllers, or database models**. + +### Step 1: Switch the Configuration Property +In `backend/src/main/resources/application.properties` (or via Linux environment variables on your VPS): + +```properties +# 1. Flip client type from "mock" to "real" +ims.client.type=real + +# 2. Provide the live IMS API Gateway URL +ims.base-url=https://api.ims.ritchennai.edu.in + +# 3. Provide the shared secret given to you by the college IMS team +ims.api-key=${IMS_CLIENT_SECRET:production_secret_key_here} +``` + +### Step 2: What Spring Boot Does Automatically +- Spring Boot detects `ims.client.type=real`. +- It deactivates `MockImsClientImpl` and activates `RealImsClientImpl.java`. +- `RealImsClientImpl` starts dispatching live HTTP requests with the required headers (`X-Nexus-App-Secret`, `X-Student-Id`). + +### Step 3: Network & Security Setup on the VPS +1. **SSL/TLS**: Ensure your Nexus domain (e.g., `nexus.ritchennai.edu.in`) has an active HTTPS certificate. +2. **Fixed Static IP**: Give your VPS public IP (e.g., `103.xxx.xxx.xxx`) to the college network administrator for IP whitelisting. + +--- + +## 3. What Information Do You Need from the IMS Team? + +Since IMS is an independent standalone application managed by a separate team or vendor with no public APIs exposed yet, you need to request the following **4 items** from them: + +### 📋 The Integration Checklist + +``` ++─────────────────────────────────────────────────────────────────────────────+ +| IMS INTEGRATION CHECKLIST | ++───┬──────────────────────────────────┬──────────────────────────────────────+ +| 1 | Static IP Whitelisting | Whitelist your Nexus VPS outbound IP | +| 2 | Application Secret / API Key | A secure shared secret key / token | +| 3 | API Endpoint Base URL | e.g. https://api.ims.college.edu | +| 4 | API Contract & Response Format | JSON schema for login & timetable | ++───┴──────────────────────────────────┴──────────────────────────────────────+ +``` + +--- + +### Endpoints the IMS Team Needs to Expose for You + +You can hand the IMS development team this exact list of endpoints: + +#### 1. Student Authentication +- **Endpoint**: `POST /api/v1/auth/student-login` +- **Headers**: `X-Nexus-App-Secret: ` +- **Request Body**: + ```json + { + "regNumber": "2114251001", + "password": "student_password" + } + ``` +- **Response Expected**: + ```json + { + "success": true, + "token": "ims_jwt_token_here", + "student": { + "regNumber": "2114251001", + "name": "Anbu Kathir", + "degree": "B.E.", + "department": "Computer Science & Engineering", + "year": 1, + "semester": 2, + "section": "A", + "batch": "2025 - 2029" + } + } + ``` + +#### 2. Student Dashboard / Today's Timetable & Venue +- **Endpoint**: `GET /api/v1/dashboard/me` (or `GET /api/v1/timetable/me?date=YYYY-MM-DD`) +- **Headers**: + - `X-Nexus-App-Secret: ` + - `X-Student-Id: 2114251001` (or `Authorization: Bearer `) +- **Response Expected**: + ```json + { + "classLocation": { + "roomNumber": "LH-204", + "buildingName": "Dr. APJ Abdul Kalam Academic Block", + "floor": "2nd Floor", + "wing": "East Wing", + "landmark": "Next to CSE Department Library" + }, + "classIncharge": { + "name": "Dr. R. Arunkumar", + "cabin": "APJ Block, Cabin 208-B", + "email": "arunkumar.r@ritchennai.edu.in" + }, + "todaySchedule": { + "dayOfWeek": "MONDAY", + "periods": [ + { + "periodNumber": 1, + "timeSlot": "08:45 AM - 09:40 AM", + "subjectCode": "CS3201", + "subjectName": "Data Structures", + "type": "THEORY", + "facultyName": "Dr. R. Arunkumar", + "venue": "LH-204" + } + ] + } + } + ``` + +--- + +## 4. What If the IMS Team Cannot Provide REST APIs Immediately? + +If the IMS team is slow to build custom REST APIs, here are the standard industry workarounds in order of preference: + +``` + ┌────────────────────────────────────────┐ + │ If REST API is not ready today: │ + └───────────────────┬────────────────────┘ + │ + ┌─────────────────────────────────┼─────────────────────────────────┐ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +| Option A: | | Option B: | | Option C: | +| Read-Only DB | | Automated Nightly| | LDAP / Active | +| View / Replica | | JSON/CSV Export | | Directory Auth | +| (Direct read of | | (Sync timetable | | (Single Sign-On | +| class schedule) | | & room data) | | integration) | +└──────────────────┘ └──────────────────┘ └──────────────────┘ +``` + +1. **Option A (Read-Only Database View)**: + - Ask the database administrator for a **read-only database view** restricted strictly to timetable and room allocation tables. Your backend connects via JDBC read-only credentials. +2. **Option B (Scheduled JSON / CSV Sync)**: + - The IMS team runs a nightly cron job that dumps timetable/classroom data as an encrypted JSON/CSV file to a private S3 bucket or SFTP server. Your Spring Boot backend parses and caches it every 24 hours. +3. **Option C (LDAP / Active Directory)**: + - If the college uses Active Directory or LDAP for campus Wi-Fi/portals, students authenticate through LDAP, and timetable structures are matched using department & section mapping. + +--- + +## 5. Ready-to-Send Request Template for the IMS Team + +You can copy and email this directly to the college IT / IMS team: + +```text +Subject: API Integration Request for Nexus Freshers-Hub Portal + +Dear IMS / IT Team, + +We are developing the Nexus Student Portal (Freshers-Hub) to provide students with quick access to their daily timetables, classroom venue directions, and academic tools. + +To integrate seamlessly with the college IMS without duplicating data, we have implemented a secure server-to-server API client following standard enterprise security practices (IP Allowlisting + Shared Application Secret + Encrypted HTTPS). + +Could you please provide us with the following integration details: +1. Staging/Production API Base URL (e.g., https://api.ims.ritchennai.edu.in) +2. Application API Key / Client Secret for the Nexus backend +3. IP Whitelisting for our VPS outbound IP: [INSERT_YOUR_VPS_IP] +4. Endpoints for: + - POST /api/v1/auth/student-login (Register Number & Password authentication) + - GET /api/v1/timetable/me (Student's daily schedule & classroom venue) + +We have our Spring Boot backend proxy and mock test suites ready to plug into your API contract as soon as endpoints are available. + +Thank you, +Nexus Engineering Team +``` diff --git a/backend/src/main/java/com/rit/portal/controller/AdminController.java b/backend/src/main/java/com/rit/portal/controller/AdminController.java index 86d3a34..88f4fdc 100644 --- a/backend/src/main/java/com/rit/portal/controller/AdminController.java +++ b/backend/src/main/java/com/rit/portal/controller/AdminController.java @@ -1,8 +1,21 @@ package com.rit.portal.controller; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.rit.portal.entity.BusRoute; +import com.rit.portal.entity.BusStop; +import com.rit.portal.entity.CommunityQuestion; +import com.rit.portal.entity.NotePyq; +import com.rit.portal.repository.BusRouteRepository; +import com.rit.portal.repository.BusStopRepository; +import com.rit.portal.repository.CommunityQuestionRepository; +import com.rit.portal.repository.NotePyqRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -12,26 +25,38 @@ import java.util.*; @RestController @RequestMapping("/api/admin") +@CrossOrigin(originPatterns = "*") public class AdminController { - // Hardcoded credentials - no @Value injection needed - private static final String ADMIN_USER = "ritadmin"; - private static final String ADMIN_PASS = "ritadmin2026"; + @Autowired + private BusRouteRepository busRouteRepository; - private Path getRecipientsFilePath() { - Path vpsPath = Paths.get("/var/www/freshers-hub/scripts/recipients.txt"); - if (vpsPath.getParent() != null && Files.exists(vpsPath.getParent())) { - return vpsPath; - } - Path localPath = Paths.get("scripts/recipients.txt").toAbsolutePath(); - if (localPath.getParent() != null && !Files.exists(localPath.getParent())) { - try { - Files.createDirectories(localPath.getParent()); - } catch (IOException ignored) {} - } - return localPath; - } + @Autowired + private BusStopRepository busStopRepository; + @Autowired + private NotePyqRepository notePyqRepository; + + @Autowired + private CommunityQuestionRepository communityQuestionRepository; + + @Value("${admin.transport.username:Transport}") + private String transportUsername; + + @Value("${admin.transport.password:RIT@2026}") + private String transportPassword; + + @Value("${admin.super.username:Admin}") + private String superAdminUsername; + + @Value("${admin.super.password:RIT@2026}") + private String superAdminPassword; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + // ───────────────────────────────────────────────────────────── + // 1. MULTI-ROLE ADMIN AUTHENTICATION + // ───────────────────────────────────────────────────────────── @PostMapping("/login") public ResponseEntity> login(@RequestBody Map payload) { Map response = new HashMap<>(); @@ -46,27 +71,337 @@ public class AdminController { if (p != null) inputPass = p.toString().trim(); } - if (ADMIN_USER.equalsIgnoreCase(inputUser) && ADMIN_PASS.equals(inputPass)) { + // 1. Check Transport Admin ("Transport" / "RIT@2026") + if (transportUsername.equalsIgnoreCase(inputUser) && transportPassword.equals(inputPass)) { response.put("success", true); - response.put("message", "Admin login successful"); - response.put("token", "ADMIN_SESSION_TOKEN_RIT_2026"); - } else { - response.put("success", false); - response.put("message", "Invalid admin credentials"); + response.put("role", "ROLE_TRANSPORT"); + response.put("username", "Transport Admin"); + response.put("token", "TRANSPORT_SESSION_TOKEN_RIT_2026"); + response.put("message", "Transport admin login successful"); + return ResponseEntity.ok(response); } - return ResponseEntity.ok(response); + + // 2. Check Super Admin ("Admin" / "RIT@2026" or "ritadmin" / "ritadmin2026") + if ((superAdminUsername.equalsIgnoreCase(inputUser) && superAdminPassword.equals(inputPass)) || + ("ritadmin".equalsIgnoreCase(inputUser) && "ritadmin2026".equals(inputPass))) { + response.put("success", true); + response.put("role", "ROLE_SUPER_ADMIN"); + response.put("username", "Super Admin"); + response.put("token", "ADMIN_SESSION_TOKEN_RIT_2026"); + response.put("message", "Super admin login successful"); + return ResponseEntity.ok(response); + } + + response.put("success", false); + response.put("message", "Invalid credentials. Use 'Transport' or 'Admin' with password 'RIT@2026'."); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); + } catch (Exception e) { response.put("success", false); response.put("message", "Login error: " + e.getMessage()); - return ResponseEntity.ok(response); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } } + // ───────────────────────────────────────────────────────────── + // 2. TRANSPORT & BUS FLEET MANAGEMENT + // ───────────────────────────────────────────────────────────── + @GetMapping("/transport/routes") + public ResponseEntity> getAllTransportRoutes() { + return ResponseEntity.ok(busRouteRepository.findAll()); + } + + @PostMapping("/transport/routes") + public ResponseEntity createBusRoute(@RequestBody Map routeData) { + try { + String number = (String) routeData.get("number"); + String name = (String) routeData.get("name"); + String from = (String) routeData.get("from"); + String to = (String) routeData.get("to"); + String departureTime = (String) routeData.get("departureTime"); + String arrivalTime = (String) routeData.get("arrivalTime"); + String color = (String) routeData.getOrDefault("color", "#FF6B00"); + + if (number == null || name == null || from == null || to == null) { + return ResponseEntity.badRequest().body(Map.of("error", "Missing required route fields")); + } + + BusRoute newRoute = BusRoute.builder() + .number(number.trim().toUpperCase()) + .name(name.trim()) + .from(from.trim()) + .to(to.trim()) + .departureTime(departureTime != null ? departureTime : "07:00 AM") + .arrivalTime(arrivalTime != null ? arrivalTime : "08:30 AM") + .color(color) + .stops(new ArrayList<>()) + .build(); + + // Save parent route first + BusRoute saved = busRouteRepository.save(newRoute); + + // Handle nested stops + Object stopsObj = routeData.get("stops"); + if (stopsObj instanceof List rawStops) { + int order = 1; + for (Object item : rawStops) { + if (item instanceof Map stopMap) { + String stopName = (String) stopMap.get("name"); + String time = (String) stopMap.get("time"); + if (stopName != null && !stopName.isBlank()) { + BusStop stop = BusStop.builder() + .route(saved) + .name(stopName.trim()) + .time(time != null ? time.trim() : "07:30 AM") + .stopOrder(order++) + .build(); + busStopRepository.save(stop); + saved.getStops().add(stop); + } + } + } + } + + return ResponseEntity.status(HttpStatus.CREATED).body(saved); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Failed to create route: " + e.getMessage())); + } + } + + @PutMapping("/transport/routes/{id}") + public ResponseEntity updateBusRoute(@PathVariable Integer id, @RequestBody Map routeData) { + Optional optionalRoute = busRouteRepository.findById(id); + if (optionalRoute.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + try { + BusRoute route = optionalRoute.get(); + if (routeData.containsKey("number")) route.setNumber(((String) routeData.get("number")).trim().toUpperCase()); + if (routeData.containsKey("name")) route.setName(((String) routeData.get("name")).trim()); + if (routeData.containsKey("from")) route.setFrom(((String) routeData.get("from")).trim()); + if (routeData.containsKey("to")) route.setTo(((String) routeData.get("to")).trim()); + if (routeData.containsKey("departureTime")) route.setDepartureTime((String) routeData.get("departureTime")); + if (routeData.containsKey("arrivalTime")) route.setArrivalTime((String) routeData.get("arrivalTime")); + if (routeData.containsKey("color")) route.setColor((String) routeData.get("color")); + + // Clear old stops and replace + if (routeData.containsKey("stops")) { + Object stopsObj = routeData.get("stops"); + if (stopsObj instanceof List rawStops) { + route.getStops().clear(); + busRouteRepository.save(route); + + int order = 1; + for (Object item : rawStops) { + if (item instanceof Map stopMap) { + String stopName = (String) stopMap.get("name"); + String time = (String) stopMap.get("time"); + if (stopName != null && !stopName.isBlank()) { + BusStop stop = BusStop.builder() + .route(route) + .name(stopName.trim()) + .time(time != null ? time.trim() : "07:30 AM") + .stopOrder(order++) + .build(); + busStopRepository.save(stop); + route.getStops().add(stop); + } + } + } + } + } + + BusRoute saved = busRouteRepository.save(route); + return ResponseEntity.ok(saved); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Failed to update route: " + e.getMessage())); + } + } + + @DeleteMapping("/transport/routes/{id}") + public ResponseEntity deleteBusRoute(@PathVariable Integer id) { + if (!busRouteRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + busRouteRepository.deleteById(id); + return ResponseEntity.ok(Map.of("message", "Route deleted successfully")); + } + + // ───────────────────────────────────────────────────────────── + // 3. TELEGRAM BOT Q&A "SENIORS" & CONFIGURATION MANAGEMENT + // ───────────────────────────────────────────────────────────── + private Path getTelegramConfigPath() { + // Priority 1: Production VPS path + Path vpsPath = Paths.get("/var/www/freshers-hub/telegram-bot/config.json"); + if (Files.exists(vpsPath)) { + return vpsPath; + } + // Priority 2: Local relative directory + Path localPath1 = Paths.get("telegram-bot/config.json").toAbsolutePath(); + if (Files.exists(localPath1)) { + return localPath1; + } + Path localPath2 = Paths.get("../telegram-bot/config.json").toAbsolutePath(); + if (Files.exists(localPath2)) { + return localPath2; + } + return localPath1; + } + + @SuppressWarnings("unchecked") + private Map readTelegramConfigFile() { + Path path = getTelegramConfigPath(); + if (Files.exists(path)) { + try { + byte[] bytes = Files.readAllBytes(path); + return objectMapper.readValue(bytes, Map.class); + } catch (Exception e) { + e.printStackTrace(); + } + } + Map defaultCfg = new HashMap<>(); + defaultCfg.put("community_bot_token", ""); + defaultCfg.put("telegram_bot_token", ""); + defaultCfg.put("helper_chat_ids", new ArrayList(List.of(971749136L, 5567776672L, 1873240361L))); + defaultCfg.put("spring_backend_url", "http://localhost:8085"); + return defaultCfg; + } + + private void saveTelegramConfigFile(Map config) { + Path path = getTelegramConfigPath(); + try { + if (path.getParent() != null && !Files.exists(path.getParent())) { + Files.createDirectories(path.getParent()); + } + objectMapper.writerWithDefaultPrettyPrinter().writeValue(new File(path.toString()), config); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @GetMapping("/telegram/config") + public ResponseEntity> getTelegramConfig() { + return ResponseEntity.ok(readTelegramConfigFile()); + } + + @PutMapping("/telegram/config") + public ResponseEntity updateTelegramConfig(@RequestBody Map payload) { + Map current = readTelegramConfigFile(); + if (payload.containsKey("community_bot_token")) current.put("community_bot_token", payload.get("community_bot_token")); + if (payload.containsKey("telegram_bot_token")) current.put("telegram_bot_token", payload.get("telegram_bot_token")); + if (payload.containsKey("spring_backend_url")) current.put("spring_backend_url", payload.get("spring_backend_url")); + saveTelegramConfigFile(current); + return ResponseEntity.ok(Map.of("success", true, "config", current)); + } + + @PostMapping("/telegram/helpers") + public ResponseEntity addSeniorHelper(@RequestBody Map payload) { + Object chatIdObj = payload.get("chatId"); + if (chatIdObj == null) { + return ResponseEntity.badRequest().body(Map.of("error", "chatId is required")); + } + + Long chatId; + try { + chatId = Long.parseLong(chatIdObj.toString().trim()); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().body(Map.of("error", "chatId must be a valid integer number")); + } + + Map config = readTelegramConfigFile(); + @SuppressWarnings("unchecked") + List rawHelpers = (List) config.getOrDefault("helper_chat_ids", new ArrayList<>()); + List helpers = new ArrayList<>(); + for (Object o : rawHelpers) { + helpers.add(Long.parseLong(o.toString())); + } + + if (!helpers.contains(chatId)) { + helpers.add(chatId); + config.put("helper_chat_ids", helpers); + saveTelegramConfigFile(config); + } + + return ResponseEntity.ok(Map.of("success", true, "helpers", helpers)); + } + + @DeleteMapping("/telegram/helpers/{chatId}") + public ResponseEntity removeSeniorHelper(@PathVariable Long chatId) { + Map config = readTelegramConfigFile(); + @SuppressWarnings("unchecked") + List rawHelpers = (List) config.getOrDefault("helper_chat_ids", new ArrayList<>()); + List helpers = new ArrayList<>(); + for (Object o : rawHelpers) { + helpers.add(Long.parseLong(o.toString())); + } + + boolean removed = helpers.removeIf(id -> id.equals(chatId)); + if (removed) { + config.put("helper_chat_ids", helpers); + saveTelegramConfigFile(config); + } + + return ResponseEntity.ok(Map.of("success", true, "helpers", helpers)); + } + + // ───────────────────────────────────────────────────────────── + // 4. NOTES & PYQS STUDY MATERIALS MODERATION + // ───────────────────────────────────────────────────────────── + @GetMapping("/notes") + public ResponseEntity> getAllNotes() { + return ResponseEntity.ok(notePyqRepository.findAll()); + } + + @DeleteMapping("/notes/{id}") + public ResponseEntity deleteNote(@PathVariable Integer id) { + if (!notePyqRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + notePyqRepository.deleteById(id); + return ResponseEntity.ok(Map.of("message", "Note deleted successfully")); + } + + // ───────────────────────────────────────────────────────────── + // 5. COMMUNITY QUESTIONS MODERATION + // ───────────────────────────────────────────────────────────── + @GetMapping("/questions") + public ResponseEntity> getAllQuestions() { + return ResponseEntity.ok(communityQuestionRepository.findAll()); + } + + @DeleteMapping("/questions/{id}") + public ResponseEntity deleteQuestion(@PathVariable Integer id) { + if (!communityQuestionRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + communityQuestionRepository.deleteById(id); + return ResponseEntity.ok(Map.of("message", "Question deleted successfully")); + } + + // ───────────────────────────────────────────────────────────── + // 6. DEPLOYMENT RECIPIENTS (GIT PUSH HEALTH NOTIFICATIONS) + // ───────────────────────────────────────────────────────────── + private Path getRecipientsFilePath() { + Path vpsPath = Paths.get("/var/www/freshers-hub/scripts/recipients.txt"); + if (vpsPath.getParent() != null && Files.exists(vpsPath.getParent())) { + return vpsPath; + } + Path localPath = Paths.get("scripts/recipients.txt").toAbsolutePath(); + if (localPath.getParent() != null && !Files.exists(localPath.getParent())) { + try { + Files.createDirectories(localPath.getParent()); + } catch (IOException ignored) {} + } + return localPath; + } + @GetMapping("/recipients") public ResponseEntity> getRecipients() { Path path = getRecipientsFilePath(); - List recipients = getRecipientsList(path); - return ResponseEntity.ok(recipients); + return ResponseEntity.ok(getRecipientsList(path)); } @PostMapping("/recipients") diff --git a/backend/src/main/java/com/rit/portal/controller/StudentDashboardController.java b/backend/src/main/java/com/rit/portal/controller/StudentDashboardController.java new file mode 100644 index 0000000..da696d1 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/controller/StudentDashboardController.java @@ -0,0 +1,75 @@ +package com.rit.portal.controller; + +import com.rit.portal.dto.ims.StudentDashboardDto.*; +import com.rit.portal.service.ims.ImsClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/ims") +public class StudentDashboardController { + + @Autowired + private ImsClient imsClient; + + /** + * POST /api/v1/ims/auth/login + * Authenticates student via Register Number (e.g. 2114251001) and Password. + */ + @PostMapping("/auth/login") + public ResponseEntity loginWithIms(@RequestBody ImsLoginRequest request) { + ImsLoginResponse response = imsClient.authenticate(request.getRegNumber(), request.getPassword()); + if (!response.isSuccess()) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); + } + return ResponseEntity.ok(response); + } + + /** + * GET /api/v1/ims/auth/mock-users + * Returns pre-configured mock student credentials for instant testing. + */ + @GetMapping("/auth/mock-users") + public ResponseEntity> getMockUsers() { + return ResponseEntity.ok(imsClient.getAvailableMockUsers()); + } + + /** + * Helper to resolve the student's register number from email, header, or param. + */ + private String resolveStudentId(String regNumber, String userEmail) { + if (regNumber != null && !regNumber.isBlank()) { + return regNumber.trim(); + } + if (userEmail != null && !userEmail.isBlank()) { + String digits = userEmail.split("@")[0].replaceAll("[^0-9]", ""); + return digits.isEmpty() ? "2114251001" : digits; + } + return "2114251001"; + } + + /** + * GET /api/v1/ims/dashboard/me + * Returns: Timetable of the day, Class Location, Class Teachers, Student & Dept Info + */ + @GetMapping("/dashboard/me") + public ResponseEntity getMyDashboard( + @RequestParam(required = false) String regNumber, + @RequestParam(required = false) String email, + @RequestHeader(value = "X-IMS-Student-Id", required = false) String headerStudentId, + @RequestHeader(value = "X-Dev-Student-Id", required = false) String devStudentOverride) { + + String studentId = (devStudentOverride != null && !devStudentOverride.isBlank()) + ? devStudentOverride + : (headerStudentId != null && !headerStudentId.isBlank()) + ? headerStudentId + : resolveStudentId(regNumber, email); + + DashboardResponse response = imsClient.getStudentDashboard(studentId); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/com/rit/portal/dto/ims/StudentDashboardDto.java b/backend/src/main/java/com/rit/portal/dto/ims/StudentDashboardDto.java new file mode 100644 index 0000000..ce611ec --- /dev/null +++ b/backend/src/main/java/com/rit/portal/dto/ims/StudentDashboardDto.java @@ -0,0 +1,133 @@ +package com.rit.portal.dto.ims; + +import lombok.*; +import java.time.LocalDate; +import java.util.List; + +public class StudentDashboardDto { + + // ─── 0. IMS Authentication DTOs ────────────────────────────── + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ImsLoginRequest { + private String regNumber; // e.g. "2114251001" or "251001" + private String password; // e.g. "rit@2026" + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ImsLoginResponse { + private boolean success; + private String message; + private String token; // IMS Session / Bearer Token + private StudentInfo student; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class MockUserCredentialDto { + private String regNumber; + private String defaultPassword; + private String studentName; + private String department; + private String section; + } + + // ─── 1. Student & Department Details ───────────────────────── + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class StudentInfo { + private String studentId; // e.g. "2114251001" or "251001" + private String regNumber; // e.g. "2114251001" + private String name; // e.g. "Anbu Kathir" + private String email; // e.g. "251001@ritchennai.edu.in" + private String degree; // e.g. "B.E." + private String department; // e.g. "Computer Science & Engineering" + private String departmentCode; // e.g. "CSE" + private int year; // e.g. 1 + private int semester; // e.g. 2 + private String section; // e.g. "A" + private String batch; // e.g. "2025 - 2029" + private String regulation; // e.g. "2021 Regulation" + } + + // ─── 2. Class & Campus Location ───────────────────────────── + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ClassLocationInfo { + private String roomNumber; // e.g. "LH-204" + private String buildingName; // e.g. "Dr. APJ Abdul Kalam Academic Block" + private String floor; // e.g. "2nd Floor" + private String wing; // e.g. "East Wing" + private String landmark; // e.g. "Next to CSE Department Library, Opposite to Seminar Hall" + } + + // ─── 3. Faculty / Staff for this Class ─────────────────────── + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ClassFacultyMember { + private String subjectCode; // e.g. "CS3201" + private String subjectName; // e.g. "Data Structures & Algorithms" + private String facultyName; // e.g. "Dr. R. Arunkumar" + private String designation; // e.g. "Associate Professor" + private String email; // e.g. "arunkumar.r@ritchennai.edu.in" + private String officeLocation; // e.g. "APJ Block, Cabin 208-B" + private String phoneExtension; // e.g. "Ext. 312" + private String avatarUrl; // Optional photo + private boolean isClassIncharge; // true if Class Incharge + } + + // ─── 4. Timetable Period ───────────────────────────────────── + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class TimetablePeriod { + private int periodNumber; // 1 to 6 + private String timeSlot; // e.g. "08:45 AM - 09:40 AM" + private String startTime; // "08:45" + private String endTime; // "09:40" + private String subjectCode; // "CS3201" + private String subjectName; // "Data Structures & Algorithms" + private String type; // "THEORY" | "LAB" | "TUTORIAL" | "LIBRARY" + private String facultyName; // "Dr. R. Arunkumar" + private String venue; // "LH-204" or "CC-03 Lab" + private String status; // "UPCOMING" | "ONGOING" | "COMPLETED" + } + + // ─── 5. Daily Timetable ────────────────────────────────────── + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DaySchedule { + private String dayOfWeek; // "MONDAY", "TUESDAY", etc. + private LocalDate date; + private List periods; + } + + // ─── Complete Dashboard Aggregate ──────────────────────────── + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DashboardResponse { + private StudentInfo student; + private ClassLocationInfo classLocation; + private List facultyList; + private DaySchedule todaySchedule; + private boolean isMockData; + } +} diff --git a/backend/src/main/java/com/rit/portal/service/ims/ImsClient.java b/backend/src/main/java/com/rit/portal/service/ims/ImsClient.java new file mode 100644 index 0000000..42c513b --- /dev/null +++ b/backend/src/main/java/com/rit/portal/service/ims/ImsClient.java @@ -0,0 +1,15 @@ +package com.rit.portal.service.ims; + +import com.rit.portal.dto.ims.StudentDashboardDto.*; +import java.time.LocalDate; +import java.util.List; + +public interface ImsClient { + ImsLoginResponse authenticate(String regNumber, String password); + List getAvailableMockUsers(); + DashboardResponse getStudentDashboard(String studentId); + StudentInfo getStudentInfo(String studentId); + ClassLocationInfo getClassLocation(String department, String section, int semester); + List getClassFaculty(String department, String section, int semester); + DaySchedule getDaySchedule(String studentId, LocalDate date); +} diff --git a/backend/src/main/java/com/rit/portal/service/ims/MockImsClientImpl.java b/backend/src/main/java/com/rit/portal/service/ims/MockImsClientImpl.java new file mode 100644 index 0000000..cda44d5 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/service/ims/MockImsClientImpl.java @@ -0,0 +1,441 @@ +package com.rit.portal.service.ims; + +import com.rit.portal.dto.ims.StudentDashboardDto.*; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.*; + +@Service +@ConditionalOnProperty(name = "ims.client.type", havingValue = "mock", matchIfMissing = true) +public class MockImsClientImpl implements ImsClient { + + private final Map studentDb = new HashMap<>(); + private final Map userPasswords = new HashMap<>(); + private final Map locationDb = new HashMap<>(); + private final Map> facultyDb = new HashMap<>(); + private final List mockUserList = new ArrayList<>(); + + public MockImsClientImpl() { + seedMockData(); + } + + private void seedMockData() { + // ───────────────────────────────────────────────────────────── + // 1. MOCK STUDENT 1: 2114251001 (CSE - Section A, Year 1, Sem 2) + // ───────────────────────────────────────────────────────────── + StudentInfo s1 = StudentInfo.builder() + .studentId("2114251001") + .regNumber("2114251001") + .name("Anbu Kathir") + .email("251001@ritchennai.edu.in") + .degree("B.E.") + .department("Computer Science & Engineering") + .departmentCode("CSE") + .year(1) + .semester(2) + .section("A") + .batch("2025 - 2029") + .regulation("2021 Regulation") + .build(); + + studentDb.put("2114251001", s1); + studentDb.put("251001", s1); + userPasswords.put("2114251001", "rit@2026"); + userPasswords.put("251001", "rit@2026"); + + mockUserList.add(MockUserCredentialDto.builder() + .regNumber("2114251001") + .defaultPassword("rit@2026") + .studentName("Anbu Kathir") + .department("Computer Science & Engineering") + .section("Sec A") + .build()); + + locationDb.put("CSE_A_2", ClassLocationInfo.builder() + .roomNumber("LH-204") + .buildingName("Dr. APJ Abdul Kalam Academic Block") + .floor("2nd Floor") + .wing("East Wing") + .landmark("Next to CSE Department Library, Opposite to Seminar Hall") + .build()); + + facultyDb.put("CSE_A_2", List.of( + ClassFacultyMember.builder() + .subjectCode("CS3201") + .subjectName("Data Structures & Algorithms") + .facultyName("Dr. R. Arunkumar") + .designation("Associate Professor & Class Incharge") + .email("arunkumar.r@ritchennai.edu.in") + .officeLocation("APJ Block, Cabin 208-B") + .phoneExtension("Ext. 312") + .avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Arunkumar") + .isClassIncharge(true) + .build(), + ClassFacultyMember.builder() + .subjectCode("MA3251") + .subjectName("Discrete Mathematics") + .facultyName("Dr. S. Malathi") + .designation("Professor (Mathematics)") + .email("malathi.s@ritchennai.edu.in") + .officeLocation("Science & Humanities Block, 1st Floor") + .phoneExtension("Ext. 118") + .avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Malathi") + .isClassIncharge(false) + .build(), + ClassFacultyMember.builder() + .subjectCode("CS3202") + .subjectName("Digital Principles & System Design") + .facultyName("Dr. M. Balaji") + .designation("Assistant Professor") + .email("balaji.m@ritchennai.edu.in") + .officeLocation("APJ Block, Cabin 104") + .phoneExtension("Ext. 325") + .avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Balaji") + .isClassIncharge(false) + .build(), + ClassFacultyMember.builder() + .subjectCode("GE3251") + .subjectName("Environmental Sciences & Sustainability") + .facultyName("Dr. T. Gayathri") + .designation("Assistant Professor (Chemistry)") + .email("gayathri.t@ritchennai.edu.in") + .officeLocation("Science Block, Chemistry Cabin 3") + .phoneExtension("Ext. 142") + .avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Gayathri") + .isClassIncharge(false) + .build(), + ClassFacultyMember.builder() + .subjectCode("CS3271") + .subjectName("Data Structures Laboratory") + .facultyName("Dr. R. Arunkumar / Dr. K. Revathi") + .designation("Lab In-Charges") + .email("lab.cse@ritchennai.edu.in") + .officeLocation("Computing Center 3 (CC-03)") + .phoneExtension("Ext. 330") + .avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Revathi") + .isClassIncharge(false) + .build() + )); + + // ───────────────────────────────────────────────────────────── + // 2. MOCK STUDENT 2: 2114251002 (AI&DS - Section B, Year 1, Sem 2) + // ───────────────────────────────────────────────────────────── + StudentInfo s2 = StudentInfo.builder() + .studentId("2114251002") + .regNumber("2114251002") + .name("Priyadharshini M") + .email("251002@ritchennai.edu.in") + .degree("B.Tech.") + .department("Artificial Intelligence & Data Science") + .departmentCode("AIDS") + .year(1) + .semester(2) + .section("B") + .batch("2025 - 2029") + .regulation("2021 Regulation") + .build(); + + studentDb.put("2114251002", s2); + studentDb.put("251002", s2); + userPasswords.put("2114251002", "rit@2026"); + userPasswords.put("251002", "rit@2026"); + + mockUserList.add(MockUserCredentialDto.builder() + .regNumber("2114251002") + .defaultPassword("rit@2026") + .studentName("Priyadharshini M") + .department("AI & Data Science") + .section("Sec B") + .build()); + + locationDb.put("AIDS_B_2", ClassLocationInfo.builder() + .roomNumber("LH-301") + .buildingName("Sir Isaac Newton Innovation Block") + .floor("3rd Floor") + .wing("North Wing") + .landmark("Opposite AI Centre of Excellence, Near Lift Lobby") + .build()); + + facultyDb.put("AIDS_B_2", List.of( + ClassFacultyMember.builder() + .subjectCode("AD3251") + .subjectName("Data Structures Design") + .facultyName("Dr. N. Saravanan") + .designation("Associate Professor & Class Incharge") + .email("saravanan.n@ritchennai.edu.in") + .officeLocation("Newton Block, Cabin 302") + .phoneExtension("Ext. 401") + .avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Saravanan") + .isClassIncharge(true) + .build(), + ClassFacultyMember.builder() + .subjectCode("AD3252") + .subjectName("Foundations of Artificial Intelligence") + .facultyName("Prof. J. Daniel") + .designation("Assistant Professor") + .email("daniel.j@ritchennai.edu.in") + .officeLocation("Newton Block, Cabin 306") + .phoneExtension("Ext. 405") + .avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Daniel") + .isClassIncharge(false) + .build(), + ClassFacultyMember.builder() + .subjectCode("MA3251") + .subjectName("Probability and Statistics") + .facultyName("Dr. V. Lakshmi") + .designation("Professor (Mathematics)") + .email("lakshmi.v@ritchennai.edu.in") + .officeLocation("Science Block, Cabin 108") + .phoneExtension("Ext. 119") + .avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Lakshmi") + .isClassIncharge(false) + .build() + )); + + // ───────────────────────────────────────────────────────────── + // 3. MOCK STUDENT 3: 2114251003 (ECE - Section A, Year 1, Sem 2) + // ───────────────────────────────────────────────────────────── + StudentInfo s3 = StudentInfo.builder() + .studentId("2114251003") + .regNumber("2114251003") + .name("Rahul V") + .email("251003@ritchennai.edu.in") + .degree("B.E.") + .department("Electronics & Communication Engineering") + .departmentCode("ECE") + .year(1) + .semester(2) + .section("A") + .batch("2025 - 2029") + .regulation("2021 Regulation") + .build(); + + studentDb.put("2114251003", s3); + studentDb.put("251003", s3); + userPasswords.put("2114251003", "rit@2026"); + userPasswords.put("251003", "rit@2026"); + + mockUserList.add(MockUserCredentialDto.builder() + .regNumber("2114251003") + .defaultPassword("rit@2026") + .studentName("Rahul V") + .department("Electronics & Comm. Engg.") + .section("Sec A") + .build()); + + locationDb.put("ECE_A_2", ClassLocationInfo.builder() + .roomNumber("LH-105") + .buildingName("Rabindranath Tagore Academic Block") + .floor("1st Floor") + .wing("South Wing") + .landmark("Near Department Auditorium") + .build()); + + facultyDb.put("ECE_A_2", List.of( + ClassFacultyMember.builder() + .subjectCode("EC3251") + .subjectName("Circuit Analysis") + .facultyName("Dr. I. Chandra") + .designation("Professor & Class Incharge") + .email("chandra.i@ritchennai.edu.in") + .officeLocation("Tagore Block, Cabin 112") + .phoneExtension("Ext. 502") + .avatarUrl("https://api.dicebear.com/7.x/avataaars/svg?seed=Chandra") + .isClassIncharge(true) + .build() + )); + } + + @Override + public ImsLoginResponse authenticate(String regNumber, String password) { + if (regNumber == null || regNumber.isBlank()) { + return ImsLoginResponse.builder() + .success(false) + .message("Register Number is required.") + .build(); + } + + String cleanedReg = regNumber.trim(); + StudentInfo student = studentDb.get(cleanedReg); + + if (student == null) { + String digits = cleanedReg.replaceAll("[^0-9]", ""); + student = studentDb.get(digits); + } + + if (student == null) { + student = StudentInfo.builder() + .studentId(cleanedReg) + .regNumber(cleanedReg) + .name("Student (" + cleanedReg + ")") + .email(cleanedReg + "@ritchennai.edu.in") + .degree("B.E.") + .department("Computer Science & Engineering") + .departmentCode("CSE") + .year(1) + .semester(2) + .section("A") + .batch("2025 - 2029") + .regulation("2021 Regulation") + .build(); + studentDb.put(cleanedReg, student); + userPasswords.put(cleanedReg, "rit@2026"); + } + + String expectedPassword = userPasswords.getOrDefault(cleanedReg, "rit@2026"); + boolean isValid = password != null && ( + password.equals(expectedPassword) || + password.equals("rit@2026") || + password.equals("password") || + password.equals("123456") + ); + + if (!isValid) { + return ImsLoginResponse.builder() + .success(false) + .message("Invalid Register Number or Password. Default password: rit@2026") + .build(); + } + + String token = "ims_session_" + student.getRegNumber() + "_" + UUID.randomUUID().toString().substring(0, 8); + + return ImsLoginResponse.builder() + .success(true) + .message("IMS Login Successful") + .token(token) + .student(student) + .build(); + } + + @Override + public List getAvailableMockUsers() { + return Collections.unmodifiableList(mockUserList); + } + + @Override + public DashboardResponse getStudentDashboard(String studentId) { + StudentInfo student = getStudentInfo(studentId); + String classKey = student.getDepartmentCode() + "_" + student.getSection() + "_" + student.getSemester(); + + ClassLocationInfo location = locationDb.getOrDefault(classKey, locationDb.get("CSE_A_2")); + List faculty = facultyDb.getOrDefault(classKey, facultyDb.get("CSE_A_2")); + DaySchedule schedule = getDaySchedule(studentId, LocalDate.now()); + + return DashboardResponse.builder() + .student(student) + .classLocation(location) + .facultyList(faculty) + .todaySchedule(schedule) + .isMockData(true) + .build(); + } + + @Override + public StudentInfo getStudentInfo(String studentId) { + if (studentId == null || studentId.isBlank()) { + return studentDb.get("2114251001"); + } + String cleaned = studentId.trim(); + StudentInfo student = studentDb.get(cleaned); + if (student != null) return student; + + String digits = cleaned.replaceAll("[^0-9]", ""); + return studentDb.getOrDefault(digits, studentDb.get("2114251001")); + } + + @Override + public ClassLocationInfo getClassLocation(String department, String section, int semester) { + String key = department + "_" + section + "_" + semester; + return locationDb.getOrDefault(key, locationDb.get("CSE_A_2")); + } + + @Override + public List getClassFaculty(String department, String section, int semester) { + String key = department + "_" + section + "_" + semester; + return facultyDb.getOrDefault(key, facultyDb.get("CSE_A_2")); + } + + @Override + public DaySchedule getDaySchedule(String studentId, LocalDate date) { + DayOfWeek day = date.getDayOfWeek(); + String dayName = (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) ? "MONDAY" : day.name(); + + LocalTime now = LocalTime.now(); + List periods = buildTimetableForDay(studentId, dayName, now); + + return DaySchedule.builder() + .dayOfWeek(dayName) + .date(date) + .periods(periods) + .build(); + } + + private List buildTimetableForDay(String studentId, String dayOfWeek, LocalTime now) { + List list = new ArrayList<>(); + + String[][] slots = { + {"1", "08:45 AM - 09:40 AM", "08:45", "09:40"}, + {"2", "09:40 AM - 10:35 AM", "09:40", "10:35"}, + {"3", "10:50 AM - 11:45 AM", "10:50", "11:45"}, + {"4", "11:45 AM - 12:40 PM", "11:45", "12:40"}, + {"5", "01:30 PM - 02:25 PM", "13:30", "14:25"}, + {"6", "02:25 PM - 03:20 PM", "14:25", "15:20"} + }; + + boolean isAids = studentId != null && studentId.contains("251002"); + String room = isAids ? "LH-301" : "LH-204"; + String lab = isAids ? "AI-02 Lab" : "CC-03 Lab"; + + String[][] subjects; + if ("WEDNESDAY".equalsIgnoreCase(dayOfWeek) || "FRIDAY".equalsIgnoreCase(dayOfWeek)) { + subjects = new String[][]{ + {isAids ? "AD3251" : "CS3201", isAids ? "Data Structures Design" : "Data Structures & Algorithms", "THEORY", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", room}, + {"MA3251", isAids ? "Probability & Stats" : "Discrete Mathematics", "THEORY", isAids ? "Dr. V. Lakshmi" : "Dr. S. Malathi", room}, + {isAids ? "AD3271" : "CS3271", isAids ? "AI & DS Practical Lab" : "Data Structures Laboratory", "LAB", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", lab}, + {isAids ? "AD3271" : "CS3271", isAids ? "AI & DS Practical Lab" : "Data Structures Laboratory", "LAB", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", lab}, + {isAids ? "AD3252" : "CS3202", isAids ? "Foundations of AI" : "Digital Principles & System Design", "THEORY", isAids ? "Prof. J. Daniel" : "Dr. M. Balaji", room}, + {"GE3251", "Environmental Sciences", "THEORY", "Dr. T. Gayathri", room} + }; + } else { + subjects = new String[][]{ + {"MA3251", isAids ? "Probability & Stats" : "Discrete Mathematics", "THEORY", isAids ? "Dr. V. Lakshmi" : "Dr. S. Malathi", room}, + {isAids ? "AD3251" : "CS3201", isAids ? "Data Structures Design" : "Data Structures & Algorithms", "THEORY", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", room}, + {isAids ? "AD3252" : "CS3202", isAids ? "Foundations of AI" : "Digital Principles & System Design", "THEORY", isAids ? "Prof. J. Daniel" : "Dr. M. Balaji", room}, + {"GE3251", "Environmental Sciences", "THEORY", "Dr. T. Gayathri", room}, + {isAids ? "AD3251" : "CS3201", isAids ? "AI Problem Solving Tutorial" : "DSA Tutorial & Problem Solving", "TUTORIAL", isAids ? "Dr. N. Saravanan" : "Dr. R. Arunkumar", room}, + {"LIB", "Digital Library & Research Hours", "LIBRARY", "Chief Librarian", "Central Library - 2nd Floor"} + }; + } + + for (int i = 0; i < slots.length; i++) { + LocalTime start = LocalTime.parse(slots[i][2]); + LocalTime end = LocalTime.parse(slots[i][3]); + + String status = "UPCOMING"; + if (now.isAfter(end)) { + status = "COMPLETED"; + } else if (now.isAfter(start) && now.isBefore(end)) { + status = "ONGOING"; + } + + list.add(TimetablePeriod.builder() + .periodNumber(Integer.parseInt(slots[i][0])) + .timeSlot(slots[i][1]) + .startTime(slots[i][2]) + .endTime(slots[i][3]) + .subjectCode(subjects[i][0]) + .subjectName(subjects[i][1]) + .type(subjects[i][2]) + .facultyName(subjects[i][3]) + .venue(subjects[i][4]) + .status(status) + .build()); + } + + return list; + } +} diff --git a/backend/src/main/java/com/rit/portal/service/ims/RealImsClientImpl.java b/backend/src/main/java/com/rit/portal/service/ims/RealImsClientImpl.java new file mode 100644 index 0000000..20da3c8 --- /dev/null +++ b/backend/src/main/java/com/rit/portal/service/ims/RealImsClientImpl.java @@ -0,0 +1,98 @@ +package com.rit.portal.service.ims; + +import com.rit.portal.dto.ims.StudentDashboardDto.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@Service +@ConditionalOnProperty(name = "ims.client.type", havingValue = "real") +public class RealImsClientImpl implements ImsClient { + + @Value("${ims.base-url:https://api.ims.ritchennai.edu.in}") + private String imsBaseUrl; + + @Value("${ims.api-key:}") + private String imsApiKey; + + private final RestTemplate restTemplate = new RestTemplate(); + + private HttpHeaders createHeaders(String studentId) { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Nexus-App-Secret", imsApiKey); + if (studentId != null && !studentId.isBlank()) { + headers.set("X-Student-Id", studentId); + } + headers.set("Accept", "application/json"); + return headers; + } + + @Override + public ImsLoginResponse authenticate(String regNumber, String password) { + String url = imsBaseUrl + "/api/v1/auth/student-login"; + ImsLoginRequest request = ImsLoginRequest.builder() + .regNumber(regNumber) + .password(password) + .build(); + HttpEntity entity = new HttpEntity<>(request, createHeaders(null)); + ResponseEntity resp = restTemplate.exchange(url, HttpMethod.POST, entity, ImsLoginResponse.class); + return resp.getBody(); + } + + @Override + public List getAvailableMockUsers() { + return Collections.emptyList(); + } + + @Override + public DashboardResponse getStudentDashboard(String studentId) { + String url = imsBaseUrl + "/api/v1/dashboard/me"; + HttpEntity entity = new HttpEntity<>(createHeaders(studentId)); + ResponseEntity resp = restTemplate.exchange(url, HttpMethod.GET, entity, DashboardResponse.class); + DashboardResponse body = resp.getBody(); + if (body != null) { + body.setMockData(false); + } + return body; + } + + @Override + public StudentInfo getStudentInfo(String studentId) { + String url = imsBaseUrl + "/api/v1/students/me"; + HttpEntity entity = new HttpEntity<>(createHeaders(studentId)); + ResponseEntity resp = restTemplate.exchange(url, HttpMethod.GET, entity, StudentInfo.class); + return resp.getBody(); + } + + @Override + public ClassLocationInfo getClassLocation(String department, String section, int semester) { + String url = String.format("%s/api/v1/classes/location?dept=%s&sec=%s&sem=%d", imsBaseUrl, department, section, semester); + HttpEntity entity = new HttpEntity<>(createHeaders(null)); + return restTemplate.exchange(url, HttpMethod.GET, entity, ClassLocationInfo.class).getBody(); + } + + @Override + public List getClassFaculty(String department, String section, int semester) { + String url = String.format("%s/api/v1/classes/faculty?dept=%s&sec=%s&sem=%d", imsBaseUrl, department, section, semester); + HttpEntity entity = new HttpEntity<>(createHeaders(null)); + ClassFacultyMember[] array = restTemplate.exchange(url, HttpMethod.GET, entity, ClassFacultyMember[].class).getBody(); + return array != null ? Arrays.asList(array) : Collections.emptyList(); + } + + @Override + public DaySchedule getDaySchedule(String studentId, LocalDate date) { + String url = String.format("%s/api/v1/timetable/me?date=%s", imsBaseUrl, date.toString()); + HttpEntity entity = new HttpEntity<>(createHeaders(studentId)); + return restTemplate.exchange(url, HttpMethod.GET, entity, DaySchedule.class).getBody(); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index e2eb212..da2aad2 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -15,6 +15,18 @@ server.port=8085 # ─── GOOGLE OAUTH2 CONFIGURATION ─── google.client.id=${GOOGLE_CLIENT_ID:81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com} -# ─── ADMIN PORTAL CREDENTIALS ─── +# ─── ADMIN PORTAL MULTI-ROLE CREDENTIALS ─── +admin.transport.username=${TRANSPORT_USERNAME:Transport} +admin.transport.password=${TRANSPORT_PASSWORD:RIT@2026} +admin.super.username=${SUPER_ADMIN_USERNAME:Admin} +admin.super.password=${SUPER_ADMIN_PASSWORD:RIT@2026} admin.username=${ADMIN_USERNAME:ritadmin} admin.password=${ADMIN_PASSWORD:ritadmin2026} + + +# ─── IMS INTEGRATION CONFIGURATION ─── +# Options: "mock" (active now for testing & simulation) | "real" (for live college IMS) +ims.client.type=mock +ims.base-url=${IMS_BASE_URL:https://api.ims.ritchennai.edu.in} +ims.api-key=${IMS_API_KEY:rit_nexus_vps_secret_key_2026} + diff --git a/src/App.tsx b/src/App.tsx index 1e72303..18f7500 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import DevCollab from '@/pages/DevCollab/DevCollab'; import LeetcodeLeaderboard from '@/pages/LeetcodeLeaderboard/LeetcodeLeaderboard'; import AdminDashboard from '@/pages/Admin/AdminDashboard'; import Raise from '@/pages/Raise/Raise'; +import StudentDashboard from '@/pages/StudentDashboard/StudentDashboard'; const queryClient = new QueryClient({ defaultOptions: { @@ -61,6 +62,9 @@ function App() { } /> } /> + {/* Student Dashboard — Timetable, Staff, Location (IMS Reg No & Password) */} + } /> + {/* Protected Routes — Require Google Sign-In */} )} -
+
+ setIsProfileOpen(false)} + className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-orange-500/15 hover:border-orange-500/20 text-xs font-semibold transition-all cursor-pointer" + > + + My Student Dashboard + + + + {/* Quick Demo Credentials */} +
+ + Quick Role-Based Logins: + +
+ + +
+
+ +
+
+ ); + } + + // ───────────────────────────────────────────────────────────── + // RENDER: AUTHENTICATED ADMIN DASHBOARD + // ───────────────────────────────────────────────────────────── + const isTransportOnly = userRole === 'ROLE_TRANSPORT'; + + return ( +
{/* Toast Notification */} {toastMessage && (
)} -
- {/* Navigation Breadcrumb */} -
-
-
- +
+ + {/* Top Header Bar */} +
+
+
+
+ {isTransportOnly ? : } +
-

- RIT Portal Admin Console -

-

Manage deployment email subscribers & system settings

+
+

+ {isTransportOnly ? 'Transport Fleet Administration' : 'RIT Closed-Loop Admin Console'} +

+ + {isTransportOnly ? 'Transport Manager' : 'Super Admin'} + +
+

+ {isTransportOnly + ? 'Manage college bus routes, stop timings, driver live tracking, and route allocation.' + : 'Manage bus fleet, Telegram Q&A senior responders, study materials, and system alerts.'} +

- - ← Back to Main Site - +
+ + ← Main Site + + +
- {!isLoggedIn ? ( - /* Login Card */ - -
-
- -
-

Admin Authentication

-

Enter your admin credentials to access subscriber controls

-
+ {/* Tab Navigation */} + {!isTransportOnly && ( +
+ {[ + { id: 'transport', label: 'Bus Fleet & Routes', icon: Bus }, + { id: 'telegram', label: 'Telegram Q&A Seniors', icon: Bot }, + { id: 'notes', label: 'Notes & Study Materials', icon: BookOpen }, + { id: 'community', label: 'Community Q&A Moderation', icon: MessageCircle }, + { id: 'subscribers', label: 'Deployment Email Alerts', icon: Mail }, + ].map((tab) => { + const Icon = tab.icon; + const isActive = activeTab === tab.id; + return ( + + ); + })} +
+ )} - {loginError && ( -
- {loginError} -
- )} - -
+ {/* ───────────────────────────────────────────────────────────── */} + {/* TAB 1: TRANSPORT & BUS FLEET MANAGEMENT */} + {/* ───────────────────────────────────────────────────────────── */} + {activeTab === 'transport' && ( +
+
- -
- - setUsername(e.target.value)} - placeholder="Enter username (e.g. ritadmin)" - className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-slate-800 border border-slate-700 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-orange-500 transition-colors" - /> -
-
- -
- -
- - setPassword(e.target.value)} - placeholder="Enter password" - className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-slate-800 border border-slate-700 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-orange-500 transition-colors" - /> -
-
- - - - - ) : ( - /* Authenticated Admin Dashboard */ - - {/* Dashboard Header Bar */} -
-
-
- - 🟢 Authenticated Session - - User: ritadmin -
-

Deployment Email Subscribers

+

+ + College Bus Routes ({routes.length} Active) +

- Every git push automatically sends live health reports to all emails listed below. + Changes made here update the student Bus Routes directory and driver tracking app instantly.

+
+ + +
+
+ + {loadingRoutes ? ( +
Loading bus routes from database...
+ ) : routes.length === 0 ? ( +
+ No routes configured yet. Click "Add New Route" to create your first bus route! +
+ ) : ( +
+ {routes.map((route) => ( + +
+ {/* Card Header */} +
+
+ + {route.number} + + {route.name} +
+ +
+ + +
+
+ + {/* Route Path */} +
+
+ Origin: + {route.from} ({route.departureTime}) +
+
+ Destination: + {route.to} ({route.arrivalTime}) +
+
+ + {/* Stops Preview */} +
+ + Intermediate Stops ({route.stops?.length || 0}) + +
+ {route.stops && route.stops.length > 0 ? ( + route.stops.map((stop, i) => ( +
+ {stop.name} + {stop.time} +
+ )) + ) : ( +

No intermediate stops configured

+ )} +
+
+
+ +
+ Driver PIN: RITDRIVER + + + Live Sync + +
+
+ ))} +
+ )} +
+ )} + + {/* ───────────────────────────────────────────────────────────── */} + {/* TAB 2: TELEGRAM Q&A SENIORS & BOT CONFIGURATION */} + {/* ───────────────────────────────────────────────────────────── */} + {activeTab === 'telegram' && ( +
+
+ + {/* Left 7 Cols: Active Senior Responders */} +
+
+

+ + Telegram Senior Responders (Q&A Network) +

+

+ When a fresher posts a question on the portal, these Telegram users receive instant alert messages and can answer directly from Telegram. +

+
+ + {/* Add Senior Form */} +
+ Add New Senior Responder +
+ setNewHelperChatId(e.target.value)} + placeholder="Telegram Chat ID (e.g. 971749136)" + className="bg-slate-900 border border-white/10 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder-slate-500 font-mono outline-none focus:border-orange-500" + required + /> + setNewHelperName(e.target.value)} + placeholder="Senior Name / Note (Optional)" + className="bg-slate-900 border border-white/10 rounded-xl px-3.5 py-2.5 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500" + /> +
+ +
+ + {/* Active Seniors List */} +
+ + Active Senior Chat IDs ({telegramConfig?.helper_chat_ids?.length || 0}) + + +
+ {telegramConfig?.helper_chat_ids && telegramConfig.helper_chat_ids.length > 0 ? ( + telegramConfig.helper_chat_ids.map((chatId) => ( +
+
+
+ TG +
+
+

Chat ID: {chatId}

+

Receives questions from freshers in real-time

+
+
+ + +
+ )) + ) : ( +

No senior helpers configured in config.json.

+ )} +
+
+
+ + {/* Right 5 Cols: How-To Guide & Bot Tokens */} +
+ {/* Guide Card */} +
+

+ + How Seniors Get Their Chat ID +

+
    +
  1. Open Telegram and search for @userinfobot.
  2. +
  3. Click Start or send any message.
  4. +
  5. Copy the numeric Id (e.g. 971749136).
  6. +
  7. Enter that Chat ID in the form on the left and click Add.
  8. +
+
+ + {/* Live Config Editor */} + {telegramConfig && ( +
+

+ + Live Telegram Bot Config (VPS) +

+ +
+ + setTelegramConfig({ ...telegramConfig, community_bot_token: e.target.value })} + className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-xs font-mono text-white outline-none focus:border-orange-500" + /> +
+ +
+ + setTelegramConfig({ ...telegramConfig, spring_backend_url: e.target.value })} + className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-xs font-mono text-white outline-none focus:border-orange-500" + /> +
+ + +
+ )} +
+ +
+
+ )} + + {/* ───────────────────────────────────────────────────────────── */} + {/* TAB 3: NOTES & STUDY MATERIALS */} + {/* ───────────────────────────────────────────────────────────── */} + {activeTab === 'notes' && ( +
+
+
+

+ + Notes & Study Materials Library ({notes.length}) +

+

View and remove student-accessible notes and previous year question papers.

+
+
- {/* Add New Subscriber Form */} -
-

- - Add New Email Recipient + {loadingNotes ? ( +
Loading notes from database...
+ ) : notes.length === 0 ? ( +
+ No study materials found in database. +
+ ) : ( +
+ {notes.map((note) => ( +
+
+
+ {note.title} + + {note.fileType} + +
+

+ {note.subject} • {note.department} (Sem {note.semester}) +

+
+ +
+ + Preview + + +
+
+ ))} +
+ )} +

+ )} + + {/* ───────────────────────────────────────────────────────────── */} + {/* TAB 4: COMMUNITY QUESTIONS MODERATION */} + {/* ───────────────────────────────────────────────────────────── */} + {activeTab === 'community' && ( +
+
+
+

+ + Community Q&A Questions ({questions.length}) +

+

Moderate and delete inappropriate or spam questions.

+
+ + +
+ + {loadingQuestions ? ( +
Loading community questions...
+ ) : questions.length === 0 ? ( +
+ No community questions found in database. +
+ ) : ( +
+ {questions.map((q) => ( +
+
+

{q.title}

+

{q.body}

+
+ Asked by: {q.authorName} + • + Votes: {q.votes} +
+
+ + +
+ ))} +
+ )} +
+ )} + + {/* ───────────────────────────────────────────────────────────── */} + {/* TAB 5: DEPLOYMENT EMAIL SUBSCRIBERS */} + {/* ───────────────────────────────────────────────────────────── */} + {activeTab === 'subscribers' && ( +
+
+

+ + Add Deployment Alert Recipient

+

+ Every git push automatically sends live health reports to all emails listed below. +

@@ -285,78 +1095,239 @@ export default function AdminDashboard() { type="email" value={newEmail} onChange={(e) => setNewEmail(e.target.value)} - placeholder="e.g. successor@ritchennai.edu.in" - className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-slate-800 border border-slate-700 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-orange-500 transition-colors" + placeholder="e.g. admin@ritchennai.edu.in" + className="w-full pl-10 pr-4 py-3 rounded-xl bg-slate-950 border border-white/10 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500" />
- {/* Active Subscribers List Table */} -
+
-
- -

- Active Email Subscribers ({recipients.length}) -

-
- -
- {loadingRecipients ? ( -
Loading subscribers list...
- ) : recipients.length === 0 ? ( -
- No email subscribers found. Use the form above to add a recipient! -
- ) : ( -
- {recipients.map((email, idx) => ( -
-
-
- -
-
-

{email}

-

Receives deployment reports on git push

-
+
+ {recipients.map((email, idx) => ( +
+
+
+
- - +

{email}

- ))} -
- )} + +
+ ))} +
- +
)} +
+ + {/* ───────────────────────────────────────────────────────────── */} + {/* MODAL: CREATE / EDIT BUS ROUTE */} + {/* ───────────────────────────────────────────────────────────── */} + + {isRouteModalOpen && ( +
+ +
+

+ + {editingRoute ? `Edit Route ${editingRoute.number}` : 'Create New Bus Route'} +

+ +
+ +
+
+
+ + setRouteFormData({ ...routeFormData, number: e.target.value })} + placeholder="e.g. RIT-05" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs font-mono text-white outline-none focus:border-orange-500" + required + /> +
+ +
+ + setRouteFormData({ ...routeFormData, name: e.target.value })} + placeholder="e.g. Tambaram Super Express" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white outline-none focus:border-orange-500" + required + /> +
+
+ +
+
+ + setRouteFormData({ ...routeFormData, from: e.target.value })} + placeholder="e.g. Tambaram" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white outline-none focus:border-orange-500" + required + /> +
+ +
+ + setRouteFormData({ ...routeFormData, to: e.target.value })} + placeholder="e.g. RIT Campus" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white outline-none focus:border-orange-500" + required + /> +
+
+ +
+
+ + setRouteFormData({ ...routeFormData, departureTime: e.target.value })} + placeholder="07:00 AM" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white font-mono outline-none focus:border-orange-500" + /> +
+ +
+ + setRouteFormData({ ...routeFormData, arrivalTime: e.target.value })} + placeholder="08:30 AM" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs text-white font-mono outline-none focus:border-orange-500" + /> +
+ +
+ + setRouteFormData({ ...routeFormData, color: e.target.value })} + placeholder="#FF6B00" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-xs font-mono text-white outline-none focus:border-orange-500" + /> +
+
+ + {/* Stops Builder */} +
+
+ + +
+ +
+ {routeFormData.stops.map((stop, i) => ( +
+ {i + 1}. + { + const updated = [...routeFormData.stops]; + updated[i].name = e.target.value; + setRouteFormData({ ...routeFormData, stops: updated }); + }} + placeholder="Stop Name (e.g. Chrompet)" + className="flex-1 bg-slate-950 border border-white/10 rounded-xl px-3 py-1.5 text-xs text-white outline-none focus:border-orange-500" + /> + { + const updated = [...routeFormData.stops]; + updated[i].time = e.target.value; + setRouteFormData({ ...routeFormData, stops: updated }); + }} + placeholder="07:20 AM" + className="w-24 bg-slate-950 border border-white/10 rounded-xl px-2.5 py-1.5 text-xs text-white font-mono outline-none focus:border-orange-500" + /> + +
+ ))} +
+
+ +
+ + +
+
+
+
+ )} +
); } diff --git a/src/pages/Faculty/Faculty.tsx b/src/pages/Faculty/Faculty.tsx index 16907d0..e5be0db 100644 --- a/src/pages/Faculty/Faculty.tsx +++ b/src/pages/Faculty/Faculty.tsx @@ -1,10 +1,11 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { motion } from 'framer-motion'; -import { Search, Users, Filter, ArrowUpDown, BookOpen, ChevronRight, AlertTriangle } from 'lucide-react'; +import { Search, Users, Filter, ArrowUpDown, BookOpen, ChevronRight, AlertTriangle, Sparkles, X } from 'lucide-react'; import FacultyCard from '@/components/FacultyCard/FacultyCard'; import { StaggerContainer, StaggerItem } from '@/components/AnimatedContainer/AnimatedContainer'; import { FACULTY_DATA, DEPARTMENTS } from '@/constants'; -import { Link } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; +import { getStoredImsSession } from '@/services/imsService'; const DEPT_MAP: Record = { 'Computer Science & Engineering': { abrv: 'CSE', full: 'Computer Science and Engineering' }, @@ -23,11 +24,57 @@ const DEPT_MAP: Record = { 'M.E. (VLSI Design)': { abrv: 'MEVLSI', full: 'M.E. (VLSI Design)' }, }; +function normalizeDepartmentName(input?: string | null): string | null { + if (!input) return null; + const cleaned = input.trim(); + + // Exact match + if (DEPARTMENTS.includes(cleaned)) return cleaned; + + // Search by abbreviation or substring + const upper = cleaned.toUpperCase(); + for (const [full, info] of Object.entries(DEPT_MAP)) { + if (info.abrv.toUpperCase() === upper || full.toUpperCase() === upper || full.toLowerCase().includes(cleaned.toLowerCase())) { + return full; + } + } + + return null; +} + export default function Faculty() { + const [searchParams] = useSearchParams(); const [searchFaculty, setSearchFaculty] = useState(''); - const [selectedDept, setSelectedDept] = useState('All Departments'); const [sortBy, setSortBy] = useState('Name A-Z'); + // Initial department detection (from URL query string or logged-in IMS student session) + const initialDepartment = useMemo(() => { + const queryDept = searchParams.get('dept'); + const matchedQuery = normalizeDepartmentName(queryDept); + if (matchedQuery) return matchedQuery; + + const session = getStoredImsSession(); + if (session?.student?.department) { + const matchedSession = normalizeDepartmentName(session.student.department); + if (matchedSession) return matchedSession; + } + + return 'All Departments'; + }, [searchParams]); + + const [selectedDept, setSelectedDept] = useState(initialDepartment); + + // Update selectedDept if URL parameter changes + useEffect(() => { + setSelectedDept(initialDepartment); + }, [initialDepartment]); + + const studentSession = useMemo(() => getStoredImsSession(), []); + const isStudentDeptAutoFiltered = selectedDept !== 'All Departments' && ( + selectedDept === studentSession?.student?.department || + searchParams.has('dept') + ); + const filteredAndSortedFaculty = useMemo(() => { const filtered = FACULTY_DATA.filter((f) => { const searchLower = searchFaculty.trim().toLowerCase(); @@ -96,7 +143,36 @@ export default function Faculty() {
-
+
+ + {/* Active Student Department Filter Badge */} + {isStudentDeptAutoFiltered && ( + +
+
+ +
+
+
Student Department Auto-Filter
+
+ Showing faculty for your department: {selectedDept} +
+
+
+ +
+ )} + {/* Under Development Banner */} + {/* Faculty Hero Section */}
@@ -153,14 +230,23 @@ export default function Faculty() { { abrv: 'MEVLSI', full: 'M.E. (VLSI Design)', color: 'rose' }, ].filter(d => FACULTY_DATA.filter(f => f.department === d.full).length > 0).map(d => { const count = FACULTY_DATA.filter(f => f.department === d.full).length; + const isSelected = selectedDept === d.full; return ( -
+
+ {count} + ); })}
@@ -256,7 +342,7 @@ export default function Faculty() { 👥 diff --git a/src/pages/StudentDashboard/StudentDashboard.tsx b/src/pages/StudentDashboard/StudentDashboard.tsx new file mode 100644 index 0000000..451b639 --- /dev/null +++ b/src/pages/StudentDashboard/StudentDashboard.tsx @@ -0,0 +1,582 @@ +import { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + GraduationCap, Clock, MapPin, Users, + Calendar, Building, ShieldCheck, + Sparkles, KeyRound, ArrowRight, LogOut, + AlertCircle, User, HelpCircle, Compass, ExternalLink +} from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { useAuth } from '@/context/AuthContext'; +import { + fetchStudentDashboard, + loginWithIms, + fetchMockImsUsers, + getStoredImsSession, + clearImsSession, + type StudentDashboardData, + type MockUserCredential +} from '@/services/imsService'; + +export default function StudentDashboard() { + const { user } = useAuth(); + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState<'timetable' | 'location'>('timetable'); + + // IMS Authentication Session State + const [imsSession, setImsSession] = useState<{ token: string; student: any } | null>(getStoredImsSession()); + const [regNumberInput, setRegNumberInput] = useState(''); + const [passwordInput, setPasswordInput] = useState(''); + const [isLoggingIn, setIsLoggingIn] = useState(false); + const [loginError, setLoginError] = useState(null); + const [mockUsers, setMockUsers] = useState([]); + const [showDemoBox, setShowDemoBox] = useState(false); + + // Load available mock accounts for demo helper + useEffect(() => { + fetchMockImsUsers().then((users) => { + setMockUsers(users); + if (users.length > 0 && !regNumberInput) { + setRegNumberInput(users[0].regNumber); + setPasswordInput(users[0].defaultPassword); + } + }); + }, []); + + const loadDashboard = async () => { + if (!imsSession?.student?.regNumber) return; + setIsLoading(true); + setError(null); + try { + const result = await fetchStudentDashboard(imsSession.student.regNumber, user?.email); + setData(result); + } catch (err: any) { + console.error('Error loading student dashboard:', err); + setError(err?.message || 'Unable to fetch student records from IMS.'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (imsSession) { + loadDashboard(); + } else { + setData(null); + } + }, [imsSession]); + + const handleImsLogin = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); + if (!regNumberInput.trim() || !passwordInput.trim()) { + setLoginError('Please enter both Register Number and Password.'); + return; + } + + setIsLoggingIn(true); + setLoginError(null); + + try { + const res = await loginWithIms(regNumberInput.trim(), passwordInput.trim()); + if (res.success && res.token && res.student) { + setImsSession({ token: res.token, student: res.student }); + } else { + setLoginError(res.message || 'Invalid Register Number or Password.'); + } + } catch (err: any) { + setLoginError(err?.message || 'Failed to authenticate with IMS server.'); + } finally { + setIsLoggingIn(false); + } + }; + + const handleQuickFillMock = (mock: MockUserCredential) => { + setRegNumberInput(mock.regNumber); + setPasswordInput(mock.defaultPassword); + }; + + const handleLogoutIms = () => { + clearImsSession(); + setImsSession(null); + setData(null); + }; + + // ───────────────────────────────────────────────────────────── + // VIEW 1: IMS STUDENT LOGIN PORTAL (When not signed in to IMS) + // ───────────────────────────────────────────────────────────── + if (!imsSession) { + return ( +
+
+ +
+
+
+ +
+
+

+ IMS Student Portal Sign In +

+

+ Enter your official college Register Number and Password to access your timetable and assigned classroom venue. +

+
+ + {/* Login Form Card */} + + {loginError && ( +
+ + {loginError} +
+ )} + +
+
+ + setRegNumberInput(e.target.value)} + placeholder="e.g. 2114251001" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono" + required + /> +
+ +
+ + setPasswordInput(e.target.value)} + placeholder="••••••••" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono" + required + /> +
+ + +
+ + {/* Expandable Demo Helper */} +
+ + + + {showDemoBox && ( + +

+ Click any sample student below to auto-fill the login form: +

+
+ {mockUsers.map((mock) => ( + + ))} +
+
+ )} +
+
+
+
+
+ ); + } + + // ───────────────────────────────────────────────────────────── + // VIEW 2: LOGGED-IN STUDENT DASHBOARD + // ───────────────────────────────────────────────────────────── + const classInchargeMember = data?.facultyList.find((f) => f.isClassIncharge); + + return ( +
+
+ + {/* ─── Top Banner ─── */} +
+
+
+ +
+
+
+ +
+
+ +
+
+

+ {data?.student.name || imsSession.student.name} +

+ + Authenticated Student + +
+ +
+ Reg No: {data?.student.regNumber || imsSession.student.regNumber} + • + {data?.student.degree} {data?.student.department} + • + Year {data?.student.year}, Sem {data?.student.semester} ({data?.student.section}) +
+
+
+ +
+ + Faculty Directory + + + +
+
+ + {/* ─── Key Stats Quick Bar ─── */} + {data && ( +
+
+
+ +
+
+

Assigned Room

+

{data.classLocation.roomNumber}

+

{data.classLocation.floor}

+
+
+ +
+
+ +
+
+

Class Incharge

+

+ {classInchargeMember?.facultyName.split(' ')[0] || 'Assigned'} +

+

+ {classInchargeMember?.officeLocation || 'Staff Cabin'} +

+
+
+ +
+
+ +
+
+

Branch & Section

+

{data.student.departmentCode} ({data.student.section})

+

Semester {data.student.semester}

+
+
+ +
+
+ +
+
+

Academic Batch

+

{data.student.batch}

+

{data.student.regulation}

+
+
+
+ )} + + {/* ─── Navigation Tabs ─── */} +
+
+ {[ + { id: 'timetable', label: "Today's Timetable", icon: Clock }, + { id: 'location', label: 'Class & Building Location', icon: MapPin }, + ].map((tab) => { + const Icon = tab.icon; + const isActive = activeTab === tab.id; + return ( + + ); + })} +
+ + {data && ( + + + View {data.student.departmentCode} Faculty Directory + + + )} +
+ + {/* ─── Main Content Tabs ─── */} + {isLoading ? ( +
+
+

Connecting to IMS services...

+
+ ) : error ? ( +
+

Error connecting to IMS Gateway

+

{error}

+ +
+ ) : data ? ( +
+ {/* ─── 1. TIMETABLE TAB ─── */} + {activeTab === 'timetable' && ( +
+
+
+ + + {data.todaySchedule.dayOfWeek} SCHEDULE ({data.todaySchedule.date}) + +
+
+ + + Live Status Active + +
+
+ +
+ {data.todaySchedule.periods.map((period) => { + const isOngoing = period.status === 'ONGOING'; + const isCompleted = period.status === 'COMPLETED'; + + return ( + + {/* Header Badge */} +
+
+ + P{period.periodNumber} + + + + {period.timeSlot} + +
+ + + {period.status} + +
+ + {/* Subject Details */} +
+
+ + {period.subjectCode} + + + {period.type} + +
+

+ {period.subjectName} +

+
+ + {/* Footer / Instructor & Venue */} +
+ {period.facultyName} + + + {period.venue} + +
+
+ ); + })} +
+
+ )} + + {/* ─── 2. CLASS LOCATION TAB ─── */} + {activeTab === 'location' && ( +
+
+
+

+ + Classroom & Section Venue +

+

+ Official lecture hall allocation from Academic Registrar. +

+
+ +
+
+
+

Room Number

+

{data.classLocation.roomNumber}

+
+ + {data.student.section} Section + +
+ +
+
+ Building Block: + {data.classLocation.buildingName} +
+
+ Floor & Wing: + {data.classLocation.floor}, {data.classLocation.wing} +
+
+ Landmark Directions: + {data.classLocation.landmark} +
+
+
+
+ +
+
+

+ + Academic Department Details +

+

+ Official department profile, curriculum regulation, and academic guidelines for batch {data.student.batch}. +

+ +
+
+

Degree & Branch

+

{data.student.degree} {data.student.departmentCode}

+
+
+

Regulation

+

{data.student.regulation}

+
+
+

Academic Year

+

Year {data.student.year} (Sem {data.student.semester})

+
+
+

Batch Cohort

+

{data.student.batch}

+
+
+
+ +
+ +

+ Need classroom adjustment? Contact your Class Incharge or Head of Department. +

+
+
+
+ )} +
+ ) : null} +
+
+ ); +} diff --git a/src/services/imsService.ts b/src/services/imsService.ts new file mode 100644 index 0000000..394d93a --- /dev/null +++ b/src/services/imsService.ts @@ -0,0 +1,419 @@ +import { getBackendUrl } from '@/lib/utils'; + +export interface StudentInfo { + studentId: string; + regNumber: string; + name: string; + email: string; + degree: string; + department: string; + departmentCode: string; + year: number; + semester: number; + section: string; + batch: string; + regulation: string; +} + +export interface ClassLocationInfo { + roomNumber: string; + buildingName: string; + floor: string; + wing: string; + landmark: string; +} + +export interface ClassFacultyMember { + subjectCode: string; + subjectName: string; + facultyName: string; + designation: string; + email: string; + officeLocation: string; + phoneExtension: string; + avatarUrl?: string; + isClassIncharge: boolean; +} + +export interface TimetablePeriod { + periodNumber: number; + timeSlot: string; + startTime: string; + endTime: string; + subjectCode: string; + subjectName: string; + type: 'THEORY' | 'LAB' | 'TUTORIAL' | 'LIBRARY'; + facultyName: string; + venue: string; + status: 'UPCOMING' | 'ONGOING' | 'COMPLETED'; +} + +export interface DaySchedule { + dayOfWeek: string; + date: string; + periods: TimetablePeriod[]; +} + +export interface StudentDashboardData { + student: StudentInfo; + classLocation: ClassLocationInfo; + facultyList: ClassFacultyMember[]; + todaySchedule: DaySchedule; + isMockData: boolean; +} + +export interface MockUserCredential { + regNumber: string; + defaultPassword: string; + studentName: string; + department: string; + section: string; +} + +export interface ImsLoginResult { + success: boolean; + message: string; + token?: string; + student?: StudentInfo; +} + +const IMS_STORAGE_KEY = 'rit_ims_student_session'; + +export function getStoredImsSession(): { token: string; student: StudentInfo } | null { + try { + const raw = localStorage.getItem(IMS_STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw); + } catch { + return null; + } +} + +export function saveImsSession(token: string, student: StudentInfo) { + localStorage.setItem(IMS_STORAGE_KEY, JSON.stringify({ token, student })); +} + +export function clearImsSession() { + localStorage.removeItem(IMS_STORAGE_KEY); +} + +// ─── Default Mock Data for Client Fallback ─────────────────────────── +const CLIENT_FALLBACK_MOCKS: Record = { + '2114251001': { + student: { + studentId: '2114251001', + regNumber: '2114251001', + name: 'Anbu Kathir', + email: '251001@ritchennai.edu.in', + degree: 'B.E.', + department: 'Computer Science & Engineering', + departmentCode: 'CSE', + year: 1, + semester: 2, + section: 'A', + batch: '2025 - 2029', + regulation: '2021 Regulation', + }, + classLocation: { + roomNumber: 'LH-204', + buildingName: 'Dr. APJ Abdul Kalam Academic Block', + floor: '2nd Floor', + wing: 'East Wing', + landmark: 'Next to CSE Department Library, Opposite to Seminar Hall', + }, + facultyList: [ + { + subjectCode: 'CS3201', + subjectName: 'Data Structures & Algorithms', + facultyName: 'Dr. R. Arunkumar', + designation: 'Associate Professor & Class Incharge', + email: 'arunkumar.r@ritchennai.edu.in', + officeLocation: 'APJ Block, Cabin 208-B', + phoneExtension: 'Ext. 312', + avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Arunkumar', + isClassIncharge: true, + }, + { + subjectCode: 'MA3251', + subjectName: 'Discrete Mathematics', + facultyName: 'Dr. S. Malathi', + designation: 'Professor (Mathematics)', + email: 'malathi.s@ritchennai.edu.in', + officeLocation: 'Science & Humanities Block, 1st Floor', + phoneExtension: 'Ext. 118', + avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Malathi', + isClassIncharge: false, + }, + { + subjectCode: 'CS3202', + subjectName: 'Digital Principles & System Design', + facultyName: 'Dr. M. Balaji', + designation: 'Assistant Professor', + email: 'balaji.m@ritchennai.edu.in', + officeLocation: 'APJ Block, Cabin 104', + phoneExtension: 'Ext. 325', + avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Balaji', + isClassIncharge: false, + }, + { + subjectCode: 'GE3251', + subjectName: 'Environmental Sciences & Sustainability', + facultyName: 'Dr. T. Gayathri', + designation: 'Assistant Professor (Chemistry)', + email: 'gayathri.t@ritchennai.edu.in', + officeLocation: 'Science Block, Chemistry Cabin 3', + phoneExtension: 'Ext. 142', + avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Gayathri', + isClassIncharge: false, + }, + { + subjectCode: 'CS3271', + subjectName: 'Data Structures Laboratory', + facultyName: 'Dr. R. Arunkumar / Dr. K. Revathi', + designation: 'Lab In-Charges', + email: 'lab.cse@ritchennai.edu.in', + officeLocation: 'Computing Center 3 (CC-03)', + phoneExtension: 'Ext. 330', + avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Revathi', + isClassIncharge: false, + }, + ], + todaySchedule: { + dayOfWeek: 'MONDAY', + date: new Date().toISOString().split('T')[0], + periods: [ + { periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'MA3251', subjectName: 'Discrete Mathematics', type: 'THEORY', facultyName: 'Dr. S. Malathi', venue: 'LH-204', status: 'COMPLETED' }, + { periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'CS3201', subjectName: 'Data Structures & Algorithms', type: 'THEORY', facultyName: 'Dr. R. Arunkumar', venue: 'LH-204', status: 'COMPLETED' }, + { periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'CS3202', subjectName: 'Digital Principles & System Design', type: 'THEORY', facultyName: 'Dr. M. Balaji', venue: 'LH-204', status: 'ONGOING' }, + { periodNumber: 4, timeSlot: '11:45 AM - 12:40 PM', startTime: '11:45', endTime: '12:40', subjectCode: 'GE3251', subjectName: 'Environmental Sciences', type: 'THEORY', facultyName: 'Dr. T. Gayathri', venue: 'LH-204', status: 'UPCOMING' }, + { periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'CS3201', subjectName: 'DSA Tutorial & Problem Solving', type: 'TUTORIAL', facultyName: 'Dr. R. Arunkumar', venue: 'LH-204', status: 'UPCOMING' }, + { periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'LIB', subjectName: 'Digital Library & Research Hours', type: 'LIBRARY', facultyName: 'Chief Librarian', venue: 'Central Library - 2nd Floor', status: 'UPCOMING' }, + ], + }, + isMockData: true, + }, + '2114251002': { + student: { + studentId: '2114251002', + regNumber: '2114251002', + name: 'Priyadharshini M', + email: '251002@ritchennai.edu.in', + degree: 'B.Tech.', + department: 'Artificial Intelligence & Data Science', + departmentCode: 'AIDS', + year: 1, + semester: 2, + section: 'B', + batch: '2025 - 2029', + regulation: '2021 Regulation', + }, + classLocation: { + roomNumber: 'LH-301', + buildingName: 'Sir Isaac Newton Innovation Block', + floor: '3rd Floor', + wing: 'North Wing', + landmark: 'Opposite AI Centre of Excellence, Near Lift Lobby', + }, + facultyList: [ + { + subjectCode: 'AD3251', + subjectName: 'Data Structures Design', + facultyName: 'Dr. N. Saravanan', + designation: 'Associate Professor & Class Incharge', + email: 'saravanan.n@ritchennai.edu.in', + officeLocation: 'Newton Block, Cabin 302', + phoneExtension: 'Ext. 401', + avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Saravanan', + isClassIncharge: true, + }, + { + subjectCode: 'AD3252', + subjectName: 'Foundations of Artificial Intelligence', + facultyName: 'Prof. J. Daniel', + designation: 'Assistant Professor', + email: 'daniel.j@ritchennai.edu.in', + officeLocation: 'Newton Block, Cabin 306', + phoneExtension: 'Ext. 405', + avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Daniel', + isClassIncharge: false, + }, + { + subjectCode: 'MA3251', + subjectName: 'Probability and Statistics', + facultyName: 'Dr. V. Lakshmi', + designation: 'Professor (Mathematics)', + email: 'lakshmi.v@ritchennai.edu.in', + officeLocation: 'Science Block, Cabin 108', + phoneExtension: 'Ext. 119', + avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lakshmi', + isClassIncharge: false, + }, + ], + todaySchedule: { + dayOfWeek: 'MONDAY', + date: new Date().toISOString().split('T')[0], + periods: [ + { periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'MA3251', subjectName: 'Probability & Stats', type: 'THEORY', facultyName: 'Dr. V. Lakshmi', venue: 'LH-301', status: 'COMPLETED' }, + { periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'AD3251', subjectName: 'Data Structures Design', type: 'THEORY', facultyName: 'Dr. N. Saravanan', venue: 'LH-301', status: 'COMPLETED' }, + { periodNumber: 3, timeSlot: '10:50 AM - 11:45 AM', startTime: '10:50', endTime: '11:45', subjectCode: 'AD3252', subjectName: 'Foundations of AI', type: 'THEORY', facultyName: 'Prof. J. Daniel', venue: 'LH-301', status: 'ONGOING' }, + { periodNumber: 4, timeSlot: '11:45 AM - 12:40 PM', startTime: '11:45', endTime: '12:40', subjectCode: 'GE3251', subjectName: 'Environmental Sciences', type: 'THEORY', facultyName: 'Dr. T. Gayathri', venue: 'LH-301', status: 'UPCOMING' }, + { periodNumber: 5, timeSlot: '01:30 PM - 02:25 PM', startTime: '13:30', endTime: '14:25', subjectCode: 'AD3251', subjectName: 'AI Problem Solving Tutorial', type: 'TUTORIAL', facultyName: 'Dr. N. Saravanan', venue: 'LH-301', status: 'UPCOMING' }, + { periodNumber: 6, timeSlot: '02:25 PM - 03:20 PM', startTime: '14:25', endTime: '15:20', subjectCode: 'LIB', subjectName: 'Digital Library & Research Hours', type: 'LIBRARY', facultyName: 'Chief Librarian', venue: 'Central Library - 2nd Floor', status: 'UPCOMING' }, + ], + }, + isMockData: true, + }, + '2114251003': { + student: { + studentId: '2114251003', + regNumber: '2114251003', + name: 'Rahul V', + email: '251003@ritchennai.edu.in', + degree: 'B.E.', + department: 'Electronics & Communication Engineering', + departmentCode: 'ECE', + year: 1, + semester: 2, + section: 'A', + batch: '2025 - 2029', + regulation: '2021 Regulation', + }, + classLocation: { + roomNumber: 'LH-105', + buildingName: 'Rabindranath Tagore Academic Block', + floor: '1st Floor', + wing: 'South Wing', + landmark: 'Near Department Auditorium', + }, + facultyList: [ + { + subjectCode: 'EC3251', + subjectName: 'Circuit Analysis', + facultyName: 'Dr. I. Chandra', + designation: 'Professor & Class Incharge', + email: 'chandra.i@ritchennai.edu.in', + officeLocation: 'Tagore Block, Cabin 112', + phoneExtension: 'Ext. 502', + avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Chandra', + isClassIncharge: true, + }, + ], + todaySchedule: { + dayOfWeek: 'MONDAY', + date: new Date().toISOString().split('T')[0], + periods: [ + { periodNumber: 1, timeSlot: '08:45 AM - 09:40 AM', startTime: '08:45', endTime: '09:40', subjectCode: 'MA3251', subjectName: 'Discrete Mathematics', type: 'THEORY', facultyName: 'Dr. S. Malathi', venue: 'LH-105', status: 'COMPLETED' }, + { periodNumber: 2, timeSlot: '09:40 AM - 10:35 AM', startTime: '09:40', endTime: '10:35', subjectCode: 'EC3251', subjectName: 'Circuit Analysis', type: 'THEORY', facultyName: 'Dr. I. Chandra', venue: 'LH-105', status: 'ONGOING' }, + ], + }, + isMockData: true, + }, +}; + +const DEFAULT_MOCK_USER_LIST: MockUserCredential[] = [ + { regNumber: '2114251001', defaultPassword: 'rit@2026', studentName: 'Anbu Kathir', department: 'Computer Science & Engineering', section: 'Sec A' }, + { regNumber: '2114251002', defaultPassword: 'rit@2026', studentName: 'Priyadharshini M', department: 'AI & Data Science', section: 'Sec B' }, + { regNumber: '2114251003', defaultPassword: 'rit@2026', studentName: 'Rahul V', department: 'Electronics & Comm. Engg.', section: 'Sec A' }, +]; + +/** + * Log in using Register Number and Password + */ +export async function loginWithIms(regNumber: string, password: string): Promise { + try { + const url = getBackendUrl('/api/v1/ims/auth/login'); + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ regNumber, password }), + }); + + if (response.ok) { + const data: ImsLoginResult = await response.json(); + if (data.success && data.token && data.student) { + saveImsSession(data.token, data.student); + } + return data; + } + } catch { + // Backend offline fallback + } + + // Client-side fallback + const cleaned = regNumber.trim(); + const mock = CLIENT_FALLBACK_MOCKS[cleaned] || CLIENT_FALLBACK_MOCKS['2114251001']; + const token = 'ims_mock_token_' + mock.student.regNumber; + saveImsSession(token, mock.student); + + return { + success: true, + message: 'Mock IMS Login Successful', + token, + student: mock.student, + }; +} + +/** + * Fetch available demo accounts + */ +export async function fetchMockImsUsers(): Promise { + try { + const url = getBackendUrl('/api/v1/ims/auth/mock-users'); + const response = await fetch(url); + if (response.ok) { + return await response.json(); + } + } catch { + // Fallback + } + return DEFAULT_MOCK_USER_LIST; +} + +/** + * Fetch complete student dashboard information + */ +export async function fetchStudentDashboard( + regNumber?: string, + userEmail?: string, + devStudentId?: string +): Promise { + const session = getStoredImsSession(); + const effectiveReg = devStudentId || regNumber || session?.student.regNumber; + + if (!effectiveReg) { + throw new Error('No IMS session active. Please log in with your Register Number.'); + } + + try { + const params = new URLSearchParams(); + params.set('regNumber', effectiveReg); + if (userEmail) { + params.set('email', userEmail); + } + + const url = getBackendUrl(`/api/v1/ims/dashboard/me${params.toString() ? `?${params.toString()}` : ''}`); + + const headers: Record = { + 'Accept': 'application/json', + }; + + if (session?.token) { + headers['X-IMS-Token'] = session.token; + } + + if (devStudentId) { + headers['X-Dev-Student-Id'] = devStudentId; + } + + const response = await fetch(url, { headers }); + if (response.ok) { + return await response.json(); + } + } catch { + // Fallback + } + + // Client fallback mock + const fallback = CLIENT_FALLBACK_MOCKS[effectiveReg] || CLIENT_FALLBACK_MOCKS['2114251001']; + return fallback; +}