added fake end points to simulate real arhitecture

This commit is contained in:
Shanmuga Krishnan S M
2026-08-17 14:36:32 +05:30
parent d827f4aab9
commit 1eebd10f3a
14 changed files with 3652 additions and 248 deletions

View File

@@ -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: <shared_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_<regNo>_<uuid>`). |
| **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: <your_secret_key>`
- **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: <your_secret_key>`
- `X-Student-Id: 2114251001` (or `Authorization: Bearer <token>`)
- **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
```

View File

@@ -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<Map<String, Object>> login(@RequestBody Map<String, Object> payload) {
Map<String, Object> 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);
}
// 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<List<BusRoute>> getAllTransportRoutes() {
return ResponseEntity.ok(busRouteRepository.findAll());
}
@PostMapping("/transport/routes")
public ResponseEntity<?> createBusRoute(@RequestBody Map<String, Object> 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<String, Object> routeData) {
Optional<BusRoute> 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<String, Object> 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<String, Object> defaultCfg = new HashMap<>();
defaultCfg.put("community_bot_token", "");
defaultCfg.put("telegram_bot_token", "");
defaultCfg.put("helper_chat_ids", new ArrayList<Long>(List.of(971749136L, 5567776672L, 1873240361L)));
defaultCfg.put("spring_backend_url", "http://localhost:8085");
return defaultCfg;
}
private void saveTelegramConfigFile(Map<String, Object> 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<Map<String, Object>> getTelegramConfig() {
return ResponseEntity.ok(readTelegramConfigFile());
}
@PutMapping("/telegram/config")
public ResponseEntity<?> updateTelegramConfig(@RequestBody Map<String, Object> payload) {
Map<String, Object> 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<String, Object> 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<String, Object> config = readTelegramConfigFile();
@SuppressWarnings("unchecked")
List<Object> rawHelpers = (List<Object>) config.getOrDefault("helper_chat_ids", new ArrayList<>());
List<Long> 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<String, Object> config = readTelegramConfigFile();
@SuppressWarnings("unchecked")
List<Object> rawHelpers = (List<Object>) config.getOrDefault("helper_chat_ids", new ArrayList<>());
List<Long> 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<List<NotePyq>> 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<List<CommunityQuestion>> 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<List<String>> getRecipients() {
Path path = getRecipientsFilePath();
List<String> recipients = getRecipientsList(path);
return ResponseEntity.ok(recipients);
return ResponseEntity.ok(getRecipientsList(path));
}
@PostMapping("/recipients")

View File

@@ -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<ImsLoginResponse> 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<List<MockUserCredentialDto>> 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<DashboardResponse> 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);
}
}

View File

@@ -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<TimetablePeriod> periods;
}
// ─── Complete Dashboard Aggregate ────────────────────────────
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DashboardResponse {
private StudentInfo student;
private ClassLocationInfo classLocation;
private List<ClassFacultyMember> facultyList;
private DaySchedule todaySchedule;
private boolean isMockData;
}
}

View File

@@ -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<MockUserCredentialDto> getAvailableMockUsers();
DashboardResponse getStudentDashboard(String studentId);
StudentInfo getStudentInfo(String studentId);
ClassLocationInfo getClassLocation(String department, String section, int semester);
List<ClassFacultyMember> getClassFaculty(String department, String section, int semester);
DaySchedule getDaySchedule(String studentId, LocalDate date);
}

View File

@@ -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<String, StudentInfo> studentDb = new HashMap<>();
private final Map<String, String> userPasswords = new HashMap<>();
private final Map<String, ClassLocationInfo> locationDb = new HashMap<>();
private final Map<String, List<ClassFacultyMember>> facultyDb = new HashMap<>();
private final List<MockUserCredentialDto> 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<MockUserCredentialDto> 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<ClassFacultyMember> 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<ClassFacultyMember> 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<TimetablePeriod> periods = buildTimetableForDay(studentId, dayName, now);
return DaySchedule.builder()
.dayOfWeek(dayName)
.date(date)
.periods(periods)
.build();
}
private List<TimetablePeriod> buildTimetableForDay(String studentId, String dayOfWeek, LocalTime now) {
List<TimetablePeriod> 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;
}
}

View File

@@ -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<ImsLoginRequest> entity = new HttpEntity<>(request, createHeaders(null));
ResponseEntity<ImsLoginResponse> resp = restTemplate.exchange(url, HttpMethod.POST, entity, ImsLoginResponse.class);
return resp.getBody();
}
@Override
public List<MockUserCredentialDto> getAvailableMockUsers() {
return Collections.emptyList();
}
@Override
public DashboardResponse getStudentDashboard(String studentId) {
String url = imsBaseUrl + "/api/v1/dashboard/me";
HttpEntity<?> entity = new HttpEntity<>(createHeaders(studentId));
ResponseEntity<DashboardResponse> 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<StudentInfo> 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<ClassFacultyMember> 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();
}
}

View File

@@ -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}

View File

@@ -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() {
<Route path="/faculty" element={<Faculty />} />
<Route path="/events" element={<Events />} />
{/* Student Dashboard — Timetable, Staff, Location (IMS Reg No & Password) */}
<Route path="/dashboard" element={<StudentDashboard />} />
{/* Protected Routes — Require Google Sign-In */}
<Route
path="/community"

View File

@@ -19,6 +19,8 @@ import {
Users,
Code2,
Trophy,
GraduationCap,
LayoutDashboard,
Home as HomeIcon,
} from 'lucide-react';
import { useAuth } from '@/context/AuthContext';
@@ -31,6 +33,7 @@ interface NavItem {
const PRIMARY_LINKS: NavItem[] = [
{ label: 'Home', path: '/', icon: HomeIcon },
{ label: 'Dashboard', path: '/dashboard', icon: LayoutDashboard },
{ label: 'Notes', path: '/notes', icon: BookOpen },
{ label: 'Toolkit', path: '/toolkit', icon: Wrench },
{ label: 'Chatbot', path: '/ai-assistant', icon: Bot },
@@ -277,7 +280,15 @@ export default function Navbar() {
</div>
)}
</div>
<div className="p-2">
<div className="p-2 space-y-1">
<Link
to="/dashboard"
onClick={() => 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"
>
<GraduationCap className="w-4 h-4 text-orange-400" />
My Student Dashboard
</Link>
<button
onClick={() => {
logout();

File diff suppressed because it is too large Load Diff

View File

@@ -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<string, { abrv: string; full: string }> = {
'Computer Science & Engineering': { abrv: 'CSE', full: 'Computer Science and Engineering' },
@@ -23,11 +24,57 @@ const DEPT_MAP: Record<string, { abrv: string; full: string }> = {
'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<string>(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() {
</div>
</div>
<div className="container-custom pt-8 pb-20 md:pb-28 space-y-8">
<div className="container-custom pt-8 pb-20 md:pb-28 space-y-6">
{/* Active Student Department Filter Badge */}
{isStudentDeptAutoFiltered && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-orange-50 border border-orange-200 rounded-2xl p-4 flex items-center justify-between gap-3 text-orange-950 shadow-xs"
>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center shrink-0">
<Sparkles className="w-5 h-5 text-[#FF6B00]" />
</div>
<div>
<div className="text-xs font-bold uppercase tracking-wider text-orange-700">Student Department Auto-Filter</div>
<div className="text-xs font-medium text-orange-950">
Showing faculty for your department: <strong>{selectedDept}</strong>
</div>
</div>
</div>
<button
onClick={() => setSelectedDept('All Departments')}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-white border border-orange-300 text-xs font-bold text-orange-800 hover:bg-orange-100 transition-colors cursor-pointer shrink-0"
>
<span>View All Departments</span>
<X className="w-3.5 h-3.5" />
</button>
</motion.div>
)}
{/* Under Development Banner */}
<motion.div
initial={{ opacity: 0, y: -10 }}
@@ -118,6 +194,7 @@ export default function Faculty() {
Mock Data Active
</span>
</motion.div>
{/* Faculty Hero Section */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch">
<div className="lg:col-span-5 bg-white rounded-2xl p-8 border border-[#E9E5EE] shadow-[0_4px_20px_-4px_rgba(19,9,36,0.04)] flex flex-col justify-center relative overflow-hidden">
@@ -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 (
<div key={d.abrv} className={`p-4 rounded-2xl bg-${d.color}-50 border border-${d.color}-100 flex flex-col gap-1 transition-transform hover:scale-105 cursor-default`}>
<button
key={d.abrv}
onClick={() => setSelectedDept(d.full)}
className={`p-4 rounded-2xl border flex flex-col gap-1 transition-all text-left cursor-pointer ${
isSelected
? 'bg-[#FF6B00] text-white border-[#FF6B00] shadow-md'
: `bg-${d.color}-50 border-${d.color}-100 hover:scale-102`
}`}
>
<div className="flex justify-between items-center">
<span className={`text-xs font-bold text-${d.color}-700`} style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>{d.abrv}</span>
<BookOpen className={`w-3.5 h-3.5 text-${d.color}-400`} />
</div>
<span className="text-2xl font-bold text-[#1E293B]" style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>{count}</span>
<span className={`text-xs font-bold ${isSelected ? 'text-white' : `text-${d.color}-700`}`} style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>{d.abrv}</span>
<BookOpen className={`w-3.5 h-3.5 ${isSelected ? 'text-white/80' : `text-${d.color}-400`}`} />
</div>
<span className={`text-2xl font-bold ${isSelected ? 'text-white' : 'text-[#1E293B]'}`} style={{ fontFamily: 'Plus Jakarta Sans, sans-serif' }}>{count}</span>
</button>
);
})}
</div>
@@ -256,7 +342,7 @@ export default function Faculty() {
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="px-6 py-3 bg-white/80 backdrop-blur-md border border-[#E8ECF4] shadow-[0_8px_30px_rgb(0,0,0,0.08)] rounded-full flex items-center gap-2"
className="px-6 py-3 bg-white/80 backdrop-blur-md border border-[#E8ECF4] shadow-[0_8px_30px_rgb(0,0,0,0.08)] rounded-full flex items-center gap-2 pointer-events-auto"
>
<span className="text-lg">👥</span>
<span className="text-sm font-semibold text-[#1E293B]" style={{ fontFamily: 'Inter, sans-serif' }}>

View File

@@ -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<StudentDashboardData | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(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<string | null>(null);
const [mockUsers, setMockUsers] = useState<MockUserCredential[]>([]);
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 (
<div className="min-h-[85vh] bg-[#0a0f1d] text-slate-100 flex items-center justify-center py-12 px-4 sm:px-6">
<div className="w-full max-w-md space-y-6">
<div className="text-center space-y-2">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-[#FF6B00] to-[#F97316] p-0.5 shadow-xl shadow-orange-500/20 mx-auto">
<div className="w-full h-full bg-slate-950/60 rounded-[14px] flex items-center justify-center backdrop-blur-sm">
<GraduationCap className="w-8 h-8 text-white" />
</div>
</div>
<h1 className="text-2xl font-extrabold text-white tracking-tight">
IMS Student Portal Sign In
</h1>
<p className="text-xs text-slate-400 max-w-sm mx-auto">
Enter your official college Register Number and Password to access your timetable and assigned classroom venue.
</p>
</div>
{/* Login Form Card */}
<motion.div
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
className="bg-slate-900/80 backdrop-blur-xl border border-white/10 rounded-3xl p-6 shadow-2xl space-y-5"
>
{loginError && (
<div className="p-3.5 rounded-2xl bg-rose-500/10 border border-rose-500/25 flex items-start gap-2.5 text-xs text-rose-300">
<AlertCircle className="w-4 h-4 text-rose-400 shrink-0 mt-0.5" />
<span>{loginError}</span>
</div>
)}
<form onSubmit={handleImsLogin} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
<User className="w-3.5 h-3.5 text-orange-400" />
Register Number / Roll No
</label>
<input
type="text"
value={regNumberInput}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
<KeyRound className="w-3.5 h-3.5 text-orange-400" />
IMS Password
</label>
<input
type="password"
value={passwordInput}
onChange={(e) => 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
/>
</div>
<button
type="submit"
disabled={isLoggingIn}
className="w-full py-3.5 px-4 rounded-xl text-white font-bold text-sm bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:opacity-95 shadow-lg shadow-orange-500/20 transition-all flex items-center justify-center gap-2 cursor-pointer"
>
{isLoggingIn ? (
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<>
<span>Sign In to Student Portal</span>
<ArrowRight className="w-4 h-4" />
</>
)}
</button>
</form>
{/* Expandable Demo Helper */}
<div className="pt-3 border-t border-white/10">
<button
type="button"
onClick={() => setShowDemoBox(!showDemoBox)}
className="w-full text-center text-xs font-semibold text-orange-400 hover:underline flex items-center justify-center gap-1 cursor-pointer"
>
<Sparkles className="w-3.5 h-3.5" />
{showDemoBox ? 'Hide Demo Accounts' : 'Testing / Demo Accounts'}
</button>
<AnimatePresence>
{showDemoBox && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden mt-3 space-y-2"
>
<p className="text-[11px] text-slate-400 text-center">
Click any sample student below to auto-fill the login form:
</p>
<div className="space-y-1.5">
{mockUsers.map((mock) => (
<button
key={mock.regNumber}
type="button"
onClick={() => handleQuickFillMock(mock)}
className="w-full flex items-center justify-between p-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left text-xs transition-colors cursor-pointer"
>
<div>
<p className="font-bold text-white">{mock.studentName}</p>
<p className="text-[10px] text-slate-400">Reg: {mock.regNumber} ({mock.department})</p>
</div>
<span className="text-[10px] text-orange-400 font-mono">Fill Form</span>
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────
// VIEW 2: LOGGED-IN STUDENT DASHBOARD
// ─────────────────────────────────────────────────────────────
const classInchargeMember = data?.facultyList.find((f) => f.isClassIncharge);
return (
<div className="min-h-screen bg-[#0a0f1d] text-slate-100 pt-6 pb-24 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* ─── Top Banner ─── */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-slate-900/60 backdrop-blur-xl border border-white/10 rounded-3xl p-6 shadow-2xl relative overflow-hidden">
<div className="absolute top-0 right-0 -mr-16 -mt-16 w-64 h-64 bg-orange-500/10 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-0 left-0 -ml-16 -mb-16 w-64 h-64 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none" />
<div className="relative z-10 flex items-start gap-4">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#FF6B00] to-[#F97316] p-0.5 shadow-lg shadow-orange-500/20 shrink-0">
<div className="w-full h-full bg-slate-950/40 rounded-[14px] flex items-center justify-center backdrop-blur-sm">
<GraduationCap className="w-7 h-7 text-white" />
</div>
</div>
<div>
<div className="flex flex-wrap items-center gap-2 mb-1">
<h1 className="text-xl sm:text-2xl font-extrabold text-white tracking-tight">
{data?.student.name || imsSession.student.name}
</h1>
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold tracking-wider bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 uppercase">
Authenticated Student
</span>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-slate-400 font-medium">
<span>Reg No: <strong className="text-white">{data?.student.regNumber || imsSession.student.regNumber}</strong></span>
<span>•</span>
<span>{data?.student.degree} {data?.student.department}</span>
<span>•</span>
<span>Year {data?.student.year}, Sem {data?.student.semester} ({data?.student.section})</span>
</div>
</div>
</div>
<div className="relative z-10 flex items-center gap-2.5 self-start md:self-auto">
<Link
to={`/faculty?dept=${encodeURIComponent(data?.student.department || '')}`}
className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-orange-500/15 hover:bg-orange-500/25 text-orange-400 border border-orange-500/30 text-xs font-bold transition-all cursor-pointer"
>
<span>Faculty Directory</span>
<ExternalLink className="w-3.5 h-3.5" />
</Link>
<button
onClick={handleLogoutIms}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-800 hover:bg-rose-500/15 text-slate-300 hover:text-rose-400 border border-white/10 hover:border-rose-500/30 text-xs font-semibold transition-all cursor-pointer"
>
<LogOut className="w-4 h-4" />
Sign Out
</button>
</div>
</div>
{/* ─── Key Stats Quick Bar ─── */}
{data && (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-slate-900/40 border border-white/10 rounded-2xl p-4 flex items-center gap-3.5">
<div className="w-10 h-10 rounded-xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center text-orange-400 shrink-0">
<Building className="w-5 h-5" />
</div>
<div className="min-w-0">
<p className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Assigned Room</p>
<p className="text-sm sm:text-base font-bold text-white truncate">{data.classLocation.roomNumber}</p>
<p className="text-[10px] text-slate-400 truncate">{data.classLocation.floor}</p>
</div>
</div>
<div className="bg-slate-900/40 border border-white/10 rounded-2xl p-4 flex items-center gap-3.5">
<div className="w-10 h-10 rounded-xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400 shrink-0">
<Users className="w-5 h-5" />
</div>
<div className="min-w-0">
<p className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Class Incharge</p>
<p className="text-sm sm:text-base font-bold text-white truncate">
{classInchargeMember?.facultyName.split(' ')[0] || 'Assigned'}
</p>
<p className="text-[10px] text-slate-400 truncate">
{classInchargeMember?.officeLocation || 'Staff Cabin'}
</p>
</div>
</div>
<div className="bg-slate-900/40 border border-white/10 rounded-2xl p-4 flex items-center gap-3.5">
<div className="w-10 h-10 rounded-xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400 shrink-0">
<GraduationCap className="w-5 h-5" />
</div>
<div className="min-w-0">
<p className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Branch & Section</p>
<p className="text-sm sm:text-base font-bold text-white truncate">{data.student.departmentCode} ({data.student.section})</p>
<p className="text-[10px] text-slate-400 truncate">Semester {data.student.semester}</p>
</div>
</div>
<div className="bg-slate-900/40 border border-white/10 rounded-2xl p-4 flex items-center gap-3.5">
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400 shrink-0">
<Calendar className="w-5 h-5" />
</div>
<div className="min-w-0">
<p className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Academic Batch</p>
<p className="text-sm sm:text-base font-bold text-white truncate">{data.student.batch}</p>
<p className="text-[10px] text-slate-400 truncate">{data.student.regulation}</p>
</div>
</div>
</div>
)}
{/* ─── Navigation Tabs ─── */}
<div className="flex items-center justify-between border-b border-white/10 pb-4 overflow-x-auto gap-4">
<div className="flex items-center gap-2">
{[
{ 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 (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs sm:text-sm font-bold transition-all cursor-pointer whitespace-nowrap ${
isActive
? 'bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white shadow-md shadow-orange-500/20'
: 'bg-slate-900/60 hover:bg-slate-800 text-slate-300 hover:text-white border border-white/10'
}`}
>
<Icon className="w-4 h-4" />
{tab.label}
</button>
);
})}
</div>
{data && (
<Link
to={`/faculty?dept=${encodeURIComponent(data.student.department)}`}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 border border-white/10 text-xs font-bold text-slate-200 hover:text-white transition-all cursor-pointer whitespace-nowrap shrink-0"
>
<Users className="w-4 h-4 text-orange-400" />
<span>View {data.student.departmentCode} Faculty Directory</span>
<ArrowRight className="w-3.5 h-3.5 text-slate-400" />
</Link>
)}
</div>
{/* ─── Main Content Tabs ─── */}
{isLoading ? (
<div className="flex flex-col items-center justify-center py-20 gap-3">
<div className="w-8 h-8 border-3 border-orange-500 border-t-transparent rounded-full animate-spin" />
<p className="text-xs text-slate-400 font-medium">Connecting to IMS services...</p>
</div>
) : error ? (
<div className="bg-rose-500/10 border border-rose-500/30 rounded-3xl p-8 text-center space-y-3">
<p className="text-rose-400 font-bold text-base">Error connecting to IMS Gateway</p>
<p className="text-slate-400 text-xs max-w-md mx-auto">{error}</p>
<button
onClick={() => loadDashboard()}
className="px-4 py-2 bg-rose-500 text-white rounded-xl text-xs font-bold hover:bg-rose-600 transition-colors"
>
Retry Connection
</button>
</div>
) : data ? (
<div>
{/* ─── 1. TIMETABLE TAB ─── */}
{activeTab === 'timetable' && (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-3 bg-slate-900/40 p-4 rounded-2xl border border-white/10">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-orange-400" />
<span className="text-sm font-bold text-white tracking-wide">
{data.todaySchedule.dayOfWeek} SCHEDULE ({data.todaySchedule.date})
</span>
</div>
<div className="flex items-center gap-3 text-xs">
<span className="flex items-center gap-1 text-emerald-400 font-semibold">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
Live Status Active
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.todaySchedule.periods.map((period) => {
const isOngoing = period.status === 'ONGOING';
const isCompleted = period.status === 'COMPLETED';
return (
<motion.div
key={period.periodNumber}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`relative rounded-2xl p-5 border transition-all ${
isOngoing
? 'bg-gradient-to-b from-orange-500/15 to-slate-900 border-orange-500/50 shadow-xl shadow-orange-500/10 ring-1 ring-orange-500/40'
: isCompleted
? 'bg-slate-950/40 border-white/5 opacity-60'
: 'bg-slate-900/50 border-white/10 hover:border-white/20'
}`}
>
{/* Header Badge */}
<div className="flex items-center justify-between gap-2 mb-3">
<div className="flex items-center gap-2">
<span className="w-6 h-6 rounded-lg bg-white/10 text-white font-bold text-xs flex items-center justify-center">
P{period.periodNumber}
</span>
<span className="text-xs font-semibold text-slate-300 flex items-center gap-1">
<Clock className="w-3 h-3 text-slate-400" />
{period.timeSlot}
</span>
</div>
<span
className={`px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wider ${
isOngoing
? 'bg-orange-500 text-white animate-pulse'
: isCompleted
? 'bg-slate-800 text-slate-400'
: 'bg-blue-500/20 text-blue-400 border border-blue-500/30'
}`}
>
{period.status}
</span>
</div>
{/* Subject Details */}
<div className="space-y-1.5 mb-4">
<div className="flex items-center gap-2">
<span className="text-[11px] font-mono font-bold text-orange-400">
{period.subjectCode}
</span>
<span className="px-1.5 py-0.2 rounded text-[9px] font-bold bg-white/10 text-slate-300 uppercase">
{period.type}
</span>
</div>
<h3 className="text-base font-bold text-white line-clamp-1">
{period.subjectName}
</h3>
</div>
{/* Footer / Instructor & Venue */}
<div className="pt-3 border-t border-white/10 flex items-center justify-between text-xs text-slate-300">
<span className="truncate pr-2">{period.facultyName}</span>
<span className="shrink-0 font-bold text-white px-2 py-0.5 rounded-lg bg-white/10 flex items-center gap-1">
<MapPin className="w-3 h-3 text-orange-400" />
{period.venue}
</span>
</div>
</motion.div>
);
})}
</div>
</div>
)}
{/* ─── 2. CLASS LOCATION TAB ─── */}
{activeTab === 'location' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-slate-900/60 border border-white/10 rounded-3xl p-6 space-y-6">
<div>
<h3 className="text-lg font-bold text-white flex items-center gap-2 mb-1">
<Building className="w-5 h-5 text-orange-400" />
Classroom & Section Venue
</h3>
<p className="text-xs text-slate-400">
Official lecture hall allocation from Academic Registrar.
</p>
</div>
<div className="space-y-4">
<div className="p-4 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-between">
<div>
<p className="text-xs text-slate-400 uppercase font-semibold">Room Number</p>
<p className="text-2xl font-black text-white">{data.classLocation.roomNumber}</p>
</div>
<span className="px-3 py-1 bg-orange-500/20 text-orange-400 border border-orange-500/30 rounded-xl text-xs font-bold">
{data.student.section} Section
</span>
</div>
<div className="space-y-3 text-xs">
<div className="flex justify-between py-2 border-b border-white/10">
<span className="text-slate-400">Building Block:</span>
<span className="font-bold text-white text-right">{data.classLocation.buildingName}</span>
</div>
<div className="flex justify-between py-2 border-b border-white/10">
<span className="text-slate-400">Floor & Wing:</span>
<span className="font-bold text-white">{data.classLocation.floor}, {data.classLocation.wing}</span>
</div>
<div className="flex justify-between py-2">
<span className="text-slate-400">Landmark Directions:</span>
<span className="font-bold text-orange-400 text-right max-w-xs">{data.classLocation.landmark}</span>
</div>
</div>
</div>
</div>
<div className="bg-slate-900/60 border border-white/10 rounded-3xl p-6 flex flex-col justify-between space-y-6">
<div className="space-y-3">
<h3 className="text-lg font-bold text-white flex items-center gap-2">
<Compass className="w-5 h-5 text-orange-400" />
Academic Department Details
</h3>
<p className="text-xs text-slate-400 leading-relaxed">
Official department profile, curriculum regulation, and academic guidelines for batch {data.student.batch}.
</p>
<div className="grid grid-cols-2 gap-3 pt-3">
<div className="p-3 bg-white/5 rounded-xl border border-white/5">
<p className="text-[10px] uppercase font-bold text-slate-400">Degree & Branch</p>
<p className="text-xs font-bold text-white mt-0.5">{data.student.degree} {data.student.departmentCode}</p>
</div>
<div className="p-3 bg-white/5 rounded-xl border border-white/5">
<p className="text-[10px] uppercase font-bold text-slate-400">Regulation</p>
<p className="text-xs font-bold text-white mt-0.5">{data.student.regulation}</p>
</div>
<div className="p-3 bg-white/5 rounded-xl border border-white/5">
<p className="text-[10px] uppercase font-bold text-slate-400">Academic Year</p>
<p className="text-xs font-bold text-white mt-0.5">Year {data.student.year} (Sem {data.student.semester})</p>
</div>
<div className="p-3 bg-white/5 rounded-xl border border-white/5">
<p className="text-[10px] uppercase font-bold text-slate-400">Batch Cohort</p>
<p className="text-xs font-bold text-white mt-0.5">{data.student.batch}</p>
</div>
</div>
</div>
<div className="p-4 rounded-2xl bg-gradient-to-r from-orange-500/10 to-indigo-500/10 border border-white/10 flex items-center gap-3">
<HelpCircle className="w-5 h-5 text-orange-400 shrink-0" />
<p className="text-xs text-slate-300">
Need classroom adjustment? Contact your Class Incharge or Head of Department.
</p>
</div>
</div>
</div>
)}
</div>
) : null}
</div>
</div>
);
}

419
src/services/imsService.ts Normal file
View File

@@ -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<string, StudentDashboardData> = {
'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<ImsLoginResult> {
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<MockUserCredential[]> {
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<StudentDashboardData> {
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<string, string> = {
'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;
}