Convert backends to Firebase and combine projects

This commit is contained in:
2026-06-18 14:07:24 +05:30
commit 0a76feafc5
147 changed files with 35104 additions and 0 deletions

View File

@@ -0,0 +1,428 @@
# RIT Event Management System (EMS) - Comprehensive System Documentation
The **RIT Event Management System (EMS)** is a state-of-the-art web application developed for the Rajalakshmi Institute of Technology (RIT) to handle, automate, audit, and coordinate campus-wide events, venue requests, and academic schedules.
---
## 1. Executive Summary & Core Objective
In an active educational institution like RIT (and the sister school RSB), coordinating events such as academic workshops, guest lectures, sports events, student club activities, and high-priority placement drives presents significant scheduling challenges:
1. **Venue Booking Conflicts:** Overlapping bookings for major facilities (e.g., auditoriums, seminar halls, labs).
2. **Complex Approvals:** Manual routing of proposals from Faculty, through Heads of Departments (HOD), up to the Principal.
3. **Academic Semester Swapping:** Transitioning routine weekly/monthly schedules from one semester to the next.
4. **Data Overload:** Migrating bulk event schedules from legacy tools or Google Forms.
The EMS platform addresses these challenges by offering:
* **Interactive Venues at a Glance:** Live timeline visuals showing hourly and daily room utilization.
* **Role-Based Workflows:** Direct automated routing of event proposals based on standard institutional roles.
* **Smart Conflict Solver:** Automated, real-time database queries matching proposed locations, dates, and institutions.
* **Overriding Placement System:** Privileged scheduling options for Placement staff to secure facilities.
* **Semester Swap Simulation:** Dynamic tool mapping active calendars onto subsequent semesters with calendar shift simulations.
* **Fuzzy Excel Parser:** In-browser sheet processor mapping arbitrary data columns into structured event templates.
---
## 2. System Architecture & Tech Stack
The platform is designed around a modern decoupled client-server architecture.
```mermaid
graph LR
subgraph Frontend [React SPA Client]
React[React 19 / TS / Vite]
Router[Component & Context Routing]
Tailwind[Tailwind CSS & Glassmorphism UI]
Framer[Framer Motion Animations]
end
subgraph Backend [Spring Boot API Server]
Controller[REST API Controllers]
Security[Spring Security Config]
Services[JPA / Hibernate Data Access]
end
subgraph Database [Relational Storage]
MySQL[(Local MySQL ems_db)]
end
React <-->|REST over HTTP| Controller
Controller <--> Services
Services <--> MySQL
```
### Backend Specifications
* **Core Framework:** Spring Boot 3.2.5 (Java 17)
* **ORM & Data Access:** Spring Data JPA with Hibernate
* **Database Driver:** MySQL Connector/J (`com.mysql.cj.jdbc.Driver`)
* **Security:** Spring Security (Permit-all filter bypass, with role-based checks and authorization handling verified inside the controller/service scope).
* **Cryptographic Hashing:** `BCryptPasswordEncoder` for storing/verifying user passwords.
* **Dev Tools:** Project Lombok, Spring Boot DevTools.
* **Port Configuration:** Runs on port `8081` (CORS-enabled for frontend origins).
### Frontend Specifications
* **Core Framework:** React 19, TypeScript, Vite
* **State Management:** React Context API (specifically `AuthContext` managing authenticated states).
* **Styling System:** Tailwind CSS with custom global CSS utilities for modern glassmorphism design layouts.
* **Animations:** Framer Motion powering tab transitions, modal slides, and responsive components.
* **Icon Suite:** Lucide React
* **Utility Libraries:**
* `date-fns`: Date formatting, calendar scheduling, and duration differences.
* `xlsx`: In-browser parsing of spreadsheet files (.xlsx, .csv).
---
## 3. Database Schema & Architecture
The database contains four primary entities managed via Spring Data JPA.
```mermaid
erDiagram
USERS {
Long id PK
String email UK
String password
String fullName
String role
String department
Boolean isClubCoordinator
Boolean isPlacementStaff
Boolean isClassIncharge
Integer classStrength
String inchargeClass
String inchargeBatch
String inchargeSection
}
EVENTS {
Long id PK
String title
Text description
LocalDateTime startDate
LocalDateTime endDate
String location
String category
String type
String institution
String department
String status
String guestName
String guestSocialProfile
Double budget
Boolean hasRegistrationFee
Double registrationFee
Text rejectionReason
String groupRequestId
Long proposer_id FK
}
CLASS_MAPPING {
Long id PK
String institution
String department
String academicYear
String status
}
INSTITUTIONAL_NOTE {
Long id PK
String content
LocalDate targetDate
String authorName
String authorEmail
LocalDateTime createdAt
}
USERS ||--o{ EVENTS : proposes
```
### A. User Entity (`users`)
Holds profiles, credentials, permissions, and roles.
* `id` (Long, Primary Key): Auto-incremented.
* `email` (String, Unique): Institutional email (`@rit.edu` or `@ritindia.edu`).
* `password` (String): BCrypt hashed password representation.
* `fullName` (String): User's display name.
* `role` (String): Enum-like values: `ADMIN`, `PRINCIPAL`, `HOD`, `FACULTY`, `PLACEMENT`.
* `department` (String): Department names (e.g., `CSE`, `AI&ML`, `ECE`, `H&S Dept`, etc.).
* `isClubCoordinator` (Boolean): Authorizes coordination of student club events.
* `isPlacementStaff` (Boolean): Grants authority to override schedule conflicts for placement events.
* `isClassIncharge` (Boolean): Authorizes dashboard widgets showing class sizes and updates.
* `classStrength` (Integer): Student count for the in-charge class.
* `inchargeClass` / `inchargeBatch` / `inchargeSection` (String): Specific details identifying the class in charge.
* `assignedClubs` (ElementCollection): List of student clubs managed by the user.
### B. Event Entity (`events`)
Details individual event proposals, resources, status, and audit workflows.
* `id` (Long, Primary Key): Auto-incremented.
* `title` (String, Not Null): The name of the event.
* `description` (Text): Detailed agenda or description.
* `startDate` / `endDate` (LocalDateTime): Timestamps for the event duration.
* `location` (String): Specific venue (e.g., `GB 4th floor auditorium`, `Wozniak Auditorium`, `Mini Seminar Hall`).
* `category` (String): Dictates the routing logic: `ACADEMIC`, `CLUB`, `PLACEMENT`, `SPORTS`, `INSTITUTIONAL`.
* `type` (String): Event type (e.g., `Workshop`, `Seminar`, `Guest Lecture`, `Competition`).
* `institution` (String): Targets `RIT` or `RSB`.
* `department` (String): Proposing department.
* `academicYears` (ElementCollection): Targeted cohorts (e.g., `1st Year`, `2nd Year`, `3rd Year`, `4th Year`).
* `targetedSections` (ElementCollection): Target sections (e.g., `A`, `B`, `C`).
* `status` (String): Current lifecycle stage:
* `REQUESTED`: Submitted by Faculty, pending HOD approval.
* `PENDING_PR`: Approved by HOD, pending Principal approval (or submitted directly by HOD/Placement, bypassing HOD stage).
* `APPROVED`: Fully approved, venue locked, and calendar entry created.
* `COMPLETED`: Event time has elapsed.
* `CANCELLED`: Cancelled manually or displaced by an overriding placement event.
* `HOD_REJECTED`: Rejected at the HOD stage.
* `PRINCIPAL_REJECTED`: Rejected at the Principal stage.
* `guestName` / `guestSocialProfile` (String): Guest details and links.
* `requirements` (ElementCollection): Resources required (e.g., `Mementos`, `Projector`, `Lunch`, `Wi-Fi`, `Mic`).
* `budget` (Double): Requested budget.
* `hasRegistrationFee` (Boolean) / `registrationFee` (Double): Entry fee details.
* `sponsors` (ElementCollection): Partner/sponsor list.
* `proposer` (ManyToOne -> User): Reference to user who created the event proposal.
* `rejectionReason` (Text): Comments input by HOD or Principal when rejecting.
* `groupRequestId` (String): Link identifier for recurring events.
### C. ClassMapping Entity (`classes`)
Used for maintaining lists of student cohorts and sections.
* `id` (Long, Primary Key)
* `institution` (String): `RIT` or `RSB`.
* `department` (String): Department name.
* `academicYear` (String): `1st Year`, `2nd Year`, etc.
* `sections` (ElementCollection): List of associated sections.
* `status` (String): Defaults to `Ready`.
### D. InstitutionalNote Entity (`institutional_notes`)
Pinned warnings, announcements, or notifications on the main calendar.
* `id` (Long, Primary Key)
* `content` (String, Not Null)
* `targetDate` (LocalDate): Date on which the note should appear on the calendar.
* `authorName` / `authorEmail` (String): Creator info.
* `createdAt` (LocalDateTime)
---
## 4. Role-Based Access Control & Navigation
The frontend app adjusts its views, menus, and dashboard components dynamically based on the logged-in user's role and authorization flags:
| Role / Flag | Menu Items Available | Special Features |
| :--- | :--- | :--- |
| **FACULTY** | Dashboard, Propose Event, All Events, Institutional Checklist, Event History | Can see personal proposal status, updates class strength if marked as Class Incharge. |
| **HOD** | Dashboard, Propose Event, Approvals, All Events, Institutional Checklist, Event History, Automation Console | Can approve/reject department proposals. Can access the Semester Swap simulator. |
| **PRINCIPAL** | Dashboard, Propose Event, Approvals, All Events, Institutional Checklist, Event History, Automation Console | Can approve/reject any pending events. Master checklist dashboard widget access. |
| **PLACEMENT** | Dashboard, Propose Event, All Events, Institutional Checklist, Event History | Proposes placement events that can optionally override existing bookings. |
| **ADMIN** | Dashboard, Propose Event, Approvals, Clubs Checklist, All Events, Institutional Checklist, Event History, Automation Console, Excel Import, Class Configurations, User Management | Full control over users, classes, excel file parses, database configurations, and approvals. |
| **isClubCoordinator = true** | Add *Clubs* Menu option | Grants access to the Club Checklist page to coordinate student club activities. |
---
## 5. Site Map & Route Specification
The system uses a Single-Page Application (SPA) shell in [App.tsx](file:///e:/RIT-EMS-main/frontend/src/App.tsx) driven by state routing.
```mermaid
graph TD
Login[LoginPage.tsx] -->|POST /api/auth/login success| Shell[App.tsx SPA Shell]
subgraph SPA Navigation & Components
Shell -->|dashboard| Overview[Overview.tsx]
Shell -->|propose| Proposal[EventProposalForm.tsx]
Shell -->|approvals| Approvals[ApprovalsView.tsx]
Shell -->|clubs| Clubs[ClubInstitutionalChecklist.tsx]
Shell -->|events| Events[AllEvents.tsx]
Shell -->|checklist| Checklist[InstitutionalChecklist.tsx]
Shell -->|history| History[EventHistory.tsx]
Shell -->|classes| ClassMgmt[ClassManagement.tsx]
Shell -->|automation| Automation[AutomationView.tsx]
Shell -->|import-excel| ExcelImport[ExcelImport.tsx]
Shell -->|user-management| UserMgmt[UserManagement.tsx]
end
subgraph Embedded Widgets in Overview
Overview --> Cal[InstitutionalCalendar.tsx]
Overview --> Ven[VenuesAtGlance.tsx]
Overview --> Time[VenueTimeline.tsx]
Overview --> Fac[FacultyEventManagement.tsx]
Overview --> Pri[PrincipalMasterChecklist.tsx]
end
```
### Route & Component Specification Details
#### 1. Login Gate
* **Component:** [LoginPage.tsx](file:///e:/RIT-EMS-main/frontend/src/pages/LoginPage.tsx)
* **Access:** Anonymous/Public.
* **Description:** Provides a modern UI with split-card layout, college logos, and forms for institutional email & password.
* **API Endpoint called:** `POST /api/auth/login`
#### 2. Overview Dashboard (`dashboard`)
* **Component:** [Overview.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/Overview.tsx)
* **Access:** All Authenticated Users.
* **Features:**
* Displays circular progress indicators representing events by category.
* Shows role-specific cards (e.g., Faculty see their proposals; Class Incharges see class stats; Principals see Master Checklist).
* **Embedded Sub-Components:**
* **[InstitutionalCalendar.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/InstitutionalCalendar.tsx):** Interactive calendar with day-based event popovers and notes.
* **[VenuesAtGlance.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/VenuesAtGlance.tsx):** Displays a grid of main campus venues and their immediate status.
* **[VenueTimeline.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/VenueTimeline.tsx):** A timeline mapping hour blocks for venue bookings.
* **[FacultyEventManagement.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/FacultyEventManagement.tsx):** Faculty-specific event table displaying submission stages.
* **[PrincipalMasterChecklist.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/PrincipalMasterChecklist.tsx):** Principal-specific checklist audit view.
* **API Endpoints called:** `GET /api/events`, `GET /api/notes`, `POST /api/notes`, `PUT /api/admin/users/{id}` (for class updates).
#### 3. Propose Event (`propose`)
* **Component:** [EventProposalForm.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/EventProposalForm.tsx)
* **Access:** All Authenticated Users.
* **Features:**
* Handles standard scheduling, multi-select sections/target academic years, and venue requirements (Catering, Projector, Wi-Fi, etc.).
* Support for recurring group bookings.
* Placement role can toggle "Override Conflicts".
* **API Endpoints called:** `POST /api/events/propose`, `PUT /api/events/{id}`
#### 4. Approvals (`approvals`)
* **Component:** [ApprovalsView.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/ApprovalsView.tsx)
* **Access:** HOD and PRINCIPAL.
* **Features:** Tabulated view listing pending events. HOD reviews `REQUESTED` events. Principal reviews `PENDING_PR` events. Rejections trigger a popover modal requesting comment.
* **API Endpoints called:** `GET /api/events`, `POST /api/events/{id}/approve`, `POST /api/events/{id}/reject`
#### 5. Clubs Checklist (`clubs`)
* **Component:** [ClubInstitutionalChecklist.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/ClubInstitutionalChecklist.tsx)
* **Access:** Club Coordinators and Admin.
* **Features:** Filters events specifically marked under the `CLUB` category and presents a checklist layout.
* **API Endpoints called:** `GET /api/events`
#### 6. All Events (`events`)
* **Component:** [AllEvents.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/AllEvents.tsx)
* **Access:** All Authenticated Users.
* **Features:** Grid searching, text filters, date search, category chips, and delete/edit buttons for event owners or Admin.
* **API Endpoints called:** `GET /api/events`, `DELETE /api/events/{id}`
#### 7. Institutional Checklist (`checklist`)
* **Component:** [InstitutionalChecklist.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/InstitutionalChecklist.tsx)
* **Access:** All Authenticated Users.
* **Features:** Visual grid displaying requirements (Wi-Fi, catering, projector) for approved upcoming events, highlighting completeness.
* **API Endpoints called:** `GET /api/events`
#### 8. Event History (`history`)
* **Component:** [EventHistory.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/EventHistory.tsx)
* **Access:** All Authenticated Users.
* **Features:** Lists elapsed events with filters. Features summary stats showing total budget spent and event count.
* **API Endpoints called:** `GET /api/events`
#### 9. Automation Console (`automation`)
* **Component:** [AutomationView.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/AutomationView.tsx)
* **Access:** HOD, PRINCIPAL, and ADMIN.
* **Features:**
* **Semester Swap:** Selects a batch of events (e.g., CSE 3rd Year RIT) and maps them to a future date range (+/- 6 months) to replicate schedules.
* **Conflict Analysis:** In-memory check identifying overlap with existing entries or weekends, recommending alternatives.
* **Review Gate:** Verification modal showing proposed events before batch creation.
* **API Endpoints called:** `GET /api/events`, `POST /api/events/batch-create`
#### 10. Excel Import (`import-excel`)
* **Component:** [ExcelImport.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/ExcelImport.tsx)
* **Access:** ADMIN.
* **Features:**
* File uploader utilizing `xlsx` to parse spreadsheet columns.
* Mapping panel to align sheet columns with event details.
* Proposer verification: Automatically registers unrecognized emails (`password` defaults to `Password@123`).
* Conflict resolver indicating venue/time overlaps and recommending shifts.
* **API Endpoints called:** `GET /api/events`, `GET /api/admin/users`, `POST /api/admin/users`, `POST /api/events/batch-create`
#### 11. Class Configurations (`classes`)
* **Component:** [ClassManagement.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/ClassManagement.tsx)
* **Access:** ADMIN.
* **Features:** Add classes, update sections, and perform the "Yearly Promotion" (e.g. promoting 1st Years -> 2nd Years, deleting graduating 4th Years).
* **API Endpoints called:** `GET /api/classes`, `POST /api/classes`, `DELETE /api/classes/{id}`, `POST /api/classes/promote`
#### 12. User Management (`user-management`)
* **Component:** [UserManagement.tsx](file:///e:/RIT-EMS-main/frontend/src/components/dashboard/UserManagement.tsx)
* **Access:** ADMIN.
* **Features:** CRUD management for user accounts. Controls department assignments, club responsibilities, role changes, and passwords.
* **API Endpoints called:** `GET /api/admin/users`, `POST /api/admin/users`, `PUT /api/admin/users/{id}`, `DELETE /api/admin/users/{id}`
---
## 6. Key Workflows & Logic Specs
### A. Real-time Conflict Auditing Engine
During event creation, the backend validates for timing and location conflicts.
1. The backend runs the custom JPA method `findConflictingEvents()` matching location, institution, status (`APPROVED`), and overlapping times:
$$\text{Start}_A < \text{End}_B \quad \text{and} \quad \text{End}_A > \text{Start}_B$$
2. **Placement Override Case:**
If the proposer is a `PLACEMENT` coordinator and `cancelConflicting=true` is checked:
* The backend sets all conflicting events to `CANCELLED`.
* The backend registers a rejection/displacement reason on the cancelled events.
* The placement event is saved in `PENDING_PR` (for Principal review).
3. **General Case:**
If conflicts exist and it is not an overridden placement, the backend throws a `409 Conflict` HTTP exception with conflicting details.
### B. Event Approval Pipeline
The approval flow routes events dynamically depending on the proposer:
```
[ Faculty Proposes Event ]
[ HOD Desk Review ]
(Status: REQUESTED)
/ \
Approve Reject ──► [ Status: HOD_REJECTED ]
/
[ Principal Desk Review ]
(Status: PENDING_PR)
/ \
Approve Reject ──► [ Status: PRINCIPAL_REJECTED ]
/
[ Status: APPROVED ]
(Venue Locked / Calendar Pinned)
```
* **Bypass Rule 1:** If the proposer is the **Principal**, the event is auto-approved (`APPROVED`).
* **Bypass Rule 2:** If the proposer is an **HOD**, or the event category is **Club**, **Placement**, or **Institutional**, the proposal bypasses HOD review and goes directly to the Principal (`PENDING_PR`).
### C. Semester Swap Calendar Shift
This tool allows admins/HODs/Principals to duplicate schedules for new semesters:
1. Select source filters (e.g., Department = CSE, Academic Year = 3rd Year).
2. The UI fetches matching events, maps them to target dates (shifted by a user-specified offset, e.g., 180 days), and simulates their placement.
3. The UI checks each shifted event against the database for venue conflicts or weekends:
* If an event lands on a Saturday or Sunday, the UI suggests shifting it to the nearest Friday or Monday.
* If a venue conflict occurs, the UI flags it and suggests venue changes.
4. Clicking "Commit" submits a bulk payload to `POST /api/events/batch-create`.
---
## 7. Developer & Local Deployment Guide
### Prerequisites
* Java JDK 17
* Node.js (v18+)
* MySQL Database Server
### 1. Database Setup
1. Start your local MySQL instance.
2. Create the target schema (automatic if configuration is active):
```sql
CREATE DATABASE ems_db;
```
3. Update the credentials in `backend/src/main/resources/application.properties` (defaults: username `root`, password `Abiram@07`).
### 2. Launch Backend Server
1. Navigate to the `backend` folder.
2. Compile and run using Maven:
```bash
mvnw spring-boot:run
```
3. The server starts at `http://localhost:8081`. The `DataInitializer` runs on startup to seed the default user accounts.
### 3. Launch Frontend Client
1. Navigate to the `frontend` folder.
2. Install dependencies:
```bash
npm install
```
3. Run the development server:
```bash
npm run dev
```
4. The frontend runs at `http://localhost:5173`.
### 4. Seed User Accounts & Logins
On startup, the system seeds accounts with their default passwords:
* **Faculty Member:** `faculty@rit.edu` / `faculty123`
* **HOD:** `hod@rit.edu` / `hod123`
* **Principal:** `principal@rit.edu` / `principal123`
* **Placement Officer:** `placement@rit.edu` / `placement123`
* **Administrator:** `admin@rit.edu` / `admin123`

View File

@@ -0,0 +1,358 @@
# Rajalakshmi Institute of Technology (RIT) Events Hub — System Documentation
Welcome to the comprehensive system documentation for the **RIT Events Hub**, a premium, high-fidelity academic event management, tracking, and auditing portal designed for Rajalakshmi Institute of Technology.
This document provides a complete breakdown of the website's architecture, user roles, file structures, database schema, operational workflows, and features, accompanied by a detailed site map.
---
## 1. Overview & Core Mission
The **RIT Events Hub** is an institutional web application designed to digitize and manage the entire lifecycle of college events. It replaces manual event scheduling, paper registrations, physical ticket checks, and fragmented certificate auditing with a unified, secure portal.
### Key Objectives:
* **Decentralized Event Creation:** Enable faculty members to coordinate, schedule, and estimate budgets for departmental activities.
* **Hierarchical Approvals:** Ensure events undergo appropriate institutional oversight through a HOD-to-Admin approval pipeline.
* **Intelligent Resource Management:** Prevent venue conflicts and respect departmental/sectional seat quotas.
* **Continuous Verification for Students:** Link student attendance, certificate uploads, and On-Duty (OD) approvals step-by-step.
* **Security & Accessibility:** Provide separate, secure authentication models for internal students, external students, coordinators (faculty/HOD), and administrators.
---
## 2. Technology Stack & Integrations
The platform is built using a modern, fast, and secure web development stack:
| Layer | Technology | Description |
| :--- | :--- | :--- |
| **Frontend Framework** | React (v19), TypeScript, Vite | Multi-page single-page application (SPA) state-managed structure. |
| **Styling & Icons** | Tailwind CSS (CDN), FontAwesome | Curated responsive styling using professional color palettes (Deep Blue `#004a99` and Academic Orange `#f97316`). |
| **Backend & Auth** | Supabase (PostgreSQL, Storage, Auth) | Manages authentication, row-level security (RLS), document uploads, and database queries. |
| **PDF Generation** | `jspdf` & `jspdf-autotable` | Compiles detailed, printable event proposals containing budget sheets, schedules, and resource details. |
| **Layout Tools** | `html2canvas` | Used to capture ticket layouts for student downloads. |
| **Fonts** | Google Fonts (Inter, Playfair Display) | Custom typography reflecting academic elegance. |
---
## 3. Database Schema
The database relies on a PostgreSQL schema managed via Supabase. Below are the primary tables and relations:
### `Studentusers` (Internal Students)
* `id` (UUID, Primary Key, references auth.users)
* `name` (Text)
* `email` (Text, Unique)
* `reg_no` (Text, Unique)
* `phone` (Text)
* `department` (Text)
* `year` (Text)
* `section` (Text)
* `college_name` (Text - Default: 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY')
* `updated_at` (Timestamp)
### `externalusers` (External Students)
* `id` (UUID, Primary Key, references auth.users)
* `name` (Text)
* `email` (Text, Unique)
* `reg_no` (Text)
* `phone` (Text)
* `department` (Text)
* `year` (Text)
* `section` (Text)
* `college` (Text)
* `college_location` (Text)
* `gender` (Text)
* `updated_at` (Timestamp)
### `Facultyusers` (Coordinators & HODs)
* `id` (UUID, Primary Key, references auth.users)
* `name` (Text)
* `email` (Text, Unique)
* `dept` (Text)
* `role` (Text - e.g., 'Faculty', 'HOD', 'System Admin')
* `phone` (Text)
* `profile_photo` (Text - URL)
* `updated_at` (Timestamp)
### `Adminusers` (Principal / Director / Administrators)
* `id` (UUID, Primary Key, references auth.users)
* `name` (Text)
* `email` (Text, Unique)
* `dept` (Text - 'Administration')
* `updated_at` (Timestamp)
### `events`
* `id` (UUID, Primary Key)
* `title` (Text)
* `location` (Text - Venue name)
* `date` (Text - Standard format)
* `category` (Text - 'TECHNICAL' \| 'NON-TECHNICAL' \| 'WORKSHOP' \| 'CENTRE-ACTIVITY')
* `domain` (Text - references domains.name)
* `pricing_type` (Text - 'FREE' \| 'PAID')
* `coordinator` (Text - Coordinator Name)
* `club` (Text)
* `image` (Text - Supabase storage URL)
* `status` (Text - 'Scheduled' \| 'Event Ongoing' \| 'Completed')
* `max_participants` (Integer)
* `registration_deadline` (Timestamp)
* `duration_days` (Integer)
* `event_summary` (Text)
* `is_team_event` (Boolean)
* `team_size_limit` (Integer)
* `team_composition` (Text - 'MIXED' \| 'INTER_DEPT')
* `participant_type` (Text - 'INTERNAL' \| 'EXTERNAL' \| 'BOTH')
* `verification_status` (Text - 'PENDING' \| 'PENDING_HOD' \| 'PENDING_ADMIN' \| 'APPROVED' \| 'REJECTED')
* `refreshment_expense` (Numeric)
* `transportation_expense` (Numeric)
* `session_coverage_fee` (Numeric)
* `total_expense` (Numeric)
* `conducting_dept` (Text)
* `created_by` (UUID, references auth.users)
* `request_by_faculty` (Timestamp)
* `request_by_hod` (Timestamp)
### `event_schedule` (Session/Batch Slots)
* `id` (UUID, Primary Key)
* `event_id` (UUID, references events.id)
* `day_idx` (Integer)
* `batch_idx` (Integer)
* `date` (Text)
* `start_time` (Text)
* `end_time` (Text)
### `resource_persons` (Guest Details)
* `id` (UUID, Primary Key)
* `event_id` (UUID, references events.id)
* `day_idx` (Integer)
* `batch_idx` (Integer)
* `schedule_id` (UUID, references event_schedule.id)
* `type` (Text - 'INTERNAL' \| 'EXTERNAL')
* `name` (Text)
* `dept` (Text)
* `college_name` (Text)
* `phone` (Text)
* `email` (Text)
### `event_dept_limits` (Departmental Seat Allocations)
* `id` (UUID, Primary Key)
* `event_id` (UUID, references events.id)
* `department` (Text)
* `max_seats` (Integer)
* `section_limits` (JSONB - Maps sections (A, B, C...) to integer limits)
### `registrations` (Student Bookings)
* `id` (Text, Primary Key - Structured as `${userId}_${eventId}`)
* `user_id` (UUID, references auth.users)
* `event_id` (UUID, references events.id)
* `user_email` (Text)
* `user_name` (Text)
* `reg_no` (Text)
* `phone` (Text)
* `gender` (Text)
* `dept` (Text)
* `section` (Text)
* `year` (Text)
* `college` (Text)
* `payment_status` (Text - 'PENDING' \| 'COMPLETED')
* `registered_at` (Timestamp)
* `team_code` (Text)
* `team_name` (Text)
* `is_team_leader` (Boolean)
* `certification_url` (Text)
* `certification_status` (Text - 'PENDING' \| 'APPROVED' \| 'REJECTED')
* `od_url` (Text)
* `od` (Boolean - OD approval)
### `announcements` (Notice Board)
* `id` (UUID, Primary Key)
* `title` (Text)
* `message` (Text)
* `type` (Text - 'DELAY' \| 'INFO' \| 'URGENT' \| 'ENDED' \| 'ONGOING')
* `event_id` (UUID, references events.id)
* `timestamp` (Timestamp)
* `expires_at` (Timestamp)
### `special_events` (Symposiums & External Links)
* `id` (UUID, Primary Key)
* `title` (Text)
* `description` (Text)
* `link` (Text)
* `created_by` (UUID)
* `is_active` (Boolean)
* `verification_status` (Text - 'PENDING' \| 'APPROVED' \| 'REJECTED')
### `domains`
* `id` (UUID, Primary Key)
* `name` (Text, Unique)
* `category` (Text)
* `image` (Text)
* `status` (Text - 'PENDING' \| 'APPROVED' \| 'REJECTED')
* `description` (Text)
---
## 4. Operational Workflows & Key Logic
### A. Core Blockage (Preventing Registration Overhead)
To maintain academic integrity, a student **cannot register for any new events** if they have an unfinalized event from the past.
* **Condition:** If a student is registered for an event that has completed (`status === 'Completed'`), they must upload their certificate and obtain coordinator approval (`certification_status === 'APPROVED'`) and OD status before the system allows them to click "Register" on any future catalog events.
### B. Overlap Prevention (Intelligent Venue Booking)
During event creation, coordinates are validated against active bookings:
* **Conflict Condition:** When a coordinator selects a venue and date, the system queries existing approved/pending events. If another event occupies the same location, and the date ranges overlap based on the `durationDays`, that venue will be flagged as "Booked" or "Under Verification" in the dropdown, disabling selection.
### C. Quota-Constrained Bookings (Sectional & Departmental Caps)
The event organizer can set constraints for RIT students:
1. **Department Limits:** Limits registration to `X` seats per department.
2. **Section Limits:** Further restricts seats down to individual sections (e.g., CSE section `A` gets 10 seats, section `B` gets 5 seats).
3. **Validation:** When a student attempts registration, the system evaluates their profile (department, section) and checks the respective quota counts in the `registrations` table. If the quota is full, booking is blocked.
### D. Team play Alliance (Team Registrations)
* For team events, the first student registers and clicks "Form Team", which inserts a random 6-character alphanumeric code (`team_code`) and designates them as `is_team_leader`.
* Subsequent students register for the event, click "Join Team", and input the code. The system verifies the team is not full (respecting `teamSizeLimit`) and matches `INTER_DEPT` composition requirements before assigning the `team_code` to their registration.
### E. Proposal PDF Compile
Coordinators and Admin can generate a standardized PDF proposal containing:
1. General overview (departments, clubs, metadata).
2. Logistical details (location, dates, capacity, formats).
3. Day-by-day itineraries mapped to guest resource persons and timings.
4. Departmental seat allocation tables.
5. Financial projections (Refreshment + Transportation + Session Fees = Total projected budget).
6. Signature/Verification box showing HOD & Admin approval timestamps.
---
## 5. Portal Flow & Visual Site Map
### A. Navigational Site Map Structure
```
[WELCOME GATEWAY]
├── STUDENT PORTAL ────► [STUDENT AUTH (Login / Sign Up)]
│ │
│ ├── HOME DASHBOARD (Notices, Banner, Stats)
│ ├── EVENTS CATALOG (Details, Booking, Team Play)
│ ├── REGISTRATIONS (QR Tickets, Track Progress)
│ ├── STATUS TRACKER (Detailed Lifecycle Step Visualizer)
│ └── PROFILE VIEW (Update Info, Uploaded Credentials)
├── FACULTY PORTAL ────► [COORDINATOR AUTH (Faculty / HOD)]
│ │
│ ├── OVERVIEW (Metrics & Quick Gateways)
│ ├── GENERATE EVENTS (Normal, Special, Domains)
│ ├── CREATIONS REGISTRY (Manage Owned Events, Edit/Delete)
│ ├── STATUS TRACKER (Verify Certificates, Audit ODs)
│ ├── EVENT STATUS CONTROL (Announcements, Status Updates)
│ ├── ATTENDANCE CONSOLE (Mark Student Attendance)
│ ├── PARTICIPANTS console (Download Rosters)
│ ├── PROFILE / LOG OUT
│ └── HOD VERIFICATION (HOD only - Review Departmental Proposals)
└── ADMIN PORTAL ──────► [ADMIN AUTH (Principal / Director / System Admin)]
├── OVERVIEW (System Metrics)
├── VERIFICATION QUEUE (Standard Events, Special Events, Domains)
├── USER MANAGEMENT CONSOLE (Update Profiles, Change Roles)
├── VENUE TRACKER CALENDAR (Visual Schedule & Collision Map)
├── STATUS TRACKER (Audit all college certifications)
├── GLOBAL NOTICES (Institutional notices)
└── PROFILE / LOG OUT
```
### B. High-Fidelity Mermaid Routing Diagram
```mermaid
graph TD
%% Base Gateways
Welcome[Welcome Screen] -->|Select STUDENT| StudentAuth[Student Authentication]
Welcome -->|Select COORDINATOR| FacultyAuth[Faculty / HOD Authentication]
Welcome -->|Select ADMIN| AdminAuth[System Admin Authentication]
%% Student Pathways
StudentAuth -->|Authenticated| StudentDash[Student Dashboard]
StudentDash --> SHome[Home Dashboard]
StudentDash --> SEvents[Events Catalog]
StudentDash --> SRegs[Registrations & QR Tickets]
StudentDash --> STrack[Event Status Tracker]
StudentDash --> SProfile[Student Profile]
SEvents -->|Register & Form/Join Team| STrack
SRegs -->|Verify Status / Upload Certificate| STrack
%% Faculty Pathways
FacultyAuth -->|Select Access Level| FacultyLvl{Access Level}
FacultyLvl -->|Faculty| FacultyOverview[Faculty Dashboard]
FacultyLvl -->|HOD| HODOverview[Faculty Dashboard + HOD Verification]
FacultyOverview --> FHome[Overview & Statistics]
FacultyOverview --> FCreate[Create Event Form]
FacultyOverview --> FCreations[Manage Creations]
FacultyOverview --> FTracker[Status Tracker - Certificate Audit]
FacultyOverview --> FStatus[Event Status & Notices]
FacultyOverview --> FAttendance[Attendance Console]
FacultyOverview --> FParticipants[Participants Console]
FacultyOverview --> FProfile[Faculty Profile]
HODOverview --> HVerify[HOD Verification Panel]
HVerify -->|Accept Department Request| FTracker
HVerify -->|Reject Request| FCreations
%% Admin Pathways
AdminAuth --> AdminOverview[Admin Hub]
AdminOverview --> AHome[System Overview]
AdminOverview --> AVerifyQueue[Verification Queue - Events/Domains/Special]
AdminOverview --> AUsers[User Management Console]
AdminOverview --> AVenue[Venue Tracker Calendar]
AdminOverview --> ATracker[Status Tracker]
AdminOverview --> ANotices[Global Announcements]
AdminOverview --> AProfile[Admin Profile]
%% Cross-Links
FCreations -->|Submit Proposal| HVerify
HVerify -->|Send to Admin Queue| AVerifyQueue
AVerifyQueue -->|Approve Event| SEvents
```
---
## 6. Directory Layout & Components Guide
### File & Component Inventory
* `App.tsx`: Main routing and authentication portal manager. Resolves session state, verifies metadata credentials, performs portal security checks, and loads global states.
* `supabase.ts`: Supabase client configuration, administrative client helper, and helper functions for uploading media (images, certificates) directly to Supabase storage.
* `types.ts`: TypeScript interfaces defining `Event`, `AppState`, `UserRole`, `StudentRequest`, `Ticket`, `ResourcePerson`, `EventSchedule`, `Announcement`, and `UserProfile`.
* `constants.tsx`: Lists static categories, default college domain mappings, clubs, and default showcase events.
* `utils/pdfGenerator.ts`: Handles compiling and exporting the official document proposals for verified events using `jspdf`.
#### Components Directory (`/components/`):
1. `WelcomeScreen.tsx`: Entrance portal page displaying entry gates for students, faculty coordinators, and admins.
2. `LoginForm.tsx`: sliding dual-panel login and sign up form handling internal/external student data collection and role checks.
3. `Dashboard.tsx`: Base container routing student pages.
4. `HomeDashboard.tsx`: Displays active announcements, stats, special events, and links to catalogs.
5. `Hero.tsx`: Dynamic slide search and welcome banner for students.
6. `UpcomingEventsSlider.tsx`: Auto-scrolling showcase slider displaying upcoming events.
7. `AboutHubSection.tsx`: Summary presentation card describing the Events Hub platform.
8. `StatsSection.tsx`: Shows real-time totals of events, participants, and clubs.
9. `AboutSection.tsx`: Institutional overview details, college details, and guidelines.
10. `ContactSection.tsx`: Map coordinates, support addresses, and telephone details for college portals.
11. `EventList.tsx`: Searchable list displaying approved events by domain filter and category.
12. `EventCard.tsx`: Display card for events containing metadata triggers, register commands, and status links.
13. `RegistrationsView.tsx`: Displays registered ticket vouchers. Generates ticket QR code cards capturing registration details, allowing downloading ticket PDFs.
14. `StatusTrackerView.tsx`: visual step progress checker showing the student their certificate review, attendance logs, and OD approvals.
15. `ProfileView.tsx`: Form for students to edit profile settings and view academic transcripts.
16. `AdminLandingPage.tsx`: Base dashboard routing all Coordinator actions.
17. `CreateEventForm.tsx`: Multi-step event proposal compiler managing budgets, itineraries, and quotas.
18. `CreateSpecialEventsView.tsx`: Allows coordinators to publish external program links.
19. `CreateDomainView.tsx`: Form to submit new technical/non-technical domains for college audit.
20. `AdminStatusTrackerView.tsx`: Review board for coordinators/admins to check student certificate uploads, verify details, and approve OD statuses.
21. `AdminEventStatusView.tsx`: Controls notifications, notice updates, and events status transitions.
22. `FacultyParticipantsView.tsx`: Table listing students registered for an event with search/filter.
23. `FacultyAttendanceView.tsx`: Logs session-by-session student attendance check-ins.
24. `FacultyProfileView.tsx`: Base profile for faculty.
25. `FacultyDashboard.tsx`: Console routing Admin actions.
26. `UserManagementView.tsx`: Table displaying all registered users. Admins can edit profile metadata, manually add students, or alter roles.
27. `TicketVerificationView.tsx`: Camera/code scanner allowing organizers to scan student ticket QR codes and update status in real-time.
28. `Footer.tsx`: Institutional styled footer.
29. `PortalAnimation.tsx`: Page transitions and loading animation overlays.

View File

@@ -0,0 +1,332 @@
# RIT Event Management System (EMS) - Project Overview & Site Map
This document provides a comprehensive technical overview of the RIT Event Management System (EMS). It details the core goals, features, technology stack, database architecture, directory layout, and a functional site map.
This file is structured specifically to allow developers and agentic AI systems to decode and understand the system perfectly.
---
## 1. Project Goal & Context
The **RIT Event Management System (EMS)** is a unified platform developed for Rajalakshmi Institute of Technology (RIT) to manage, propose, audit, approve, and automate institutional events. Events range from academic workshops and guest lectures to placement drives, cultural events, and sports activities.
The system addresses critical administrative bottlenecks such as:
1. **Venue Conflicts:** Preventing overlapping bookings for key venues (e.g., auditoriums, seminar halls, computer labs).
2. **Multi-tier Approvals:** Routing proposals through HODs and the Principal based on roles, departments, and event categories.
3. **Semester Scheduling:** Transitioning calendar plans from one semester to the next with minimal manual effort.
4. **Data Aggregation:** Importing bulk event plans directly from external sources (such as Google Form spreadsheet responses).
---
## 2. Technology Stack
### Backend
* **Core Framework:** Spring Boot 3.2.5 (Java 17)
* **API Architecture:** RESTful Controllers
* **Data Access Layer:** Spring Data JPA with Hibernate
* **Database:** MySQL (local schema: `ems_db`)
* **Security:** Spring Security (endpoints are permit-all at filter level, but validation and role checks occur inside controller services; password encryption via `BCryptPasswordEncoder`)
* **Developer Tools:** Project Lombok, Spring Boot DevTools
### Frontend
* **Core Framework:** React 19, TypeScript, Vite
* **Routing & State:** Component/Context state driving dynamic view rendering in a single-page application (SPA) shell
* **Styling:** Tailwind CSS, custom Vanilla CSS tokens, modern glassmorphism aesthetic
* **Animations:** Framer Motion (for smooth layout and viewport transitions)
* **Icons:** Lucide React
* **Utility Libraries:**
* `date-fns` for robust date comparisons and timeline layouts
* `xlsx` for parsing spreadsheet files (.xlsx/.csv) in-browser
---
## 3. Database Schema & Architecture
The system contains four principal entities mapped via JPA.
### A. User Entity (`users`)
Holds institutional account profiles, login credentials, permissions, and roles.
* `id` (Long, PK): Auto-incremented identifier.
* `email` (String, Unique): Institutional email (e.g., `@rit.edu` or `@ritindia.edu`).
* `password` (String): BCrypt hashed passcode.
* `fullName` (String): User's display name.
* `role` (String): Access levels: `ADMIN`, `PRINCIPAL`, `HOD`, `FACULTY`, `PLACEMENT`.
* `department` (String): E.g., `CSE`, `AI&ML`, `ECE`, `H&S Dept`, etc.
* `isClubCoordinator` (Boolean): Grants authorization to manage student club events.
* `isPlacementStaff` (Boolean): Grants authority to propose overriding placement activities.
* `isClassIncharge` (Boolean): Grants class representative dashboard view.
* `classStrength` (Integer): Total student count for incharge class.
* `inchargeClass` / `inchargeBatch` / `inchargeSection` (String): Targeted class details.
* `assignedClubs` (ElementCollection): List of student clubs the user is permitted to coordinate.
### B. Event Entity (`events`)
Models event proposals, scheduling, requirements, and life-cycles.
* `id` (Long, PK)
* `title` (String, Nullable=false)
* `description` (Text)
* `startDate` / `endDate` (LocalDateTime)
* `location` (String): Maps to standard venues (e.g., `GB 4th floor auditorium`, `Wozniak Auditorium`).
* `category` (String): Categories dictate processing logic: `ACADEMIC`, `CLUB`, `PLACEMENT`, `SPORTS`, `INSTITUTIONAL`.
* `type` (String): E.g., `Workshop`, `Seminar`, `Guest Lecture`, `Competition`.
* `institution` (String): `RIT` or `RSB`.
* `department` (String)
* `academicYears` (ElementCollection): List of targeted academic batches (e.g., `1st Year`, `2nd Year`).
* `targetedSections` (ElementCollection): Targeted sections (e.g., `A`, `B`).
* `status` (String): `REQUESTED` (Pending HoD review), `PENDING_PR` (Pending Principal approval), `APPROVED` (Active scheduled event), `COMPLETED` (Past event), `CANCELLED`, `HOD_REJECTED`, `PRINCIPAL_REJECTED`.
* `guestName` / `guestSocialProfile` (String)
* `requirements` (ElementCollection): Mandatory resources (e.g., `Mementos`, `Projector`, `Lunch`, `Wi-Fi`).
* `budget` (Double)
* `hasRegistrationFee` (Boolean) / `registrationFee` (Double)
* `sponsors` (ElementCollection)
* `proposer` (ManyToOne -> `User`)
* `rejectionReason` (Text)
* `groupRequestId` (String): Identifier tying grouped recurring requests.
### C. ClassMapping Entity (`classes`)
Used for maintaining class information and batch promotions.
* `id` (Long, PK)
* `institution` (String): E.g., `RIT`, `RSB`.
* `department` (String)
* `academicYear` (String): E.g., `1st Year`, `2nd Year`, etc.
* `sections` (ElementCollection): List of section names associated with this mapping.
* `status` (String): Defaults to `Ready`.
### D. InstitutionalNote Entity (`institutional_notes`)
Used for pinning warnings, reminders, or notifications on the main calendar.
* `id` (Long, PK)
* `content` (String, Nullable=false)
* `targetDate` (LocalDate)
* `authorName` / `authorEmail` (String)
* `createdAt` (LocalDateTime)
---
## 4. Key Workflows & Backend API Endpoints
### A. Authentication
1. **Login Flow:** User submits credentials to `POST /api/auth/login`.
2. **Matching:** The system performs database lookup and BCrypt check.
3. **Response:** A JSON representation of the logged-in User is returned (stored in `localStorage` as `ems_user`).
### B. Event Proposal & Approval Chain
1. **Proposal submission:** Submits to `POST /api/events/propose`.
2. **Real-time Conflict Audit:** The backend executes `findConflictingEvents()` matching `location`, `institution`, and date overlaps against `APPROVED` events.
* **Placement Overriding:** If the new proposal is a `PLACEMENT` event and `cancelConflicting` is `true`, the system sets conflicting events' status to `CANCELLED` and writes a displacement reason.
* **General Conflict:** If a conflict exists and it isn't an overridden placement, a `409 Conflict` status is returned with overlap logs.
3. **Workflow Routing:**
* Proposer is **Principal** -> Auto-approved (`APPROVED`).
* Proposer is **HoD**, or event is **Club/Placement/Institutional** -> Sent straight to Principal (`PENDING_PR`).
* Proposer is **Faculty** -> Sent to HoD (`REQUESTED`).
4. **HoD Action (`POST /api/events/{id}/approve`):** Sets status to `PENDING_PR` (Principal Desk).
5. **Principal Action (`POST /api/events/{id}/approve`):** Sets status to `APPROVED`.
6. **Rejections (`POST /api/events/{id}/reject`):** Updates status to `HOD_REJECTED` or `PRINCIPAL_REJECTED` and embeds a mandatory `reason`.
### C. Class Promotions
* `POST /api/classes/promote?institution=RIT`
* Promotes all class mappings for the specified institution:
* `1st Year` -> `2nd Year`
* `2nd Year` -> `3rd Year`
* `3rd Year` -> `4th Year`
* `4th Year` mappings are deleted (graduated).
---
## 5. Directory Layout & Architecture
### Backend Layout
```text
backend/src/main/java/com/ems/backend/
├── BackendApplication.java # Boot Entry Point
├── config/
│ ├── DataInitializer.java # Verify and populate seed users on startup
│ ├── DataSeeder.java # Verify and seed mock events on startup
│ ├── SecurityConfig.java # CORS and filter rules configuration
│ └── CustomUserDetailsService # Loads core user details for Spring Security
├── controller/
│ ├── AdminController.java # CRUD endpoints for Users (/api/admin/users)
│ ├── AuthController.java # Login endpoints (/api/auth)
│ ├── ClassController.java # Class mappings and promotions (/api/classes)
│ ├── EventController.java # Propose, approve, batch-create events (/api/events)
│ ├── HealthController.java # System and database connectivity check (/api/health)
│ └── NoteController.java # Board notices and institutional logs (/api/notes)
├── dto/
│ └── LoginRequest.java # Login DTO container
├── model/
│ ├── BaseEntity.java # Audited fields ID superclass
│ ├── ClassMapping.java # Class config model
│ ├── Event.java # Core event proposal model
│ ├── InstitutionalNote.java # Date-based board notices model
│ └── User.java # Account profiles model
└── repository/
├── ClassRepository.java
├── EventRepository.java # Custom JPA Query for timing conflict resolution
├── InstitutionalNoteRepository.java
└── UserRepository.java
```
### Frontend Layout
```text
frontend/src/
├── App.tsx # Main Router shell
├── main.tsx # React Entry mount
├── App.css # Framework and styling additions
├── index.css # Core Tailwind and glassmorphism styling definitions
├── assets/ # Logos, background images
├── context/
│ └── AuthContext.tsx # Session controller (localStorage wrapper)
├── lib/
│ ├── config.ts # API Base URL config (http://localhost:8081)
│ └── utils.ts # Class-name merging helper (cn)
├── pages/
│ └── LoginPage.tsx # Institutional Portal interface
└── components/
├── DashboardLayout.tsx # Frame containing sidebar, header, and content
├── Sidebar.tsx # Interactive sidebar navigation (role filtered)
└── dashboard/ # Workspace Tab Components:
├── Overview.tsx # Dashboard welcome, stats, and calendars
├── AllEvents.tsx # Grid view and edit trigger for all system events
├── EventProposalForm.x # Configurator for single and recurring proposals
├── ApprovalsView.tsx # HoD/Principal checklist for pending items
├── InstitutionalChecklist.tsx # Audit checklist of requirement fulfillments
├── InstitutionalCalendar.tsx # Dynamic React Calendar showing notes/events
├── VenueTimeline.tsx # Live visual scheduling chart of venue occupancies
├── VenuesAtGlance.tsx # Today's room availability checker
├── FacultyEventManagement.tsx # Faculty personal dashboard view
├── PrincipalMasterChecklist.x # Checklist of all active requirements
├── ClubInstitutionalChecklist.x # Special checklist for Student Clubs
├── AutomationView.tsx # Semester Swap automation dashboard
├── ExcelImport.tsx # Bulk spreadsheet spreadsheet import tool
├── UserManagement.tsx # Admin credentials manager
├── ClassManagement.tsx # Admin class config manager
├── EventDetailsModal.tsx # Detailed view overlays
└── Pagination.tsx # Table list page controllers
```
---
## 6. Functional Site Map (Page Router Decodes)
The following map defines frontend routes, navigation identifiers, role restrictions, and the underlying controllers/endpoints.
```mermaid
graph TD
Login[LoginPage.tsx] -->|Auth Success| Dashboard[App.tsx Dashboard Shell]
subgraph Dashboard Views
OverviewView[Overview.tsx <br> Dashboard Option]
ProposalView[EventProposalForm.tsx <br> Propose Option]
ApprovalsView[ApprovalsView.tsx <br> Approvals Option]
AllEventsView[AllEvents.tsx <br> All Events Option]
ChecklistView[InstitutionalChecklist.tsx <br> Checklist Option]
HistoryView[EventHistory.tsx <br> History Option]
ClubsView[ClubInstitutionalChecklist.tsx <br> Clubs Option]
AutomationView[AutomationView.tsx <br> Automation Option]
ExcelImportView[ExcelImport.tsx <br> Excel Import Option]
ClassesView[ClassManagement.tsx <br> Classes Option]
UsersView[UserManagement.tsx <br> Manage Users Option]
end
classDef roleAll fill:#e0f7fa,stroke:#00acc1,stroke-width:1px;
classDef roleStaff fill:#e8eaf6,stroke:#3f51b5,stroke-width:1px;
classDef roleApprover fill:#fff3e0,stroke:#ff9800,stroke-width:1px;
classDef roleAdmin fill:#fbe9e7,stroke:#ff5722,stroke-width:1px;
class OverviewView,ProposalView,AllEventsView,ChecklistView,HistoryView roleAll;
class ClubsView roleStaff;
class ApprovalsView,AutomationView roleApprover;
class ClassesView,UsersView,ExcelImportView roleAdmin;
```
### Route-by-Route Specification
#### 1. Login Gate
* **Component:** `LoginPage.tsx`
* **Route Logic:** Rendered if `isAuthenticated` is `false`.
* **Access:** Public.
* **Backend Interaction:** `POST /api/auth/login`
#### 2. Overview Dashboard (`dashboard`)
* **Component:** `Overview.tsx`
* **Access:** All Authenticated Users.
* **Contextual Features:**
* **Principal Desk:** Shows `PrincipalMasterChecklist` (auditing table).
* **Class Incharge Desk:** Shows current class details and a form to submit class strength updates via `PUT /api/admin/users/{id}`.
* **Faculty Desk:** Shows `FacultyEventManagement` (table of personal proposed events, status, edit buttons).
* **Live Analytics:** Renders circular progress counters matching categories (Academic, Cultural, Placement, Sports, Other).
* **Embedded Sub-Components:**
* `InstitutionalCalendar.tsx` (Grid of events, notes fetching from `GET /api/notes`, notes creation via `POST /api/notes`).
* `VenuesAtGlance.tsx` (Highlights venue reservation blocks).
* `VenueTimeline.tsx` (Hour-by-hour calendar lines for room allocations).
* **Backend Interaction:** `GET /api/events`, `GET /api/notes`, `POST /api/notes`
#### 3. Propose Event (`propose`)
* **Component:** `EventProposalForm.tsx`
* **Access:** `FACULTY`, `HOD`, `PRINCIPAL`, `PLACEMENT`, `ADMIN`.
* **Features:** Form requesting Title, Start/End DateTime, Location, Category (Academic, Club, Placement, Sports), Type (Workshop, Seminar, etc.), targeted Academic Years & Sections, Guest details, budget details, sponsors, and checklist requirements.
* **Modes:** Handles new proposals, recurring group proposals, and edit modes (loading existing values into form inputs).
* **Backend Interaction:** `POST /api/events/propose`, `PUT /api/events/{id}`
#### 4. Approvals (`approvals`)
* **Component:** `ApprovalsView.tsx`
* **Access:** `HOD`, `PRINCIPAL`.
* **Features:** Lists all events where status is:
* `REQUESTED` (If role is `HOD`)
* `PENDING_PR` (If role is `PRINCIPAL`)
* **Actions:** Approve triggers `POST /api/events/{id}/approve?userId={id}`. Reject opens a modal requesting description and triggers `POST /api/events/{id}/reject?userId={id}`.
* **Backend Interaction:** `GET /api/events`, `POST /api/events/{id}/approve`, `POST /api/events/{id}/reject`
#### 5. Clubs (`clubs`)
* **Component:** `ClubInstitutionalChecklist.tsx`
* **Access:** Users with `isClubCoordinator = true` or `role = ADMIN`.
* **Features:** Lists current club activity proposals and maps checklists for student societies.
* **Backend Interaction:** `GET /api/events`
#### 6. All Events (`events`)
* **Component:** `AllEvents.tsx`
* **Access:** All Authenticated Users.
* **Features:** Tabulated search dashboard filtering all system events (Upcoming vs. Finished). Allows editing events (via Propose Event view) if the user is the proposer or an administrator.
* **Backend Interaction:** `GET /api/events`
#### 7. Institutional Checklist (`checklist`)
* **Component:** `InstitutionalChecklist.tsx`
* **Access:** All Authenticated Users.
* **Features:** Generates a checklist report detailing logistics (Catering, venue prep, Wi-Fi configuration, etc.) for upcoming events.
* **Backend Interaction:** `GET /api/events`
#### 8. Event History (`history`)
* **Component:** `EventHistory.tsx`
* **Access:** All Authenticated Users.
* **Features:** Searchable catalog of past completed events with analytics summaries and export tools.
* **Backend Interaction:** `GET /api/events`
#### 9. Automation Console (`automation`)
* **Component:** `AutomationView.tsx`
* **Access:** `HOD`, `PRINCIPAL`, `ADMIN`.
* **Features:**
* **Semester Swap:** Selects source Institution/Dept/Batch (e.g. RIT CSE 3rd Year) and target Institution/Dept/Batch. Simulates calendar shifts (+/- 6 months).
* **Conflict Analysis:** Performs live in-memory evaluation for weekend events (recommends quick-shifting to Friday or Monday) and database overlaps.
* **Review Gate:** Displays simulated changes in a modal allowing the operator to Commit, Reschedule, or Skip. Committing makes a batch API request.
* **Backend Interaction:** `GET /api/events`, `POST /api/events/batch-create`
#### 10. Excel Import (`import-excel`)
* **Component:** `ExcelImport.tsx`
* **Access:** `ADMIN` (and accessible from Dashboard views).
* **Features:**
* **Fuzzy Parsing:** Converts excel columns (Form Responses) into structured Event payloads.
* **Auto-Registration:** Identifies if the listed proposer does not exist in the user database. If missing, registers a new account (`password` defaults to `Password@123`) using the `POST /api/admin/users` service.
* **Conflict Solver:** Identifies venue/timing conflicts. Suggests moving to alternative venues or shifting times.
* **Backend Interaction:** `GET /api/events`, `GET /api/admin/users`, `POST /api/admin/users`, `POST /api/events/batch-create`
#### 11. Class Configurations (`classes`)
* **Component:** `ClassManagement.tsx`
* **Access:** `ADMIN`.
* **Features:**
* Configures class metadata (Institution, Department, Academic Year, Sections).
* **Yearly Promotion:** Promotes student batches via a quick action (e.g. promoting 1st Years to 2nd Years, deleting graduating 4th Years).
* **Backend Interaction:** `GET /api/classes`, `POST /api/classes`, `DELETE /api/classes/{id}`, `POST /api/classes/promote`
#### 12. User Management (`user-management`)
* **Component:** `UserManagement.tsx`
* **Access:** `ADMIN`.
* **Features:** Add new users, edit existing user profile fields (including modifying student club assignments and Class Incharge details), delete accounts, or change passcodes.
* **Backend Interaction:** `GET /api/admin/users`, `POST /api/admin/users`, `PUT /api/admin/users/{id}`, `DELETE /api/admin/users/{id}`

2
RIT-EMS-main/backend/.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

33
RIT-EMS-main/backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip

295
RIT-EMS-main/backend/mvnw vendored Normal file
View File

@@ -0,0 +1,295 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

189
RIT-EMS-main/backend/mvnw.cmd vendored Normal file
View File

@@ -0,0 +1,189 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.4
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ems</groupId>
<artifactId>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend</name>
<description/>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,13 @@
package com.ems.backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
}

View File

@@ -0,0 +1,31 @@
package com.ems.backend.config;
import com.ems.backend.model.User;
import com.ems.backend.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email));
return new org.springframework.security.core.userdetails.User(
user.getEmail(),
user.getPassword(),
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole()))
);
}
}

View File

@@ -0,0 +1,61 @@
package com.ems.backend.config;
import com.ems.backend.model.User;
import com.ems.backend.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class DataInitializer {
@Bean
public CommandLineRunner initData(UserRepository userRepository, PasswordEncoder passwordEncoder) {
return args -> {
updateOrCreateUser(userRepository, passwordEncoder, "admin@rit.edu", "admin123", "System Administrator", "ADMIN", "ADMIN");
updateOrCreateUser(userRepository, passwordEncoder, "faculty@rit.edu", "faculty123", "Dr. Faculty Member", "FACULTY", "CSE");
updateOrCreateUser(userRepository, passwordEncoder, "hod@rit.edu", "hod123", "Prof. Head of Dept", "HOD", "AI&ML");
updateOrCreateUser(userRepository, passwordEncoder, "principal@rit.edu", "principal123", "Dr. College Principal", "PRINCIPAL", null);
updateOrCreateUser(userRepository, passwordEncoder, "placement@rit.edu", "placement123", "Placement Coordinator", "PLACEMENT", "Placement Department");
// Newly added users based on request
updateOrCreateUser(userRepository, passwordEncoder, "admin2@rit.edu", "admin123", "Secondary Admin", "ADMIN", "ADMIN");
updateOrCreateUser(userRepository, passwordEncoder, "principal2@rit.edu", "principal123", "Vice Principal", "PRINCIPAL", null);
updateOrCreateUser(userRepository, passwordEncoder, "hod_cse@rit.edu", "hod123", "CSE HOD", "HOD", "CSE");
updateOrCreateUser(userRepository, passwordEncoder, "faculty2@rit.edu", "faculty123", "Assistant Professor CSE", "FACULTY", "CSE");
System.out.println("Demo users verified and updated.");
};
}
private void updateOrCreateUser(UserRepository repo, PasswordEncoder encoder, String email, String pass, String name, String role, String dept) {
User user = repo.findByEmail(email).orElse(new User());
user.setEmail(email);
user.setPassword(encoder.encode(pass));
user.setFullName(name);
user.setRole(role);
user.setDepartment(dept);
// Synchronize flags with roles/departments
if ("PLACEMENT".equals(role) || "Placement Department".equals(dept)) {
user.setPlacementStaff(true);
}
if ("FACULTY".equals(role)) {
user.setClassIncharge(true);
user.setInchargeClass("CSE");
user.setInchargeBatch("3rd Year");
user.setInchargeSection("A");
if (user.getClassStrength() == null || user.getClassStrength() == 0) {
user.setClassStrength(60);
}
}
// Ensure new fields are initialized if new
if (user.getAssignedClubs() == null) {
user.setAssignedClubs(new java.util.ArrayList<>());
}
repo.save(user);
}
}

View File

@@ -0,0 +1,88 @@
package com.ems.backend.config;
import com.ems.backend.model.Event;
import com.ems.backend.model.User;
import com.ems.backend.repository.EventRepository;
import com.ems.backend.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@Configuration
public class DataSeeder {
@Bean
CommandLineRunner initDatabase(EventRepository eventRepository, UserRepository userRepository) {
return args -> {
// Check if we already have events to avoid duplicate seeding on every restart
// unless the user specifically wants to clear them.
// For this task, we will add them if they don't exist for these depts in this range.
User proposer = userRepository.findByEmail("faculty@rit.edu").orElse(null);
if (proposer == null) {
System.out.println("No faculty user found to seed events. Please run DataInitializer first.");
return;
}
String[] depts = {"CSE", "AI&ML", "ECE"};
String[] statuses = {"REQUESTED", "APPROVED", "COMPLETED", "CANCELLED", "PENDING_PR"};
String[] categories = {"ACADEMIC", "CLUB", "PLACEMENT", "SPORTS"};
String[] types = {"Workshop", "Seminar", "Guest Lecture", "Competition"};
String[] years = {"1st Year", "2nd Year", "3rd Year", "4th Year"};
Random random = new Random();
for (String dept : depts) {
for (int month = 1; month <= 6; month++) {
// Create 2 events per department per month
for (int i = 0; i < 2; i++) {
int day = random.nextInt(25) + 1;
int hour = 9 + random.nextInt(8);
LocalDateTime start = LocalDateTime.of(2026, month, day, hour, 0);
LocalDateTime end = start.plusHours(2 + random.nextInt(3));
String status = statuses[random.nextInt(statuses.length)];
// Logic: Past events are more likely to be COMPLETED or APPROVED
if (start.isBefore(LocalDateTime.now())) {
if (random.nextBoolean()) status = "COMPLETED";
}
String[] venues = {
"GB 4th floor auditorium",
"Wozniak Auditorium",
"C6-02 Indoor Theatre",
"H Block Guest Lecture Theatre",
"Steve Jobs Computer Centre 1",
"Steve Jobs Computer Centre 2"
};
String venue = venues[random.nextInt(venues.length)];
eventRepository.save(Event.builder()
.title(dept + " " + types[random.nextInt(types.length)] + " - " + month + "/" + day)
.description("Sample event for " + dept + " department.")
.startDate(start)
.endDate(end)
.location(venue)
.category(categories[random.nextInt(categories.length)])
.type(types[random.nextInt(types.length)])
.institution("RIT")
.department(dept)
.academicYears(Arrays.asList(years[random.nextInt(years.length)]))
.status(status)
.proposer(proposer)
.budget(1000.0 * (random.nextInt(20) + 5))
.build());
}
}
}
System.out.println("Seeded events for CSE, AI&ML, ECE for Jan-June semester.");
};
}
}

View File

@@ -0,0 +1,57 @@
package com.ems.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
)
.httpBasic(basic -> basic.disable())
.formLogin(form -> form.disable());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*")); // For development
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

View File

@@ -0,0 +1,85 @@
package com.ems.backend.controller;
import com.ems.backend.model.User;
import com.ems.backend.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/admin")
@CrossOrigin(origins = "*")
public class AdminController {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userRepository.findAll());
}
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody User user) {
if (user == null || user.getEmail() == null || user.getEmail().trim().isEmpty()) {
return ResponseEntity.status(400).body(Map.of("message", "Email is required"));
}
if (user.getFullName() == null || user.getFullName().trim().isEmpty()) {
return ResponseEntity.status(400).body(Map.of("message", "Full name is required"));
}
if (user.getPassword() == null || user.getPassword().trim().isEmpty()) {
return ResponseEntity.status(400).body(Map.of("message", "Password is required"));
}
user.setEmail(user.getEmail().trim().toLowerCase());
user.setFullName(user.getFullName().trim());
if (user.getRole() == null || user.getRole().trim().isEmpty()) {
user.setRole("FACULTY");
}
if (user.getDepartment() == null || user.getDepartment().trim().isEmpty()) {
user.setDepartment("H&S Dept");
}
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
return ResponseEntity.status(400).body(Map.of("message", "User with this email already exists"));
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
userRepository.save(user);
return ResponseEntity.ok(user);
}
@PutMapping("/users/{id}")
public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User userDetails) {
User user = userRepository.findById(id).orElseThrow();
user.setFullName(userDetails.getFullName());
user.setEmail(userDetails.getEmail());
user.setRole(userDetails.getRole());
user.setDepartment(userDetails.getDepartment());
user.setClubCoordinator(userDetails.isClubCoordinator());
user.setPlacementStaff(userDetails.isPlacementStaff());
user.setClassIncharge(userDetails.isClassIncharge());
user.setClassStrength(userDetails.getClassStrength());
user.setInchargeClass(userDetails.getInchargeClass());
user.setInchargeBatch(userDetails.getInchargeBatch());
user.setInchargeSection(userDetails.getInchargeSection());
user.setAssignedClubs(userDetails.getAssignedClubs());
if (userDetails.getPassword() != null && !userDetails.getPassword().isEmpty()) {
user.setPassword(passwordEncoder.encode(userDetails.getPassword()));
}
userRepository.save(user);
return ResponseEntity.ok(user);
}
@DeleteMapping("/users/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userRepository.deleteById(id);
return ResponseEntity.ok(Map.of("message", "User deleted successfully"));
}
}

View File

@@ -0,0 +1,53 @@
package com.ems.backend.controller;
import com.ems.backend.dto.LoginRequest;
import com.ems.backend.model.User;
import com.ems.backend.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/auth")
@CrossOrigin(origins = "*")
public class AuthController {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
Optional<User> userOptional = userRepository.findByEmail(loginRequest.getEmail());
if (userOptional.isPresent()) {
User user = userOptional.get();
if (passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
java.util.Map<String, Object> responseMap = new java.util.HashMap<>();
responseMap.put("id", user.getId());
responseMap.put("email", user.getEmail());
responseMap.put("fullName", user.getFullName());
responseMap.put("role", user.getRole());
responseMap.put("department", user.getDepartment() != null ? user.getDepartment() : "N/A");
responseMap.put("isClubCoordinator", user.isClubCoordinator());
responseMap.put("isPlacementStaff", user.isPlacementStaff());
responseMap.put("isClassIncharge", user.isClassIncharge());
responseMap.put("inchargeClass", user.getInchargeClass());
responseMap.put("inchargeBatch", user.getInchargeBatch());
responseMap.put("inchargeSection", user.getInchargeSection());
responseMap.put("classStrength", user.getClassStrength());
responseMap.put("assignedClubs", user.getAssignedClubs() != null ? user.getAssignedClubs() : java.util.List.of());
return ResponseEntity.ok(responseMap);
}
}
return ResponseEntity.status(401).body(Map.of("message", "Invalid email or passcode"));
}
}

View File

@@ -0,0 +1,70 @@
package com.ems.backend.controller;
import com.ems.backend.model.ClassMapping;
import com.ems.backend.repository.ClassRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/classes")
@CrossOrigin(origins = "*")
public class ClassController {
@Autowired
private ClassRepository classRepository;
@GetMapping
public ResponseEntity<List<ClassMapping>> getAllClasses() {
return ResponseEntity.ok(classRepository.findAll());
}
@PostMapping
public ResponseEntity<?> createClass(@RequestBody ClassMapping classMapping) {
classRepository.save(classMapping);
return ResponseEntity.ok(classMapping);
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteClass(@PathVariable Long id) {
classRepository.deleteById(id);
return ResponseEntity.ok(Map.of("message", "Class mapping deleted successfully"));
}
@PostMapping("/promote")
public ResponseEntity<?> promoteAcademicYear(@RequestParam String institution) {
List<ClassMapping> allClasses = classRepository.findAll();
for (ClassMapping cm : allClasses) {
if (!cm.getInstitution().trim().equalsIgnoreCase(institution.trim())) {
continue;
}
String currentYear = cm.getAcademicYear();
switch (currentYear) {
case "1st Year":
cm.setAcademicYear("2nd Year");
classRepository.save(cm);
break;
case "2nd Year":
cm.setAcademicYear("3rd Year");
classRepository.save(cm);
break;
case "3rd Year":
cm.setAcademicYear("4th Year");
classRepository.save(cm);
break;
case "4th Year":
classRepository.delete(cm);
break;
default:
break;
}
}
return ResponseEntity.ok(Map.of("message", "Academic year promotion completed for " + institution));
}
}

View File

@@ -0,0 +1,437 @@
package com.ems.backend.controller;
import com.ems.backend.model.Event;
import com.ems.backend.model.User;
import com.ems.backend.repository.EventRepository;
import com.ems.backend.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/events")
@CrossOrigin(origins = "*")
public class EventController {
@Autowired
private EventRepository eventRepository;
@Autowired
private UserRepository userRepository;
@PostMapping("/propose")
@Transactional
public ResponseEntity<?> proposeEvent(@RequestBody Map<String, Object> payload) {
try {
Long userId = Long.valueOf(payload.get("userId").toString());
User proposer = userRepository.findById(userId).orElseThrow();
if (!"FACULTY".equals(proposer.getRole()) &&
!"HOD".equals(proposer.getRole()) &&
!"PRINCIPAL".equals(proposer.getRole()) &&
!"PLACEMENT".equals(proposer.getRole()) &&
!"ADMIN".equals(proposer.getRole())) {
return ResponseEntity.status(403).body(Map.of("message", "Only authorized users can propose events"));
}
Event event = new Event();
event.setTitle(payload.get("eventName").toString());
event.setStartDate(parseDateTime(payload.get("startDate")));
event.setEndDate(parseDateTime(payload.get("endDate")));
event.setType(payload.get("eventType") != null ? payload.get("eventType").toString() : "Institutional");
event.setInstitution(payload.get("institution").toString());
event.setDepartment(payload.get("department").toString());
event.setAcademicYears((List<String>) payload.get("academicYears"));
event.setLocation(payload.get("venue").toString());
event.setGuestName(payload.get("guestName") != null ? payload.get("guestName").toString() : null);
event.setGuestSocialProfile(payload.get("socialProfile") != null ? payload.get("socialProfile").toString() : null);
event.setRequirements((List<String>) payload.get("requirements"));
event.setTargetedSections((List<String>) payload.get("targetedSections"));
event.setGroupRequestId(payload.get("groupRequestId") != null ? payload.get("groupRequestId").toString() : null);
// New Fields
event.setDescription(payload.get("description") != null ? payload.get("description").toString() : null);
event.setSponsors((List<String>) payload.get("sponsors"));
String budgetStr = payload.get("budget") != null ? payload.get("budget").toString() : "";
event.setBudget(budgetStr.isEmpty() ? 0.0 : Double.valueOf(budgetStr));
event.setHasRegistrationFee(payload.get("hasRegistrationFee") != null && (boolean) payload.get("hasRegistrationFee"));
String feeStr = payload.get("registrationFee") != null ? payload.get("registrationFee").toString() : "";
event.setRegistrationFee(feeStr.isEmpty() ? 0.0 : Double.valueOf(feeStr));
String category = payload.get("category") != null ? payload.get("category").toString() : "ACADEMIC";
event.setCategory(category);
event.setCentreName(payload.get("centreName") != null ? payload.get("centreName").toString() : null);
if (payload.containsKey("isPublicEvent")) {
event.setPublicEvent((boolean) payload.get("isPublicEvent"));
}
// Workflow Logic
if ("PRINCIPAL".equals(proposer.getRole())) {
event.setStatus("APPROVED");
} else if ("HOD".equals(proposer.getRole()) ||
"CLUB".equals(category) ||
"PLACEMENT".equals(category) ||
"INSTITUTIONAL".equals(category) ||
"PLACEMENT".equals(proposer.getRole()) ||
proposer.isPlacementStaff() ||
"Placement Department".equals(proposer.getDepartment())) {
event.setStatus("PENDING_PR");
} else {
event.setStatus("REQUESTED");
}
event.setProposer(proposer);
if (event.getStartDate().isBefore(LocalDateTime.now())) {
return ResponseEntity.status(400).body(Map.of("message", "Cannot schedule events in the past"));
}
if (event.getEndDate().isBefore(event.getStartDate()) || event.getEndDate().isEqual(event.getStartDate())) {
return ResponseEntity.status(400).body(Map.of("message", "End date must be after start date"));
}
event.setLocation(event.getLocation().trim());
event.setInstitution(event.getInstitution().trim());
// Priority & Conflict Logic
List<Event> conflicts = eventRepository.findConflictingEvents(
event.getInstitution(),
event.getLocation(),
event.getStartDate(),
event.getEndDate(),
-1L,
event.getGroupRequestId()
);
if (!conflicts.isEmpty()) {
if ("PLACEMENT".equals(category) && Boolean.TRUE.equals(payload.get("cancelConflicting"))) {
for (Event conflict : conflicts) {
conflict.setStatus("CANCELLED");
conflict.setRejectionReason("This event is cancelled due to the placement activity happening at the venue at this timing.");
eventRepository.save(conflict);
}
} else {
String conflictMsg = getConflictMessage(event);
return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of(
"message", conflictMsg,
"conflicts", conflicts,
"canOverride", "PLACEMENT".equals(category)
));
}
}
eventRepository.save(event);
return ResponseEntity.ok(Map.of("message", "Event proposed successfully", "id", event.getId()));
} catch (Exception e) {
return ResponseEntity.status(400).body(Map.of("message", "Failed to propose event: " + e.getMessage()));
}
}
@GetMapping
public List<Event> getAllEvents() {
List<Event> events = eventRepository.findAll();
populateConflictMessages(events);
return events;
}
@PostMapping("/batch-create")
@Transactional
public ResponseEntity<?> batchCreateEvents(@RequestBody List<Map<String, Object>> eventsPayload) {
try {
if (eventsPayload == null || eventsPayload.isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("message", "No events provided for import"));
}
List<Event> eventsToSave = new ArrayList<>();
for (Map<String, Object> payload : eventsPayload) {
Event event = new Event();
String title = payload.get("title") != null ? payload.get("title").toString() : "Untitled Event";
event.setTitle(title);
Object dateObj = payload.get("startDate") != null ? payload.get("startDate") : payload.get("finalDate");
if (dateObj == null) {
throw new RuntimeException("Missing date for event: " + title);
}
LocalDateTime start = parseDateTime(dateObj);
event.setStartDate(start);
// Try to calculate duration from original event if available
if (payload.get("startDate") != null && payload.get("endDate") != null) {
try {
LocalDateTime origStart = parseDateTime(payload.get("startDate"));
LocalDateTime origEnd = parseDateTime(payload.get("endDate"));
java.time.Duration duration = java.time.Duration.between(origStart, origEnd);
event.setEndDate(start.plus(duration));
} catch (Exception e) {
event.setEndDate(start.plusHours(2));
}
} else {
event.setEndDate(start.plusHours(2));
}
event.setType(payload.get("type") != null ? payload.get("type").toString() : "Seminar");
event.setInstitution(payload.get("institution") != null ? payload.get("institution").toString().trim() : "RIT");
String dept = payload.get("targetDepartment") != null ? payload.get("targetDepartment").toString() :
(payload.get("department") != null ? payload.get("department").toString() : "General");
event.setDepartment(dept);
Object ay = payload.get("targetBatch") != null ? List.of(payload.get("targetBatch").toString()) : payload.get("academicYears");
event.setAcademicYears((List<String>) ay);
String venue = payload.get("venue") != null ? payload.get("venue").toString() :
(payload.get("location") != null ? payload.get("location").toString() : "TBD");
event.setLocation(venue.trim());
event.setCategory(payload.get("category") != null ? payload.get("category").toString() : "ACADEMIC");
event.setStatus(payload.get("status") != null ? payload.get("status").toString() : "APPROVED");
if (payload.containsKey("requirements")) {
event.setRequirements((List<String>) payload.get("requirements"));
}
if (payload.get("proposer") != null) {
try {
Object proposerObj = payload.get("proposer");
if (proposerObj instanceof Map) {
Map<String, Object> prop = (Map<String, Object>) proposerObj;
Object idObj = prop.get("id");
if (idObj != null) {
Long propId = Long.valueOf(idObj.toString());
userRepository.findById(propId).ifPresent(event::setProposer);
}
}
} catch (Exception e) {
System.err.println("Warning: Could not map proposer for batch event: " + e.getMessage());
}
}
if (event.getStartDate() == null || event.getEndDate() == null) {
throw new RuntimeException("Missing start or end date for event: " + title);
}
if (event.getEndDate().isBefore(event.getStartDate()) || event.getEndDate().isEqual(event.getStartDate())) {
throw new RuntimeException("End date must be after start date for event: " + title);
}
eventsToSave.add(event);
}
for (Event event : eventsToSave) {
String conflictMsg = getConflictMessage(event);
if (conflictMsg != null) {
throw new RuntimeException("Conflict in batch item '" + event.getTitle() + "': " + conflictMsg);
}
}
for (int i = 0; i < eventsToSave.size(); i++) {
Event current = eventsToSave.get(i);
for (int j = i + 1; j < eventsToSave.size(); j++) {
Event other = eventsToSave.get(j);
if (isVenueTimingConflict(current, other)) {
throw new RuntimeException("Conflict between imported events '" + current.getTitle() + "' and '" + other.getTitle() + "' at " + current.getLocation());
}
}
}
for (Event event : eventsToSave) {
eventRepository.save(event);
}
return ResponseEntity.ok(Map.of("message", "Batch events created successfully", "count", eventsToSave.size()));
} catch (Exception e) {
String errorMsg = e.getMessage() != null ? e.getMessage() : "Unknown error during batch creation";
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("message", "Failed to create batch events: " + errorMsg));
}
}
@PostMapping("/{id}/approve")
@Transactional
public ResponseEntity<?> approveEvent(@PathVariable Long id, @RequestParam Long userId) {
User user = userRepository.findById(userId).orElseThrow();
Event event = eventRepository.findById(id).orElseThrow();
String conflictMsg = getConflictMessage(event);
if (conflictMsg != null) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("message", conflictMsg));
}
if ("HOD".equals(user.getRole())) {
event.setStatus("PENDING_PR");
} else if ("PRINCIPAL".equals(user.getRole())) {
event.setStatus("APPROVED");
} else {
return ResponseEntity.status(403).body(Map.of("message", "Only HoD or Principal can approve events"));
}
eventRepository.save(event);
return ResponseEntity.ok(Map.of("message", "Event action completed successfully", "status", event.getStatus()));
}
@PostMapping("/{id}/reject")
public ResponseEntity<?> rejectEvent(@PathVariable Long id, @RequestParam Long userId, @RequestBody(required = false) Map<String, String> payload) {
User user = userRepository.findById(userId).orElseThrow();
Event event = eventRepository.findById(id).orElseThrow();
String reason = payload != null ? payload.get("reason") : null;
if (reason == null || reason.trim().isEmpty()) {
return ResponseEntity.status(400).body(Map.of("message", "Rejection reason is mandatory"));
}
if ("HOD".equals(user.getRole())) {
event.setStatus("HOD_REJECTED");
} else if ("PRINCIPAL".equals(user.getRole())) {
event.setStatus("PRINCIPAL_REJECTED");
} else {
return ResponseEntity.status(403).body(Map.of("message", "Only HoD or Principal can reject events"));
}
event.setRejectionReason(reason);
eventRepository.save(event);
return ResponseEntity.ok(Map.of("message", "Event rejected successfully", "status", event.getStatus()));
}
@PutMapping("/{id}")
@Transactional
public ResponseEntity<?> updateEvent(@PathVariable Long id, @RequestBody Map<String, Object> payload) {
try {
Event event = eventRepository.findById(id).orElseThrow();
Long userId = Long.valueOf(payload.get("userId").toString());
User user = userRepository.findById(userId).orElseThrow();
boolean isAdmin = "ADMIN".equals(user.getRole());
if (!isAdmin) {
if (!event.getProposer().getId().equals(userId)) {
return ResponseEntity.status(403).body(Map.of("message", "Only the proposer can edit this event"));
}
if (!"REQUESTED".equals(event.getStatus())) {
return ResponseEntity.status(403).body(Map.of("message", "Event cannot be edited once it moves past the initial request stage"));
}
}
if (payload.containsKey("title")) event.setTitle(payload.get("title").toString());
if (payload.containsKey("startDate")) event.setStartDate(parseDateTime(payload.get("startDate")));
if (payload.containsKey("endDate")) event.setEndDate(parseDateTime(payload.get("endDate")));
if (payload.containsKey("eventType")) event.setType(payload.get("eventType").toString());
if (payload.containsKey("institution")) event.setInstitution(payload.get("institution").toString());
if (payload.containsKey("department")) event.setDepartment(payload.get("department").toString());
if (payload.containsKey("venue")) event.setLocation(payload.get("venue").toString());
if (payload.containsKey("guestName")) event.setGuestName(payload.get("guestName").toString());
if (payload.containsKey("socialProfile")) event.setGuestSocialProfile(payload.get("socialProfile").toString());
if (payload.containsKey("academicYears")) event.setAcademicYears((List<String>) payload.get("academicYears"));
if (payload.containsKey("targetedSections")) event.setTargetedSections((List<String>) payload.get("targetedSections"));
if (payload.containsKey("requirements")) event.setRequirements((List<String>) payload.get("requirements"));
if (payload.containsKey("sponsors")) event.setSponsors((List<String>) payload.get("sponsors"));
if (payload.containsKey("description")) event.setDescription(payload.get("description").toString());
if (payload.containsKey("budget")) {
String budgetStr = payload.get("budget").toString();
event.setBudget(budgetStr.isEmpty() ? 0.0 : Double.valueOf(budgetStr));
}
if (payload.containsKey("hasRegistrationFee")) event.setHasRegistrationFee((boolean) payload.get("hasRegistrationFee"));
if (payload.containsKey("registrationFee")) {
String feeStr = payload.get("registrationFee").toString();
event.setRegistrationFee(feeStr.isEmpty() ? 0.0 : Double.valueOf(feeStr));
}
if (payload.containsKey("centreName")) event.setCentreName(payload.get("centreName").toString());
if (payload.containsKey("isPublicEvent")) event.setPublicEvent((boolean) payload.get("isPublicEvent"));
if (payload.containsKey("status") && isAdmin) {
event.setStatus(payload.get("status").toString());
}
if (event.getEndDate().isBefore(event.getStartDate()) || event.getEndDate().isEqual(event.getStartDate())) {
return ResponseEntity.status(400).body(Map.of("message", "End date must be after start date"));
}
event.setLocation(event.getLocation().trim());
event.setInstitution(event.getInstitution().trim());
String conflictMsg = getConflictMessage(event);
if (conflictMsg != null) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("message", conflictMsg));
}
eventRepository.save(event);
return ResponseEntity.ok(Map.of("message", "Event updated successfully"));
} catch (Exception e) {
return ResponseEntity.status(400).body(Map.of("message", "Failed to update event: " + e.getMessage()));
}
}
private void populateConflictMessages(List<Event> events) {
for (Event event : events) {
if (!"APPROVED".equals(event.getStatus()) && !"COMPLETED".equals(event.getStatus()) && !"CANCELLED".equals(event.getStatus())) {
event.setConflictMessage(getConflictMessage(event));
}
}
}
private String getConflictMessage(Event event) {
LocalDateTime start = event.getStartDate();
LocalDateTime end = event.getEndDate();
String location = event.getLocation().trim();
String institution = event.getInstitution().trim();
List<Event> conflicts = eventRepository.findConflictingEvents(
institution,
location,
start,
end,
event.getId() != null ? event.getId() : -1L,
event.getGroupRequestId()
);
if (!conflicts.isEmpty()) {
Event conflict = conflicts.get(0);
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
return String.format("Venue Conflict: '%s' is already booked for '%s' from %s to %s",
location, conflict.getTitle(),
conflict.getStartDate().format(timeFormatter),
conflict.getEndDate().format(timeFormatter));
}
return null;
}
private boolean isVenueTimingConflict(Event first, Event second) {
if ("CANCELLED".equals(first.getStatus()) || "CANCELLED".equals(second.getStatus())) {
return false;
}
return first.getInstitution().trim().equals(second.getInstitution().trim())
&& first.getLocation().trim().equalsIgnoreCase(second.getLocation().trim())
&& first.getStartDate().isBefore(second.getEndDate())
&& first.getEndDate().isAfter(second.getStartDate());
}
private LocalDateTime parseDateTime(Object value) {
if (value == null) return null;
String str = value.toString().trim();
if (str.isEmpty()) return null;
str = str.replace(" ", "T");
if (str.length() == 10) str += "T00:00:00";
if (str.length() == 16) str += ":00";
if (str.contains("+")) str = str.substring(0, str.indexOf("+"));
if (str.contains("Z")) str = str.replace("Z", "");
if (str.contains(".")) str = str.substring(0, str.indexOf("."));
try {
return LocalDateTime.parse(str);
} catch (Exception e) {
System.err.println("Failed to parse date: " + str);
throw e;
}
}
}

View File

@@ -0,0 +1,34 @@
package com.ems.backend.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Map;
@RestController
public class HealthController {
@Autowired
private DataSource dataSource;
@GetMapping("/api/health")
public Map<String, Object> health() {
String dbStatus = "DOWN";
try (Connection connection = dataSource.getConnection()) {
if (connection.isValid(1)) {
dbStatus = "UP";
}
} catch (Exception e) {
dbStatus = "DOWN: " + e.getMessage();
}
return Map.of(
"status", "UP",
"message", "EMS Backend is running",
"database", dbStatus
);
}
}

View File

@@ -0,0 +1,27 @@
package com.ems.backend.controller;
import com.ems.backend.model.InstitutionalNote;
import com.ems.backend.repository.InstitutionalNoteRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/notes")
@CrossOrigin(origins = "*")
public class NoteController {
@Autowired
private InstitutionalNoteRepository noteRepository;
@GetMapping
public List<InstitutionalNote> getAllNotes() {
return noteRepository.findAll();
}
@PostMapping
public InstitutionalNote createNote(@RequestBody InstitutionalNote note) {
return noteRepository.save(note);
}
}

View File

@@ -0,0 +1,13 @@
package com.ems.backend.dto;
import lombok.Data;
public class LoginRequest {
private String email;
private String password;
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}

View File

@@ -0,0 +1,36 @@
package com.ems.backend.model;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
public BaseEntity() {}
public BaseEntity(Long id, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,44 @@
package com.ems.backend.model;
import jakarta.persistence.*;
import lombok.*;
import java.util.List;
@Entity
@Table(name = "classes")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ClassMapping extends BaseEntity {
@Column(nullable = false)
private String institution; // RIT, RSB
@Column(nullable = false)
private String department;
@Column(nullable = false)
private String academicYear; // 1st Year, 2nd Year, etc.
@ElementCollection
@CollectionTable(name = "class_sections", joinColumns = @JoinColumn(name = "class_id"))
@Column(name = "section_name")
private List<String> sections;
@Builder.Default
private String status = "Ready";
public String getInstitution() { return institution; }
public void setInstitution(String institution) { this.institution = institution; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public String getAcademicYear() { return academicYear; }
public void setAcademicYear(String academicYear) { this.academicYear = academicYear; }
public java.util.List<String> getSections() { return sections; }
public void setSections(java.util.List<String> sections) { this.sections = sections; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}

View File

@@ -0,0 +1,206 @@
package com.ems.backend.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "events")
public class Event extends BaseEntity {
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String description;
@Column(nullable = false)
private LocalDateTime startDate;
@Column(nullable = false)
private LocalDateTime endDate;
@Column(nullable = false)
private String location;
@Column(nullable = false)
private String category; // ACADEMIC, CLUB, etc.
@Column(nullable = false)
private String type; // Workshop, Seminar, etc.
@Column(nullable = false)
private String institution; // RIT, RSB
@Column(nullable = false)
private String department;
@jakarta.persistence.ElementCollection
private List<String> academicYears;
@Column(nullable = false)
private String status; // PENDING, APPROVED, COMPLETED, CANCELLED
private String guestName;
private String guestSocialProfile;
@jakarta.persistence.ElementCollection
private List<String> requirements;
@jakarta.persistence.ElementCollection
private List<String> targetedSections;
private Double budget;
@Column(columnDefinition = "boolean default false")
private boolean hasRegistrationFee;
private Double registrationFee;
@jakarta.persistence.ElementCollection
private List<String> sponsors;
@jakarta.persistence.ManyToOne
@jakarta.persistence.JoinColumn(name = "user_id")
private User proposer;
@Column(columnDefinition = "TEXT")
private String rejectionReason;
private String groupRequestId;
private String centreName;
@Column(columnDefinition = "boolean default false")
private boolean isPublicEvent;
@jakarta.persistence.Transient
private String conflictMessage;
public Event() {}
public Event(String title, String description, LocalDateTime startDate, LocalDateTime endDate, String location, String category, String type, String institution, String department, List<String> academicYears, String status, String guestName, String guestSocialProfile, List<String> requirements, List<String> targetedSections, Double budget, boolean hasRegistrationFee, Double registrationFee, User proposer, String centreName, boolean isPublicEvent) {
this.title = title;
this.description = description;
this.startDate = startDate;
this.endDate = endDate;
this.location = location;
this.category = category;
this.type = type;
this.institution = institution;
this.department = department;
this.academicYears = academicYears;
this.status = status;
this.guestName = guestName;
this.guestSocialProfile = guestSocialProfile;
this.requirements = requirements;
this.targetedSections = targetedSections;
this.budget = budget;
this.hasRegistrationFee = hasRegistrationFee;
this.registrationFee = registrationFee;
this.proposer = proposer;
this.centreName = centreName;
this.isPublicEvent = isPublicEvent;
}
// Manual Getters and Setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public LocalDateTime getStartDate() { return startDate; }
public void setStartDate(LocalDateTime startDate) { this.startDate = startDate; }
public LocalDateTime getEndDate() { return endDate; }
public void setEndDate(LocalDateTime endDate) { this.endDate = endDate; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getInstitution() { return institution; }
public void setInstitution(String institution) { this.institution = institution; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public List<String> getAcademicYears() { return academicYears; }
public void setAcademicYears(List<String> academicYears) { this.academicYears = academicYears; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getGuestName() { return guestName; }
public void setGuestName(String guestName) { this.guestName = guestName; }
public String getGuestSocialProfile() { return guestSocialProfile; }
public void setGuestSocialProfile(String guestSocialProfile) { this.guestSocialProfile = guestSocialProfile; }
public List<String> getRequirements() { return requirements; }
public void setRequirements(List<String> requirements) { this.requirements = requirements; }
public List<String> getTargetedSections() { return targetedSections; }
public void setTargetedSections(List<String> targetedSections) { this.targetedSections = targetedSections; }
public Double getBudget() { return budget; }
public void setBudget(Double budget) { this.budget = budget; }
public boolean isHasRegistrationFee() { return hasRegistrationFee; }
public void setHasRegistrationFee(boolean hasRegistrationFee) { this.hasRegistrationFee = hasRegistrationFee; }
public Double getRegistrationFee() { return registrationFee; }
public void setRegistrationFee(Double registrationFee) { this.registrationFee = registrationFee; }
public List<String> getSponsors() { return sponsors; }
public void setSponsors(List<String> sponsors) { this.sponsors = sponsors; }
public User getProposer() { return proposer; }
public void setProposer(User proposer) { this.proposer = proposer; }
public String getRejectionReason() { return rejectionReason; }
public void setRejectionReason(String rejectionReason) { this.rejectionReason = rejectionReason; }
public String getGroupRequestId() { return groupRequestId; }
public void setGroupRequestId(String groupRequestId) { this.groupRequestId = groupRequestId; }
public String getConflictMessage() { return conflictMessage; }
public void setConflictMessage(String conflictMessage) { this.conflictMessage = conflictMessage; }
public String getCentreName() {
return centreName;
}
public void setCentreName(String centreName) {
this.centreName = centreName;
}
public boolean isPublicEvent() {
return isPublicEvent;
}
public void setPublicEvent(boolean publicEvent) {
isPublicEvent = publicEvent;
}
// Manual Builder
public static EventBuilder builder() {
return new EventBuilder();
}
public static class EventBuilder {
private Event event = new Event();
public EventBuilder title(String title) { event.setTitle(title); return this; }
public EventBuilder description(String description) { event.setDescription(description); return this; }
public EventBuilder startDate(LocalDateTime startDate) { event.setStartDate(startDate); return this; }
public EventBuilder endDate(LocalDateTime endDate) { event.setEndDate(endDate); return this; }
public EventBuilder location(String location) { event.setLocation(location); return this; }
public EventBuilder category(String category) { event.setCategory(category); return this; }
public EventBuilder type(String type) { event.setType(type); return this; }
public EventBuilder institution(String institution) { event.setInstitution(institution); return this; }
public EventBuilder department(String department) { event.setDepartment(department); return this; }
public EventBuilder academicYears(List<String> academicYears) { event.setAcademicYears(academicYears); return this; }
public EventBuilder status(String status) { event.setStatus(status); return this; }
public EventBuilder guestName(String guestName) { event.setGuestName(guestName); return this; }
public EventBuilder guestSocialProfile(String guestSocialProfile) { event.setGuestSocialProfile(guestSocialProfile); return this; }
public EventBuilder requirements(List<String> requirements) { event.setRequirements(requirements); return this; }
public EventBuilder targetedSections(List<String> targetedSections) { event.setTargetedSections(targetedSections); return this; }
public EventBuilder budget(Double budget) { event.setBudget(budget); return this; }
public EventBuilder hasRegistrationFee(boolean hasRegistrationFee) { event.setHasRegistrationFee(hasRegistrationFee); return this; }
public EventBuilder registrationFee(Double registrationFee) { event.setRegistrationFee(registrationFee); return this; }
public EventBuilder sponsors(List<String> sponsors) { event.setSponsors(sponsors); return this; }
public EventBuilder proposer(User proposer) { event.setProposer(proposer); return this; }
public EventBuilder groupRequestId(String groupRequestId) { event.setGroupRequestId(groupRequestId); return this; }
public Event build() {
return event;
}
}
}

View File

@@ -0,0 +1,31 @@
package com.ems.backend.model;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Entity
@Data
@Table(name = "institutional_notes")
public class InstitutionalNote {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String content;
@Column(nullable = false)
private LocalDate targetDate;
private String authorName;
private String authorEmail;
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,94 @@
package com.ems.backend.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@Entity
@Table(name = "users")
public class User extends BaseEntity {
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String fullName;
@Column(nullable = false)
private String role; // FACULTY, HOD, PRINCIPAL
private String department;
@Column(columnDefinition = "boolean default false")
@JsonProperty("isClubCoordinator")
private boolean isClubCoordinator;
@Column(columnDefinition = "boolean default false")
@JsonProperty("isPlacementStaff")
private boolean isPlacementStaff;
@Column(columnDefinition = "boolean default false")
@JsonProperty("isClassIncharge")
private boolean isClassIncharge;
@Column
private Integer classStrength;
@Column
private String inchargeClass;
@Column
private String inchargeBatch;
@Column
private String inchargeSection;
@jakarta.persistence.ElementCollection
@JsonProperty("assignedClubs")
private List<String> assignedClubs;
public User() {}
public User(String email, String password, String fullName, String role, String department, boolean isClubCoordinator, boolean isPlacementStaff, List<String> assignedClubs) {
this.email = email;
this.password = password;
this.fullName = fullName;
this.role = role;
this.department = department;
this.isClubCoordinator = isClubCoordinator;
this.isPlacementStaff = isPlacementStaff;
this.assignedClubs = assignedClubs;
}
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public boolean isClubCoordinator() { return isClubCoordinator; }
public void setClubCoordinator(boolean clubCoordinator) { isClubCoordinator = clubCoordinator; }
public boolean isPlacementStaff() { return isPlacementStaff; }
public void setPlacementStaff(boolean placementStaff) { isPlacementStaff = placementStaff; }
public boolean isClassIncharge() { return isClassIncharge; }
public void setClassIncharge(boolean classIncharge) { isClassIncharge = classIncharge; }
public Integer getClassStrength() { return classStrength; }
public void setClassStrength(Integer classStrength) { this.classStrength = classStrength; }
public String getInchargeClass() { return inchargeClass; }
public void setInchargeClass(String inchargeClass) { this.inchargeClass = inchargeClass; }
public String getInchargeBatch() { return inchargeBatch; }
public void setInchargeBatch(String inchargeBatch) { this.inchargeBatch = inchargeBatch; }
public String getInchargeSection() { return inchargeSection; }
public void setInchargeSection(String inchargeSection) { this.inchargeSection = inchargeSection; }
public List<String> getAssignedClubs() { return assignedClubs; }
public void setAssignedClubs(List<String> assignedClubs) { this.assignedClubs = assignedClubs; }
}

View File

@@ -0,0 +1,9 @@
package com.ems.backend.repository;
import com.ems.backend.model.ClassMapping;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ClassRepository extends JpaRepository<ClassMapping, Long> {
}

View File

@@ -0,0 +1,26 @@
package com.ems.backend.repository;
import com.ems.backend.model.Event;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface EventRepository extends JpaRepository<Event, Long> {
List<Event> findByCategory(String category);
List<Event> findByStatus(String status);
@Query("SELECT e FROM Event e WHERE e.status = 'APPROVED' AND e.institution = :institution AND e.location = :location " +
"AND e.startDate < :endDate AND e.endDate > :startDate AND e.id != :eventId " +
"AND (e.groupRequestId IS NULL OR :groupRequestId IS NULL OR e.groupRequestId != :groupRequestId)")
List<Event> findConflictingEvents(@Param("institution") String institution,
@Param("location") String location,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
@Param("eventId") Long eventId,
@Param("groupRequestId") String groupRequestId);
}

View File

@@ -0,0 +1,10 @@
package com.ems.backend.repository;
import com.ems.backend.model.InstitutionalNote;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
public interface InstitutionalNoteRepository extends JpaRepository<InstitutionalNote, Long> {
List<InstitutionalNote> findByTargetDate(LocalDate targetDate);
}

View File

@@ -0,0 +1,12 @@
package com.ems.backend.repository;
import com.ems.backend.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}

View File

@@ -0,0 +1,14 @@
server.port=8081
spring.application.name=backend
# Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/ems_db?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=Abiram@07
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA Configuration
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

View File

@@ -0,0 +1,13 @@
package com.ems.backend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class BackendApplicationTests {
@Test
void contextLoads() {
}
}

24
RIT-EMS-main/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -0,0 +1,15 @@
Timestamp,Date of the event (start date),Date of the event (End date),starting Time,ending Time,Venue,Department,Requirements [Row 1],ending Time.1,Name of the event,Name of the faculty coordinator,Name of the Guest,Email Address,"Any Other Requirements, Please mention",Name of the faculty coordinator.1,Date of the event (End date).1,Status
2024-03-08 14:26:41.487,3/22/0024,,15:00:00,15:00:00,Wozniak Auditorium,RSB,"Memento, Projector, Mike, Refreshments",15:00:00,Biz-analytix guest lecture,,Mr Gokul Palani,badri.toppur@gmail.com,"pointer. cab for pickup and drop-off. Lunch for seven club members, and boquet.",,,
2024-07-05 12:03:24.523,1983-12-07 00:00:00,,15:00:00,15:00:00,Wozniak Auditorium,RIT Placement,"Memento, Projector, Mike, White Board, Refreshments, Lunch, Breakfast, Guest Transport (Transport Form to be submitted)",15:00:00,JP,,JP GANTHI,training@ritchennai.edu.in,,Pandithurai ,,
2024-01-23 00:00:00.000,2024-01-23 00:00:00,,11:00:00,11:00:00,Wozniak Auditorium,RSB,"2. Audio/Video, PA system, Uninterrupted power supply
3.LED Screen
4. Flower decoration
5. Computer lab with 65 systems with webcam and wifi connection.
6.Board Room for two days (stationery required)
7. Guest hospitality(Lunch & Refreshments for 8 people - Tea,Coffee, Juice, Cookies, Special Lunch)
8. Bouquets - 5
9. Transportation for Guest - (would intimate numbers once confirmed)",11:00:00,Mindsprint Campus Placement,,,,,,,
,2024-01-23 00:00:00,,15:00:00,15:00:00,Wozniak Auditorium,RSB,"1. Wozniak auditorium
2. Lunch - 4 (2 guests, Director and Faculty)
3. Memento - 2 guests",15:00:00,"guest lecture from CII, Logistics Center of Excellence, IIT Madras",,,,,,,
2024-01-22 12:33:45.130,2024-01-23 00:00:00,,15:00:00,15:00:00,Steve Jobs 1st Floor,RIT Mech,"Projector, White Board",15:00:00,Awareness session on Start - up and chatGpt,,"Internal faculties (Dr. V.M. Gopinath, Dr.M.Shaju and Dr.M.Dinesh Babu)",pragadish.n@ritchennai.edu.in,,,,
1 Timestamp Date of the event (start date) Date of the event (End date) starting Time ending Time Venue Department Requirements [Row 1] ending Time.1 Name of the event Name of the faculty coordinator Name of the Guest Email Address Any Other Requirements, Please mention Name of the faculty coordinator.1 Date of the event (End date).1 Status
2 2024-03-08 14:26:41.487 3/22/0024 15:00:00 15:00:00 Wozniak Auditorium RSB Memento, Projector, Mike, Refreshments 15:00:00 Biz-analytix guest lecture Mr Gokul Palani badri.toppur@gmail.com pointer. cab for pickup and drop-off. Lunch for seven club members, and boquet.
3 2024-07-05 12:03:24.523 1983-12-07 00:00:00 15:00:00 15:00:00 Wozniak Auditorium RIT Placement Memento, Projector, Mike, White Board, Refreshments, Lunch, Breakfast, Guest Transport (Transport Form to be submitted) 15:00:00 JP JP GANTHI training@ritchennai.edu.in Pandithurai
4 2024-01-23 00:00:00.000 2024-01-23 00:00:00 11:00:00 11:00:00 Wozniak Auditorium RSB 2. Audio/Video, PA system, Uninterrupted power supply 3.LED Screen 4. Flower decoration 5. Computer lab with 65 systems with webcam and wifi connection. 6.Board Room for two days (stationery required) 7. Guest hospitality(Lunch & Refreshments for 8 people - Tea,Coffee, Juice, Cookies, Special Lunch) 8. Bouquets - 5 9. Transportation for Guest - (would intimate numbers once confirmed) 11:00:00 Mindsprint Campus Placement
5 2024-01-23 00:00:00 15:00:00 15:00:00 Wozniak Auditorium RSB 1. Wozniak auditorium 2. Lunch - 4 (2 guests, Director and Faculty) 3. Memento - 2 guests 15:00:00 guest lecture from CII, Logistics Center of Excellence, IIT Madras
6 2024-01-22 12:33:45.130 2024-01-23 00:00:00 15:00:00 15:00:00 Steve Jobs 1st Floor RIT Mech Projector, White Board 15:00:00 Awareness session on Start - up and chatGpt Internal faculties (Dr. V.M. Gopinath, Dr.M.Shaju and Dr.M.Dinesh Babu) pragadish.n@ritchennai.edu.in

View File

@@ -0,0 +1,36 @@
import os
import re
api_import = "import { API_BASE_URL } from '../../lib/config';"
# Need to adjust depth based on file location
# But I can just use a relative path that works for most components
def fix_file(path):
with open(path, 'r') as f:
content = f.read()
# Calculate depth for import
rel_path = os.path.relpath('src/lib/config.ts', os.path.dirname(path))
rel_path = rel_path.replace('.ts', '').replace('\\', '/')
if not rel_path.startswith('.'):
rel_path = './' + rel_path
import_line = f"import {{ API_BASE_URL }} from '{rel_path}';"
# Replace the messy strings
new_content = content.replace("'http://' + window.location.hostname + ':8080", "API_BASE_URL")
new_content = new_content.replace("`http://' + window.location.hostname + ':8080", "`${API_BASE_URL}")
if new_content != content:
# Add import if not present
if 'API_BASE_URL' in new_content and 'import { API_BASE_URL }' not in new_content:
new_content = import_line + "\n" + new_content
with open(path, 'w') as f:
f.write(new_content)
print(f"Fixed {path}")
for root, dirs, files in os.walk('src'):
for file in files:
if file.endswith(('.ts', '.tsx')):
fix_file(os.path.join(root, file))

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RIT EMS - Event Management System</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4579
RIT-EMS-main/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@types/uuid": "^10.0.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"firebase": "^12.15.0",
"framer-motion": "^12.38.0",
"lucide-react": "^1.8.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"tailwind-merge": "^3.5.0",
"uuid": "^14.0.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@tailwindcss/postcss": "^4.2.4",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"postcss": "^8.5.10",
"tailwindcss": "^4.2.4",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.9"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
autoprefixer: {},
},
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,187 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
&>div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 25px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

View File

@@ -0,0 +1,175 @@
import { API_BASE_URL } from './lib/config';
import React, { useState } from 'react';
import { DashboardLayout } from './components/DashboardLayout';
import { InstitutionalChecklist } from './components/dashboard/InstitutionalChecklist';
import { EventProposalForm } from './components/dashboard/EventProposalForm';
import { AllEvents } from './components/dashboard/AllEvents';
import { ApprovalsView } from './components/dashboard/ApprovalsView';
import { UserManagement } from './components/dashboard/UserManagement';
import { ClassManagement } from './components/dashboard/ClassManagement';
import { LoginPage } from './pages/LoginPage';
import { AuthProvider, useAuth } from './context/AuthContext';
import { motion, AnimatePresence } from 'framer-motion';
import { StatusTimeline, type Event } from './components/dashboard/EventStatusTimeline';
import { useEffect } from 'react';
import { InstitutionalCalendar } from './components/dashboard/InstitutionalCalendar';
import { VenueTimeline } from './components/dashboard/VenueTimeline';
import { EventHistory } from './components/dashboard/EventHistory';
import { ClubInstitutionalChecklist } from './components/dashboard/ClubInstitutionalChecklist';
import { AutomationView } from './components/dashboard/AutomationView';
import { Overview } from './components/dashboard/Overview';
import { ExcelImport } from './components/dashboard/ExcelImport';
const AppContent: React.FC = () => {
const { isAuthenticated, user } = useAuth();
const isClubAuthorized = user?.isClubCoordinator || user?.role === 'ADMIN';
const [activeItem, setActiveItem] = useState('dashboard');
const [userEvents, setUserEvents] = useState<Event[]>([]);
const [preFillData, setPreFillData] = useState<any>(null);
const handleIncompleteClick = (data: any) => {
setPreFillData(data);
setActiveItem('propose');
};
const handleCalendarPropose = (date: string) => {
setPreFillData({ startDate: `${date}T09:00` });
setActiveItem('propose');
};
const handleClubEventClick = (data: any) => {
setPreFillData(data);
setActiveItem('propose');
};
const handleEditEvent = (event: any) => {
setPreFillData({
...event,
eventName: event.title,
venue: event.location,
socialProfile: event.guestSocialProfile,
isEditMode: true
});
setActiveItem('propose');
};
const handleSidebarClick = (item: string) => {
if (item === 'propose') setPreFillData(null);
setActiveItem(item);
};
useEffect(() => {
const handleNav = (e: any) => setActiveItem(e.detail);
window.addEventListener('navigate', handleNav);
return () => window.removeEventListener('navigate', handleNav);
}, []);
useEffect(() => {
if (isAuthenticated && activeItem === 'dashboard') {
fetch(API_BASE_URL + '/api/events')
.then(res => {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
return res.json();
})
.then(data => {
if (Array.isArray(data)) {
let filtered = data;
if (user?.role === 'HOD') {
filtered = data.filter((e: Event) => e.department === user?.department);
} else if (user?.role === 'FACULTY') {
filtered = data.filter((e: Event) => e.proposer?.email === user?.email);
}
setUserEvents(filtered);
}
})
.catch(err => console.error("Failed to fetch dashboard events:", err));
}
}, [isAuthenticated, activeItem, user?.email, user?.role, user?.department]);
if (!isAuthenticated) {
return (
<AnimatePresence mode="wait">
<motion.div
key="login"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<LoginPage />
</motion.div>
</AnimatePresence>
);
}
const renderContent = () => {
const isDashboard = (
<Overview
userEvents={userEvents}
onNavigate={handleSidebarClick}
onIncompleteClick={handleIncompleteClick}
onCalendarPropose={handleCalendarPropose}
onClubEventClick={handleClubEventClick}
onEditEvent={handleEditEvent}
/>
);
switch (activeItem) {
case 'user-management':
return user?.role === 'ADMIN' ? <UserManagement /> : isDashboard;
case 'approvals':
return (user?.role === 'HOD' || user?.role === 'PRINCIPAL') ? <ApprovalsView /> : isDashboard;
case 'clubs':
return isClubAuthorized ? <ClubInstitutionalChecklist onEventClick={handleClubEventClick} /> : isDashboard;
case 'events':
return <AllEvents onEditEvent={handleEditEvent} />;
case 'propose':
return <EventProposalForm initialData={preFillData} />;
case 'checklist':
return <InstitutionalChecklist isGlobalView={true} onIncompleteClick={handleIncompleteClick} />;
case 'history':
return <EventHistory />;
case 'classes':
return user?.role === 'ADMIN' ? <ClassManagement /> : isDashboard;
case 'automation':
return (user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? <AutomationView /> : isDashboard;
case 'import-excel':
return <ExcelImport />;
case 'dashboard':
return isDashboard;
default:
return isDashboard;
}
};
return (
<DashboardLayout
activeItem={activeItem}
onItemClick={handleSidebarClick}
>
<AnimatePresence mode="wait">
<motion.div
key={activeItem}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
{renderContent()}
</motion.div>
</AnimatePresence>
</DashboardLayout>
);
};
function App() {
return (
<AuthProvider>
<AppContent />
</AuthProvider>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,214 @@
import { Sidebar } from './Sidebar';
import { Search, Bell, Settings, LogOut, User as UserIcon, Plus, Calendar, ClipboardList, CheckCircle, Users, BookOpen, Zap, Clock, History as HistoryIcon, LayoutGrid, FileSpreadsheet } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { useState, useEffect, useRef } from 'react';
import { API_BASE_URL } from '../lib/config';
import { cn } from '../lib/utils';
import { motion, AnimatePresence } from 'framer-motion';
interface DashboardLayoutProps {
children: React.ReactNode;
activeItem: string;
onItemClick: (id: string) => void;
}
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
children,
activeItem,
onItemClick
}) => {
const { user, logout } = useAuth();
const [searchQuery, setSearchQuery] = useState('');
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [events, setEvents] = useState<any[]>([]);
const searchRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
setIsSearchOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
useEffect(() => {
if (isSearchOpen && events.length === 0) {
fetch(API_BASE_URL + '/api/events')
.then(res => res.json())
.then(data => setEvents(Array.isArray(data) ? data : []))
.catch(err => console.error('Search fetch failed:', err));
}
}, [isSearchOpen]);
const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutGrid, description: 'Main overview and stats' },
{ id: 'approvals', label: 'Approvals', icon: CheckCircle, description: 'Review pending requests', roles: ['HOD', 'PRINCIPAL', 'ADMIN'] },
{ id: 'events', label: 'All Events', icon: ClipboardList, description: 'Browse institution events' },
{ id: 'checklist', label: 'Checklist', icon: Calendar, description: 'Audit and compliance view' },
{ id: 'history', label: 'History', icon: HistoryIcon, description: 'Event ledger and audit trail' },
{ id: 'user-management', label: 'Manage Users', icon: Users, description: 'User roles and access', roles: ['ADMIN'] },
{ id: 'classes', label: 'Classes', icon: BookOpen, description: 'Academic structure', roles: ['ADMIN'] },
{ id: 'import-excel', label: 'Import Excel', icon: FileSpreadsheet, description: 'Bulk upload from response sheets', roles: ['ADMIN', 'PRINCIPAL'] },
{ id: 'automation', label: 'Automation', icon: Zap, description: 'Smart scheduling tools', roles: ['ADMIN', 'PRINCIPAL', 'HOD'] },
].filter(item => !item.roles || item.roles.includes(user?.role || ''));
const filteredNav = searchQuery.length > 1
? navItems.filter(item => item.label.toLowerCase().includes(searchQuery.toLowerCase()))
: [];
const filteredEvents = searchQuery.length > 1
? events.filter(e =>
e.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
e.department?.toLowerCase().includes(searchQuery.toLowerCase())
).slice(0, 5)
: [];
const handleResultClick = (id: string, type: 'nav' | 'event') => {
if (type === 'nav') {
onItemClick(id);
} else {
// For events, we'll navigate to 'events' and we could potentially filter there
onItemClick('events');
}
setSearchQuery('');
setIsSearchOpen(false);
};
return (
<div className="min-h-screen bg-[#FDFDFE]">
<Sidebar
activeItem={activeItem}
onItemClick={onItemClick}
/>
<main className="ml-[18rem] min-h-screen transition-all duration-500 pr-8">
{/* Header */}
<header className="sticky top-0 z-40 bg-[#FDFDFE]/80 backdrop-blur-md py-6 flex items-center justify-between gap-8">
<div className="flex-1 max-w-xl relative group" ref={searchRef}>
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-slate-400 group-focus-within:text-brand-indigo transition-colors" />
<input
type="text"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setIsSearchOpen(true);
}}
onFocus={() => setIsSearchOpen(true)}
placeholder="Search events, pages, or tools..."
className="w-full bg-white border border-slate-200/60 rounded-[1.25rem] py-3 pl-12 pr-4 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-brand-indigo/10 focus:border-brand-indigo transition-all shadow-sm"
/>
<AnimatePresence>
{isSearchOpen && (filteredNav.length > 0 || filteredEvents.length > 0) && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="absolute top-full left-0 right-0 mt-3 bg-white/95 backdrop-blur-xl border border-slate-100 rounded-[2rem] shadow-[0_20px_50px_rgba(0,0,0,0.1)] overflow-hidden z-50 p-2"
>
{filteredNav.length > 0 && (
<div className="mb-2">
<p className="px-4 py-2 text-[10px] font-black uppercase tracking-widest text-slate-400">Navigation</p>
{filteredNav.map(item => (
<button
key={item.id}
onClick={() => handleResultClick(item.id, 'nav')}
className="w-full flex items-center gap-4 p-3 hover:bg-slate-50 rounded-2xl transition-all text-left group"
>
<div className="w-10 h-10 rounded-xl bg-brand-glow flex items-center justify-center text-brand-indigo group-hover:bg-brand-indigo group-hover:text-white transition-all">
<item.icon className="w-5 h-5" />
</div>
<div>
<p className="text-sm font-black text-brand-navy">{item.label}</p>
<p className="text-[10px] font-medium text-slate-400">{item.description}</p>
</div>
</button>
))}
</div>
)}
{filteredEvents.length > 0 && (
<div>
<p className="px-4 py-2 text-[10px] font-black uppercase tracking-widest text-slate-400">Recent Events</p>
{filteredEvents.map(event => (
<button
key={event.id}
onClick={() => handleResultClick(event.id, 'event')}
className="w-full flex items-center gap-4 p-3 hover:bg-slate-50 rounded-2xl transition-all text-left group"
>
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
event.status === 'APPROVED' ? "bg-emerald-50 text-emerald-600" :
event.status === 'PENDING' || event.status === 'REQUESTED' ? "bg-amber-50 text-amber-600" : "bg-slate-50 text-slate-400"
)}>
<ClipboardList className="w-5 h-5" />
</div>
<div className="flex-1">
<p className="text-sm font-black text-brand-navy truncate max-w-[250px]">{event.title}</p>
<div className="flex items-center gap-2">
<span className="text-[9px] font-bold uppercase tracking-wider text-slate-400">{event.department}</span>
<span className={cn(
"text-[8px] font-black uppercase px-1.5 py-0.5 rounded",
event.status === 'APPROVED' ? "bg-emerald-50 text-emerald-600" :
event.status === 'PENDING' || event.status === 'REQUESTED' ? "bg-amber-50 text-amber-600" : "bg-slate-100 text-slate-400"
)}>{event.status}</span>
</div>
</div>
<Clock className="w-3.5 h-3.5 text-slate-300" />
</button>
))}
</div>
)}
</motion.div>
)}
</AnimatePresence>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => onItemClick('propose')}
className="flex items-center gap-2 px-5 py-2.5 bg-brand-indigo text-white rounded-[1.25rem] text-sm font-black transition-all hover:shadow-lg hover:shadow-brand-indigo/20 hover:scale-[1.02]"
>
<Plus className="w-4 h-4" />
New Event
</button>
<div className="flex items-center gap-1 bg-white p-1 rounded-2xl border border-slate-100 shadow-sm">
<button className="p-2.5 rounded-xl hover:bg-slate-50 transition-all text-slate-500 relative">
<Bell className="w-5 h-5" />
<span className="absolute top-2.5 right-2.5 w-2 h-2 bg-red-500 rounded-full border-2 border-white" />
</button>
</div>
<div className="flex items-center gap-3 bg-white pl-1 pr-4 py-1 rounded-[1.5rem] shadow-sm border border-slate-100">
<div className="w-10 h-10 rounded-full bg-brand-glow flex items-center justify-center overflow-hidden border border-brand-indigo/10">
<UserIcon className="w-5 h-5 text-brand-indigo" />
</div>
<div className="flex flex-col">
<span className="text-xs font-black text-brand-navy leading-none mb-0.5">
{user?.fullName}
</span>
<span className="text-[10px] font-bold text-brand-indigo/60 uppercase tracking-wider">
{user?.role}
</span>
</div>
</div>
<button
onClick={logout}
className="p-3 rounded-2xl bg-white border border-slate-100 text-slate-400 hover:text-red-500 hover:border-red-100 hover:bg-red-50 transition-all shadow-sm"
title="Sign Out"
>
<LogOut className="w-5 h-5" />
</button>
</div>
</header>
<div className="pb-10">
{children}
</div>
</main>
</div>
);
};

View File

@@ -0,0 +1,104 @@
import React, { useState } from 'react';
import {
LayoutGrid,
ClipboardList,
CheckSquare,
History as HistoryIcon,
Zap,
BookOpen,
PlusCircle,
ChevronRight,
ShieldCheck,
Users,
Calendar,
HelpCircle,
ExternalLink,
CheckCircle
} from 'lucide-react';
import { cn } from '../lib/utils';
import { motion } from 'framer-motion';
import ritLogo from '../assets/images/college-logo.png';
import { useAuth } from '../context/AuthContext';
interface SidebarProps {
activeItem: string;
onItemClick: (id: string) => void;
}
export const Sidebar: React.FC<SidebarProps> = ({ activeItem, onItemClick }) => {
const { user } = useAuth();
const isClubAuthorized = user?.isClubCoordinator || user?.role === 'ADMIN';
const menuItems = [
{ icon: LayoutGrid, label: 'Dashboard', id: 'dashboard' },
...((user?.role === 'HOD' || user?.role === 'PRINCIPAL') ? [{ icon: CheckCircle, label: 'Approvals', id: 'approvals' }] : []),
...(isClubAuthorized ? [{ icon: ShieldCheck, label: 'Clubs', id: 'clubs' }] : []),
{ icon: ClipboardList, label: 'All Events', id: 'events' },
{ icon: CheckSquare, label: 'Checklist', id: 'checklist' },
{ icon: HistoryIcon, label: 'History', id: 'history' },
...((user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? [{ icon: Zap, label: 'Automation', id: 'automation' }] : []),
...(user?.role === 'ADMIN' ? [{ icon: BookOpen, label: 'Classes', id: 'classes' }] : []),
...(user?.role === 'ADMIN' ? [{ icon: Users, label: 'Manage Users', id: 'user-management' }] : [])
];
return (
<aside className="floating-sidebar">
{/* Logo */}
<div className="flex items-center gap-3 mb-8 px-2">
<img src={ritLogo} alt="College Logo" className="h-14 w-auto object-contain" />
</div>
{/* Navigation */}
<nav className="flex-1 space-y-1.5 overflow-y-auto custom-scrollbar pr-1">
<p className="px-4 text-[10px] font-black uppercase tracking-widest text-slate-400 mb-4">Main Menu</p>
{menuItems.map((item) => (
<motion.div
key={item.id}
whileHover={{ x: 4 }}
whileTap={{ scale: 0.98 }}
onClick={() => onItemClick(item.id)}
className={cn(
"sidebar-item group relative",
activeItem === item.id && "sidebar-item-active"
)}
>
<div className={cn(
"p-2 rounded-xl transition-all",
activeItem === item.id ? "bg-white shadow-sm" : "bg-transparent group-hover:bg-white/50"
)}>
<item.icon className="w-4.5 h-4.5" strokeWidth={2.5} />
</div>
<span className="flex-1 text-sm">{item.label}</span>
{activeItem === item.id && (
<div className="w-1.5 h-1.5 rounded-full bg-brand-indigo" />
)}
</motion.div>
))}
</nav>
{/* Footer Build Info */}
<div className="mt-auto px-4 pt-6 border-t border-slate-50">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-1.5 px-3 py-1 bg-brand-glow text-brand-indigo rounded-full border border-brand-indigo/10 backdrop-blur-sm group/beta hover:bg-brand-indigo hover:text-white transition-all duration-300 cursor-default">
<div className="w-1 h-1 bg-brand-indigo rounded-full group-hover/beta:bg-white animate-pulse" />
<span className="text-[9px] font-black uppercase tracking-[0.2em]">Beta</span>
</div>
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-slate-300">
v0.1.5
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-emerald-500/70">
Live Systems
</span>
</div>
</div>
</div>
</aside>
);
};

View File

@@ -0,0 +1,234 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import {
Calendar,
MapPin,
Building2,
Clock,
ChevronRight,
Filter,
Search,
MoreVertical,
FileSpreadsheet
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { useAuth } from '../../context/AuthContext';
import { StatusTimeline, type Event } from './EventStatusTimeline';
import { Pagination } from './Pagination';
interface AllEventsProps {
onEditEvent?: (event: any) => void;
}
export const AllEvents: React.FC<AllEventsProps> = ({ onEditEvent }) => {
const { user } = useAuth();
const [events, setEvents] = useState<Event[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
fetchEvents();
}, []);
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
setEvents(data);
}
} catch (error) {
console.error('Failed to fetch events:', error);
} finally {
setIsLoading(false);
}
};
const getStatusColor = (status: Event['status']) => {
switch (status) {
case 'REQUESTED': return 'bg-brand-glow text-brand-indigo border-brand-indigo/20';
case 'PENDING_PR': return 'bg-amber-50 text-amber-600 border-amber-100';
case 'APPROVED': return 'bg-emerald-50 text-emerald-600 border-emerald-100';
case 'COMPLETED': return 'bg-slate-100 text-slate-600 border-slate-200';
case 'CANCELLED': return 'bg-red-50 text-red-600 border-red-100';
default: return 'bg-slate-50 text-slate-500 border-slate-100';
}
};
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const filteredEvents = events
.filter(event =>
event.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
event.department.toLowerCase().includes(searchTerm.toLowerCase())
)
.sort((a, b) => {
const dateA = new Date(a.updatedAt || a.createdAt || 0).getTime();
const dateB = new Date(b.updatedAt || b.createdAt || 0).getTime();
return dateB - dateA;
});
const totalPages = Math.ceil(filteredEvents.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const currentEvents = filteredEvents.slice(startIndex, startIndex + itemsPerPage);
useEffect(() => {
setCurrentPage(1);
}, [searchTerm]);
return (
<div className="space-y-8">
{/* Header & Controls */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">All Events</h2>
<p className="text-text-muted font-medium">Overview of all institutional activities and their status.</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="text"
placeholder="Filter events..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white border border-slate-200 rounded-xl py-2 pl-10 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-brand-indigo/10 focus:border-brand-indigo transition-all premium-shadow-sm"
/>
</div>
{(user?.role === 'ADMIN' || user?.role === 'PRINCIPAL') && (
<button
onClick={() => window.dispatchEvent(new CustomEvent('navigate', { detail: 'import-excel' }))}
className="flex items-center gap-2 px-4 py-2 bg-brand-glow text-brand-indigo rounded-xl hover:bg-brand-indigo hover:text-white transition-all premium-shadow-sm border border-brand-indigo/10 text-sm font-black uppercase tracking-widest"
>
<FileSpreadsheet className="w-4 h-4" />
Import
</button>
)}
<button className="p-2 bg-white border border-slate-200 rounded-xl hover:bg-slate-50 transition-all premium-shadow-sm">
<Filter className="w-5 h-5 text-text-secondary" />
</button>
</div>
</div>
{/* Events List */}
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden flex flex-col">
{isLoading ? (
<div className="p-20 text-center text-text-muted italic">Loading events...</div>
) : filteredEvents.length === 0 ? (
<div className="p-20 text-center">
<div className="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4">
<Calendar className="w-8 h-8 text-slate-300" />
</div>
<p className="text-text-muted font-medium italic">No events found matching your search.</p>
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-50">
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Event Details</th>
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Date & Venue</th>
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Department</th>
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Status</th>
<th className="px-8 py-6 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{currentEvents.map((event, idx) => (
<motion.tr
key={event.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
className="group hover:bg-slate-50/50 transition-colors"
>
<td className="px-8 py-6">
<div className="flex flex-col">
<span className="font-bold text-text-dark text-sm group-hover:text-brand-indigo transition-colors">{event.title}</span>
<span className="text-[10px] font-black uppercase tracking-widest text-text-muted mt-1">{event.type} {event.institution}</span>
</div>
</td>
<td className="px-8 py-6">
<div className="space-y-1.5">
<div className="flex items-center gap-2 text-xs font-semibold text-text-secondary">
<Clock className="w-3.5 h-3.5 text-brand-indigo opacity-60" />
{parseDate(event.startDate) ? parseDate(event.startDate)!.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }) : 'N/A'}
</div>
<div className="flex items-center gap-2 text-[10px] font-bold text-text-muted uppercase tracking-wider">
<MapPin className="w-3.5 h-3.5 text-brand-indigo opacity-60" />
{event.location}
</div>
</div>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-lg bg-brand-glow flex items-center justify-center text-[10px] font-black text-brand-indigo">
{event.department.substring(0, 2)}
</div>
<span className="text-xs font-bold text-text-secondary">{event.department}</span>
</div>
</td>
<td className="px-8 py-6">
{user?.role === 'FACULTY' && event.proposer?.email === user?.email ? (
<StatusTimeline event={event} compact />
) : (
<span className={cn(
"inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest border",
getStatusColor(event.status)
)}>
{event.status === 'PENDING_PR' ? 'PENDING PRINCIPAL' : event.status}
</span>
)}
</td>
<td className="px-8 py-6 text-right">
<div className="flex items-center justify-end gap-2">
{(user?.role === 'ADMIN' || (event.status === 'REQUESTED' && event.proposer?.email === user?.email)) && onEditEvent && (
<button
onClick={() => onEditEvent(event)}
className="px-4 py-2 bg-slate-50 text-brand-indigo hover:bg-brand-indigo hover:text-white rounded-xl text-[9px] font-black uppercase tracking-widest transition-all border border-brand-indigo/10"
>
Edit
</button>
)}
<button className="p-2 rounded-lg hover:bg-white hover:premium-shadow transition-all text-slate-300 hover:text-brand-indigo">
<MoreVertical className="w-5 h-5" />
</button>
</div>
</td>
</motion.tr>
))}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredEvents.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 20, 50]}
/>
</>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,417 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
CheckCircle,
XCircle,
Clock,
User,
Building2,
ChevronRight,
AlertCircle,
ShieldCheck,
MapPin,
Wallet,
Users,
Layers,
Ticket,
AlertTriangle,
FileText,
Heart,
Calendar
} from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { cn } from '../../lib/utils';
import { Pagination } from './Pagination';
interface Event {
id: number;
title: string;
startDate: string;
endDate: string;
type: string;
institution: string;
department: string;
academicYears: string[];
status: string;
location: string;
proposer?: {
fullName: string;
department: string;
};
budget: number;
hasRegistrationFee: boolean;
registrationFee: number;
category: string;
targetedSections: string[];
conflictMessage?: string;
description?: string;
sponsors?: string[];
rejectionReason?: string;
}
export const ApprovalsView: React.FC = () => {
const { user } = useAuth();
const [events, setEvents] = useState<Event[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [processingId, setProcessingId] = useState<number | null>(null);
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
// Filter based on role
if (user?.role === 'HOD') {
setEvents(data.filter((e: Event) => e.status === 'REQUESTED' && e.department === user.department));
} else if (user?.role === 'PRINCIPAL') {
setEvents(data.filter((e: Event) => e.status === 'PENDING_PR'));
}
}
} catch (error) {
console.error('Failed to fetch events:', error);
} finally {
setIsLoading(false);
}
};
const handleAction = async (id: number, action: 'approve' | 'reject') => {
let reason = '';
if (action === 'reject') {
reason = window.prompt('Please enter a rejection reason:') || '';
if (!reason) return;
}
setProcessingId(id);
try {
const response = await fetch(`${API_BASE_URL}/api/events/${id}/${action}?userId=${user?.id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: action === 'reject' ? JSON.stringify({ reason }) : undefined
});
if (response.ok) {
setEvents(events.filter(e => e.id !== id));
setSelectedEvent(null);
} else {
const err = await response.json();
alert(err.message || 'Operation failed');
}
} catch (error) {
console.error(`Failed to ${action} event:`, error);
} finally {
setProcessingId(null);
}
};
const sortedEvents = useMemo(() => {
return [...events].sort((a: any, b: any) => {
const dateA = new Date(a.updatedAt || a.createdAt || 0).getTime();
const dateB = new Date(b.updatedAt || b.createdAt || 0).getTime();
return dateB - dateA;
});
}, [events]);
const currentEvents = sortedEvents.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-8">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">Pending Approvals</h2>
<p className="text-text-muted font-medium">Review and take action on event proposals from your {user?.role === 'HOD' ? 'department' : 'institution'}.</p>
</div>
<div className="space-y-4">
{isLoading ? (
<div className="flex items-center justify-center p-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-indigo"></div>
</div>
) : events.length === 0 ? (
<div className="bg-white rounded-[2.5rem] p-20 text-center border border-slate-100 premium-shadow">
<CheckCircle className="w-16 h-16 text-emerald-500 mx-auto mb-6 opacity-20" />
<h3 className="text-xl font-black text-text-dark mb-2">Queue is Empty</h3>
<p className="text-text-muted italic">All proposals have been processed. Great work!</p>
</div>
) : (
<AnimatePresence mode="popLayout">
{currentEvents.map((event) => (
<motion.div
key={event.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
onClick={() => setSelectedEvent(event)}
className={cn(
"bg-white rounded-[2rem] p-6 border transition-all flex flex-col md:flex-row items-center justify-between gap-6 cursor-pointer group",
event.conflictMessage ? "border-red-200 bg-red-50/30" : "border-slate-100 hover:premium-shadow"
)}
>
<div className="flex items-center gap-6 w-full md:w-auto">
<div className={cn(
"w-16 h-16 rounded-2xl flex items-center justify-center font-black text-xl shadow-lg",
event.conflictMessage ? "bg-red-500 text-white" : "bg-brand-glow text-brand-indigo"
)}>
{event.title[0]}
</div>
<div>
<div className="flex items-center gap-2 mb-1">
{event.category === 'CLUB' && (
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-600 rounded-md text-[8px] font-black uppercase tracking-widest border border-emerald-100">
Institutional Club
</span>
)}
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">
{event.type}
</span>
</div>
<h3 className="text-xl font-black text-text-dark leading-tight mb-2 group-hover:text-brand-indigo transition-colors">{event.title}</h3>
{event.conflictMessage ? (
<div className="flex items-center gap-2 text-red-500 text-[10px] font-black uppercase tracking-widest mt-1">
<AlertTriangle className="w-3.5 h-3.5" />
{event.conflictMessage}
</div>
) : (
<div className="flex items-center justify-center md:justify-start gap-4 text-xs font-bold text-text-muted">
<div className="flex items-center gap-1.5">
<User className="w-3.5 h-3.5" />
{event.proposer?.fullName || 'Faculty Member'}
</div>
<div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
{new Date(event.startDate).toLocaleDateString()}
</div>
</div>
)}
</div>
</div>
<div className="flex items-center gap-3 w-full md:w-auto" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleAction(event.id, 'reject')}
disabled={processingId === event.id}
className="flex-1 md:flex-none flex items-center justify-center gap-2 px-6 py-3 rounded-xl bg-red-50 text-red-600 hover:bg-red-100 transition-all font-black text-[10px] uppercase tracking-widest border border-red-100/50"
>
<XCircle className="w-4 h-4" />
Reject
</button>
<button
onClick={() => handleAction(event.id, 'approve')}
disabled={processingId === event.id || !!event.conflictMessage}
className={cn(
"flex-1 md:flex-none flex items-center justify-center gap-2 px-8 py-3 rounded-xl transition-all font-black text-[10px] uppercase tracking-widest premium-shadow",
event.conflictMessage
? "bg-slate-200 text-slate-400 cursor-not-allowed"
: "bg-brand-navy text-white hover:scale-105"
)}
>
{processingId === event.id ? 'Processing...' : (
<>
<CheckCircle className="w-4 h-4" />
Approve
</>
)}
</button>
</div>
</motion.div>
))}
</AnimatePresence>
)}
</div>
{events.length > 0 && (
<Pagination
currentPage={currentPage}
totalItems={events.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 15, 20]}
/>
)}
{/* Detail Modal */}
<AnimatePresence>
{selectedEvent && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedEvent(null)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-2xl bg-white rounded-[2.5rem] premium-shadow overflow-hidden max-h-[90vh] overflow-y-auto"
>
<div className={cn(
"p-8 text-white flex justify-between items-start",
selectedEvent.conflictMessage ? "bg-red-600" : "bg-brand-navy"
)}>
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">Proposal Details</span>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">{selectedEvent.institution}</span>
{selectedEvent.category === 'CLUB' && (
<>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-emerald-400">Institutional Club Event</span>
</>
)}
</div>
<h3 className="text-2xl font-black tracking-tight">{selectedEvent.title}</h3>
</div>
<button
onClick={() => setSelectedEvent(null)}
className="p-2 hover:bg-white/10 rounded-xl transition-all"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<div className="p-8 space-y-8">
{/* Description Section */}
{selectedEvent.description && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<FileText className="w-3.5 h-3.5" />
Event Description
</div>
<p className="text-sm font-medium text-text-dark leading-relaxed bg-slate-50 p-4 rounded-2xl border border-slate-100">
{selectedEvent.description}
</p>
</div>
)}
<div className="grid grid-cols-2 gap-8">
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Calendar className="w-3.5 h-3.5" />
Schedule
</div>
<p className="text-sm font-bold text-text-dark">
{new Date(selectedEvent.startDate).toLocaleString()}
</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<MapPin className="w-3.5 h-3.5" />
Venue
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.location}</p>
</div>
</div>
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<User className="w-3.5 h-3.5" />
Proposed By
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.proposer?.fullName || 'Faculty'}</p>
<p className="text-[10px] font-black text-brand-indigo uppercase">{selectedEvent.department}</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Users className="w-3.5 h-3.5" />
Target Audience
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.academicYears.join(', ')} Batches</p>
</div>
</div>
</div>
{/* Sponsors Section */}
{selectedEvent.sponsors && selectedEvent.sponsors.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Heart className="w-3.5 h-3.5 text-brand-indigo" />
Event Sponsors
</div>
<div className="flex flex-wrap gap-2">
{selectedEvent.sponsors.map((sponsor, idx) => (
<span key={idx} className="px-3 py-1 bg-brand-glow text-brand-indigo rounded-lg text-[10px] font-black uppercase tracking-widest border border-brand-indigo/10">
{sponsor}
</span>
))}
</div>
</div>
)}
<div className="grid grid-cols-2 gap-8 pt-4 border-t border-slate-50">
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Wallet className="w-3.5 h-3.5" />
Estimated Budget
</div>
<p className="text-lg font-black text-text-dark">{selectedEvent.budget?.toLocaleString() || '0'}</p>
</div>
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Ticket className="w-3.5 h-3.5" />
Registration
</div>
<p className="text-lg font-black text-text-dark">
{selectedEvent.hasRegistrationFee ? `${selectedEvent.registrationFee}` : 'FREE'}
</p>
</div>
</div>
{selectedEvent.conflictMessage && (
<div className="p-4 bg-red-50 border border-red-100 rounded-2xl flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-500" />
<div>
<p className="text-[10px] font-black uppercase text-red-500">Critical Conflict Detected</p>
<p className="text-xs font-bold text-red-700">{selectedEvent.conflictMessage}</p>
</div>
</div>
)}
<div className="flex items-center gap-4 pt-4">
<button
onClick={() => handleAction(selectedEvent.id, 'reject')}
disabled={processingId === selectedEvent.id}
className="flex-1 bg-red-50 text-red-600 py-4 rounded-2xl font-black text-[11px] uppercase tracking-widest hover:bg-red-100 transition-all"
>
Reject Proposal
</button>
<button
onClick={() => handleAction(selectedEvent.id, 'approve')}
disabled={processingId === selectedEvent.id || !!selectedEvent.conflictMessage}
className={cn(
"flex-1 py-4 rounded-2xl font-black text-[11px] uppercase tracking-widest transition-all premium-shadow",
selectedEvent.conflictMessage ? "bg-slate-200 text-slate-400" : "bg-brand-navy text-white hover:scale-[1.02]"
)}
>
{processingId === selectedEvent.id ? 'Processing...' : 'Approve Event'}
</button>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -0,0 +1,787 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import {
Zap,
ArrowRightLeft,
ChevronLeft,
ChevronRight,
ShieldAlert,
Play,
Building2,
Users,
Layers,
Calendar,
ChevronDown,
Loader2,
CheckCircle2,
X,
AlertTriangle,
Clock,
ChevronUp
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '../../lib/utils';
import {
format,
addMonths,
subMonths,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
eachDayOfInterval,
isSameMonth,
isSameDay
} from 'date-fns';
const DEPARTMENTS = [
"AI&DS", "AI&ML", "CSE", "CCE", "CSBS", "ECE",
"MECH", "EE(VLSI)", "BIOTECH", "Placement Department",
"H&S Dept", "Club", "Centre"
];
const BATCHES = ['1st Year', '2nd Year', '3rd Year', '4th Year'];
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
interface Event {
id: number;
title: string;
startDate: string;
endDate: string;
department: string;
academicYears: string[];
institution?: string;
type?: string;
category?: string;
location?: string;
proposer?: any;
}
const MiniCalendar: React.FC<{
title: string;
subtitle: string;
date: Date;
onDateChange: (d: Date) => void;
events: Event[];
accentColor: string;
onEventClick?: (e: Event) => void;
}> = ({ title, subtitle, date, onDateChange, events, accentColor, onEventClick }) => {
const days = eachDayOfInterval({
start: startOfWeek(startOfMonth(date)),
end: endOfWeek(endOfMonth(date))
});
return (
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden flex-1">
<div className="p-6 border-b border-slate-50 flex items-center justify-between">
<div>
<h4 className="text-sm font-black text-text-dark tracking-tight">{title}</h4>
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted mt-0.5">{subtitle}</p>
</div>
<div className="flex gap-1">
<button onClick={() => onDateChange(subMonths(date, 1))} className="p-1.5 hover:bg-slate-50 rounded-lg transition-all"><ChevronLeft className="w-4 h-4 text-text-muted" /></button>
<button onClick={() => onDateChange(addMonths(date, 1))} className="p-1.5 hover:bg-slate-50 rounded-lg transition-all"><ChevronRight className="w-4 h-4 text-text-muted" /></button>
</div>
</div>
<div className="p-6">
<div className="text-center mb-4">
<span className="text-lg font-black text-text-dark">{format(date, 'MMMM yyyy')}</span>
</div>
<div className="grid grid-cols-7 gap-px bg-slate-50 border border-slate-50 rounded-2xl overflow-hidden">
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => (
<div key={`${d}-${i}`} className="bg-white py-2 text-center text-[8px] font-black text-text-muted">{d}</div>
))}
{days.map(day => {
const isCurrentMonth = isSameMonth(day, date);
const dayStr = format(day, 'yyyy-MM-dd');
const dayEvents = events.filter(e => {
const d = parseDate(e.startDate);
return d && format(d, 'yyyy-MM-dd') === dayStr;
});
return (
<div key={day.toString()} className={cn(
"min-h-[8rem] bg-white p-2 border-t border-slate-50 relative",
!isCurrentMonth && "opacity-20"
)}>
<span className="text-xs font-black text-text-muted mb-2 block">{format(day, 'd')}</span>
<div className="space-y-1.5 overflow-y-auto max-h-[6rem] custom-scrollbar">
{dayEvents.map(e => (
<div
key={e.id}
onClick={() => onEventClick?.(e)}
className="bg-brand-glow border border-brand-indigo/10 rounded-lg p-2 transition-all hover:scale-[1.02] cursor-pointer"
>
<h5 className="text-[9px] font-black text-brand-indigo leading-tight truncate">{e.title}</h5>
<p className="text-[7px] font-bold text-brand-indigo/60 uppercase tracking-wider">{e.department}</p>
</div>
))}
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
export const AutomationView: React.FC = () => {
const [sourceDate, setSourceDate] = useState(new Date());
const [targetDate, setTargetDate] = useState(addMonths(new Date(), 6));
const [events, setEvents] = useState<Event[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isExecuting, setIsExecuting] = useState(false);
const [status, setStatus] = useState<'idle' | 'success'>('idle');
const [sourceConfig, setSourceConfig] = useState({
institution: 'RIT',
department: 'AI&DS',
batch: '1st Year'
});
const [targetConfig, setTargetConfig] = useState({
institution: 'RIT',
department: 'AI&DS',
batch: '1st Year'
});
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
setEvents(data);
}
} catch (error) {
console.error('Failed to fetch events:', error);
} finally {
setIsLoading(false);
}
}
const filteredSourceEvents = events.filter(e =>
e.department?.trim() === sourceConfig.department.trim() &&
(e.academicYears || []).map((y: string) => y.trim()).includes(sourceConfig.batch.trim())
);
const filteredTargetEvents = events.filter(e =>
e.department?.trim() === targetConfig.department.trim() &&
(e.academicYears || []).map((y: string) => y.trim()).includes(targetConfig.batch.trim())
);
const [proposedChanges, setProposedChanges] = useState<any[]>([]);
const [showReviewModal, setShowReviewModal] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<any>(null);
const prepareSwap = () => {
const changes = filteredSourceEvents.map(event => {
const originalDate = parseDate(event.startDate);
if (!originalDate) return { ...event, resolution: 'skip' };
const isOddSem = originalDate.getMonth() >= 6;
const targetBaseDate = isOddSem ? subMonths(originalDate, 6) : addMonths(originalDate, 6);
// Preserving time
const targetDateStr = format(targetBaseDate, 'yyyy-MM-dd');
const timeStr = format(originalDate, 'HH:mm:ss');
const finalDateTimeStr = `${targetDateStr}T${timeStr}`;
const dayOfWeek = targetBaseDate.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
// Conflict Detection (Any event on target date)
const targetConflicts = filteredTargetEvents.filter(te =>
format(parseDate(te.startDate)!, 'yyyy-MM-dd') === targetDateStr
);
const hasTimeConflict = targetConflicts.some(tc => {
const tcDate = parseDate(tc.startDate);
return tcDate && format(tcDate, 'HH:mm') === format(originalDate, 'HH:mm');
});
const isAlreadySwapped = targetConflicts.some(tc =>
tc.title.trim().toLowerCase() === event.title.trim().toLowerCase()
);
return {
...event,
originalDate: event.startDate,
targetDate: finalDateTimeStr, // Now includes time
targetDepartment: targetConfig.department,
targetBatch: targetConfig.batch,
isWeekend,
isAlreadySwapped,
targetConflicts,
hasTimeConflict,
resolution: isAlreadySwapped ? 'skip' : (targetConflicts.length > 0 ? 'review' : 'create'),
adjustment: isWeekend ? 'monday' : 'none'
};
});
setProposedChanges(changes);
setShowReviewModal(true);
};
const handleExecuteSwap = async () => {
setIsExecuting(true);
const finalizedChanges = proposedChanges
.filter(c => c.resolution !== 'skip')
.map(change => {
let finalDate = new Date(change.targetDate);
// Handle Weekend Adjustments
if (change.resolution !== 'reschedule' && change.isWeekend) {
if (change.adjustment === 'friday') {
const day = finalDate.getDay();
const diff = day === 6 ? 1 : 2;
finalDate.setDate(finalDate.getDate() - diff);
} else if (change.adjustment === 'monday') {
const day = finalDate.getDay();
const diff = day === 6 ? 2 : 1;
finalDate.setDate(finalDate.getDate() + diff);
}
}
return {
...change,
finalDate: format(finalDate, "yyyy-MM-dd'T'HH:mm:ss")
};
});
console.log('Finalized Swap Payload:', finalizedChanges);
try {
const response = await fetch(API_BASE_URL + '/api/events/batch-create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(finalizedChanges.map(c => ({
...c,
targetDepartment: c.targetDepartment,
targetBatch: c.targetBatch,
startDate: c.finalDate
})))
});
if (response.ok) {
setStatus('success');
fetchEvents();
} else {
const errData = await response.json();
alert('Failed to execute automation swap: ' + (errData.message || 'Unknown error'));
}
} catch (error) {
console.error('Automation swap failed:', error);
alert('Network error during automation swap');
} finally {
setIsExecuting(false);
setShowReviewModal(false);
setTimeout(() => setStatus('idle'), 3000);
}
};
return (
<div className="space-y-8 pb-20">
<div className="flex justify-between items-start">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">Event Automation</h2>
<p className="text-text-muted font-medium">Configure and manage automated department event workflows.</p>
</div>
<div className="w-12 h-12 bg-amber-50 rounded-2xl flex items-center justify-center text-amber-500 premium-shadow">
<Zap className="w-6 h-6 fill-current" />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="bg-white rounded-[2.5rem] p-8 border border-slate-100 premium-shadow relative overflow-hidden">
<div className="flex items-center gap-4 mb-8">
<div className="w-10 h-10 bg-slate-50 rounded-xl flex items-center justify-center text-text-muted">
<Building2 className="w-5 h-5" />
</div>
<div>
<h3 className="font-black text-text-dark">Source Selection</h3>
<p className="text-[9px] font-black uppercase tracking-widest text-text-muted">Select branch to clone events from</p>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Institution</label>
<div className="bg-slate-50 rounded-xl p-3 flex justify-between items-center cursor-pointer">
<span className="text-xs font-bold text-text-dark">RIT</span>
<ChevronDown className="w-3 h-3 text-text-muted" />
</div>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Department</label>
<select
value={sourceConfig.department}
onChange={(e) => setSourceConfig({...sourceConfig, department: e.target.value})}
className="w-full bg-slate-50 border-none rounded-xl p-3 text-xs font-bold text-text-dark outline-none appearance-none"
>
{DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
</select>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Batch</label>
<select
value={sourceConfig.batch}
onChange={(e) => setSourceConfig({...sourceConfig, batch: e.target.value})}
className="w-full bg-slate-50 border-none rounded-xl p-3 text-xs font-bold text-text-dark outline-none appearance-none"
>
{BATCHES.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
</div>
</div>
<div className="bg-white rounded-[2.5rem] p-8 border border-slate-100 premium-shadow relative overflow-hidden">
<div className="flex items-center gap-4 mb-8">
<div className="w-10 h-10 bg-brand-indigo rounded-xl flex items-center justify-center text-white">
<ArrowRightLeft className="w-5 h-5" />
</div>
<div>
<h3 className="font-black text-text-dark">Target Workspace</h3>
<p className="text-[9px] font-black uppercase tracking-widest text-brand-indigo">Automation output destination</p>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Target Inst</label>
<div className="bg-slate-50 rounded-xl p-3 flex justify-between items-center cursor-pointer">
<span className="text-xs font-bold text-text-dark">RIT</span>
<ChevronDown className="w-3 h-3 text-text-muted" />
</div>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Target Dept</label>
<select
value={targetConfig.department}
onChange={(e) => setTargetConfig({...targetConfig, department: e.target.value})}
className="w-full bg-slate-50 border-none rounded-xl p-3 text-xs font-bold text-text-dark outline-none appearance-none"
>
{DEPARTMENTS.map(d => <option key={d} value={d}>{d}</option>)}
</select>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Target Batch</label>
<select
value={targetConfig.batch}
onChange={(e) => setTargetConfig({...targetConfig, batch: e.target.value})}
className="w-full bg-slate-50 border-none rounded-xl p-3 text-xs font-bold text-text-dark outline-none appearance-none"
>
{BATCHES.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
</div>
</div>
</div>
<div className="bg-brand-navy rounded-3xl p-6 flex items-center justify-between text-white premium-shadow">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-white/10 flex items-center justify-center text-amber-400">
<ShieldAlert className="w-5 h-5" />
</div>
<div>
<h4 className="text-sm font-black tracking-tight">Automation Safety Protocols Active</h4>
<p className="text-[9px] font-medium text-white/50">Cloning institutional events shifts schedules by +/- 6 months (Semester Swap).</p>
</div>
</div>
<div className="flex items-center gap-6">
<button className="text-[9px] font-black uppercase tracking-widest text-white/40 hover:text-white transition-colors">Clear Workspace</button>
<button
onClick={prepareSwap}
disabled={isExecuting || filteredSourceEvents.length === 0}
className={cn(
"flex items-center gap-3 px-8 py-4 rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] transition-all premium-shadow",
status === 'success' ? "bg-emerald-500 text-white" :
filteredSourceEvents.length === 0 ? "bg-slate-200 text-slate-400 cursor-not-allowed" :
"bg-brand-indigo text-white hover:scale-105"
)}
>
{isExecuting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : status === 'success' ? (
<>
<CheckCircle2 className="w-4 h-4" />
Swap Completed
</>
) : filteredSourceEvents.length === 0 ? (
<>
<ShieldAlert className="w-4 h-4" />
No Events Found to Swap
</>
) : (
<>
<ArrowRightLeft className="w-4 h-4" />
Execute Semester Swap
</>
)}
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<MiniCalendar
title="Primary Schedule (Source)"
subtitle={`${sourceConfig.department}${sourceConfig.batch}`}
date={sourceDate}
onDateChange={setSourceDate}
events={filteredSourceEvents}
accentColor="bg-brand-indigo"
onEventClick={setSelectedEvent}
/>
<MiniCalendar
title="Target Workspace Events"
subtitle={`${targetConfig.department}${targetConfig.batch}`}
date={targetDate}
onDateChange={setTargetDate}
events={filteredTargetEvents}
accentColor="bg-emerald-400"
onEventClick={setSelectedEvent}
/>
</div>
<AnimatePresence>
{selectedEvent && (
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedEvent(null)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-xl bg-white rounded-[2.5rem] premium-shadow overflow-hidden"
>
<div className="p-8 bg-brand-navy text-white flex justify-between items-start">
<div>
<span className="px-3 py-1 bg-white/10 rounded-full text-[8px] font-black uppercase tracking-widest text-brand-indigo border border-white/10">
{selectedEvent.category || 'Academic'} Event
</span>
<h3 className="text-2xl font-black tracking-tight mt-3">{selectedEvent.title}</h3>
</div>
<button onClick={() => setSelectedEvent(null)} className="p-2 hover:bg-white/10 rounded-xl transition-all">
<X className="w-6 h-6" />
</button>
</div>
<div className="p-8 space-y-6">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Start Date</span>
<p className="text-xs font-bold text-text-dark">
{parseDate(selectedEvent.startDate) ? format(parseDate(selectedEvent.startDate)!, 'PPP') : 'N/A'}
</p>
</div>
<div className="space-y-1">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Venue</span>
<p className="text-xs font-bold text-text-dark">{selectedEvent.location || 'Seminar Hall'}</p>
</div>
<div className="space-y-1">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Department</span>
<p className="text-xs font-bold text-text-dark">{selectedEvent.department}</p>
</div>
<div className="space-y-1">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Academic Year</span>
<p className="text-xs font-bold text-text-dark">{selectedEvent.academicYears?.join(', ') || 'N/A'}</p>
</div>
</div>
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-100">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted block mb-2">Proposer Information</span>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-white rounded-lg border border-slate-200 flex items-center justify-center text-[10px] font-black text-brand-indigo">
{selectedEvent.proposer?.fullName?.charAt(0) || 'F'}
</div>
<div>
<p className="text-[10px] font-black text-text-dark leading-none">{selectedEvent.proposer?.fullName || 'Faculty Member'}</p>
<p className="text-[8px] font-bold text-text-muted mt-1">{selectedEvent.proposer?.email || 'faculty@rit.edu'}</p>
</div>
</div>
</div>
</div>
<div className="p-8 border-t border-slate-100 flex justify-end">
<button
onClick={() => setSelectedEvent(null)}
className="px-10 py-4 bg-brand-navy text-white rounded-2xl font-black text-xs uppercase tracking-widest premium-shadow hover:scale-105 active:scale-95 transition-all"
>
Close Insights
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
<AnimatePresence>
{showReviewModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowReviewModal(false)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-4xl bg-white rounded-[2.5rem] premium-shadow overflow-hidden flex flex-col max-h-[80vh]"
>
<div className="p-8 bg-brand-navy text-white flex justify-between items-start">
<div>
<h3 className="text-2xl font-black tracking-tight">Review Automated Changes</h3>
<p className="text-white/60 text-xs font-medium mt-1">Audit and adjust planned semester-swap dates.</p>
</div>
<button onClick={() => setShowReviewModal(false)} className="p-2 hover:bg-white/10 rounded-xl transition-all">
<ChevronDown className="w-6 h-6 rotate-180" />
</button>
</div>
<div className="flex-1 overflow-y-auto p-8 custom-scrollbar">
<div className="space-y-4">
{proposedChanges.map((change, idx) => (
<div key={change.id} className={cn(
"p-6 rounded-[2.5rem] border transition-all space-y-6",
change.resolution === 'skip' ? "bg-slate-50 border-slate-100 opacity-60" :
change.targetConflicts?.length > 0 ? "bg-red-50/30 border-red-100" :
change.isWeekend ? "bg-amber-50 border-amber-100" : "bg-white border-slate-100"
)}>
<div className="flex items-center justify-between gap-8">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-[8px] font-black uppercase tracking-widest text-text-muted">Event</span>
<h4 className="text-sm font-black text-text-dark">{change.title}</h4>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 px-3 py-1.5 bg-slate-100 rounded-lg">
<Calendar className="w-3 h-3 text-text-muted" />
<span className="text-[10px] font-bold text-text-dark">
{parseDate(change.originalDate) ? format(parseDate(change.originalDate)!, 'MMM d') : 'N/A'}
</span>
<ArrowRightLeft className="w-3 h-3 text-brand-indigo" />
<span className="text-[10px] font-black text-brand-indigo">
{parseDate(change.targetDate) ? format(parseDate(change.targetDate)!, 'MMM d, yyyy') : 'N/A'}
</span>
</div>
{change.isAlreadySwapped && (
<span className="flex items-center gap-1.5 px-3 py-1 bg-emerald-100 text-emerald-700 rounded-full text-[8px] font-black uppercase tracking-widest">
<CheckCircle2 className="w-3 h-3" /> Already Swapped
</span>
)}
</div>
</div>
<div className="flex items-center gap-3">
{change.targetConflicts?.length > 0 && change.resolution !== 'skip' && (
<div className="flex items-center gap-2 px-4 py-2 bg-red-100 text-red-700 rounded-2xl border border-red-200">
<AlertTriangle className="w-4 h-4" />
<div className="flex flex-col">
<span className="text-[9px] font-black uppercase">Conflict Detected</span>
<span className="text-[8px] font-bold opacity-70">
{change.targetConflicts.length} event(s) already on this date
{change.hasTimeConflict && " (Time Overlap!)"}
</span>
</div>
</div>
)}
<div className="flex bg-white rounded-2xl border border-slate-100 p-1.5 premium-shadow-sm">
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].resolution = 'create';
setProposedChanges(updated);
}}
className={cn(
"px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all",
change.resolution === 'create' || change.resolution === 'review' ? "bg-brand-indigo text-white shadow-lg shadow-brand-indigo/20" : "text-text-muted hover:text-text-dark"
)}
>
Commit
</button>
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].resolution = 'reschedule';
setProposedChanges(updated);
}}
className={cn(
"px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all",
change.resolution === 'reschedule' ? "bg-amber-500 text-white shadow-lg shadow-amber-500/20" : "text-text-muted hover:text-text-dark"
)}
>
Reschedule
</button>
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].resolution = 'skip';
setProposedChanges(updated);
}}
className={cn(
"px-4 py-2 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all",
change.resolution === 'skip' ? "bg-slate-200 text-slate-600" : "text-text-muted hover:text-text-dark"
)}
>
Skip
</button>
</div>
{/* Weekend Quick-Shift */}
{change.isWeekend && change.resolution !== 'skip' && change.resolution !== 'reschedule' && (
<div className="flex bg-amber-100/50 p-1 rounded-xl border border-amber-200">
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].adjustment = 'friday';
setProposedChanges(updated);
}}
title="Shift to Friday"
className={cn(
"px-3 py-1.5 rounded-lg text-[8px] font-black uppercase tracking-widest transition-all",
change.adjustment === 'friday' ? "bg-amber-600 text-white shadow-sm" : "text-amber-700 hover:bg-amber-100"
)}
>
Fri
</button>
<button
onClick={() => {
const updated = [...proposedChanges];
updated[idx].adjustment = 'monday';
setProposedChanges(updated);
}}
title="Shift to Monday"
className={cn(
"px-3 py-1.5 rounded-lg text-[8px] font-black uppercase tracking-widest transition-all",
change.adjustment === 'monday' ? "bg-amber-600 text-white shadow-sm" : "text-amber-700 hover:bg-amber-100"
)}
>
Mon
</button>
</div>
)}
</div>
</div>
{/* Expanded Conflict / Reschedule Details */}
{(change.resolution === 'reschedule' || (change.targetConflicts?.length > 0 && change.resolution !== 'skip')) && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="pt-6 border-t border-slate-100 space-y-4"
>
{change.targetConflicts?.length > 0 && (
<div className="space-y-2">
<span className="text-[8px] font-black uppercase tracking-widest text-red-500">Conflicts at Destination</span>
{change.targetConflicts.map((conf: any) => (
<div key={conf.id} className="flex items-center justify-between p-3 bg-red-50/50 rounded-xl border border-red-100">
<div className="flex items-center gap-3">
<div className="w-6 h-6 bg-white rounded-lg flex items-center justify-center text-[8px] font-black text-red-500 border border-red-100">
{conf.department.substring(0, 2)}
</div>
<span className="text-[11px] font-bold text-text-dark">{conf.title}</span>
</div>
<div className="flex items-center gap-2 text-[9px] font-black text-red-600/60 uppercase">
<Clock className="w-3 h-3" />
{format(new Date(conf.startDate), 'hh:mm a')}
</div>
</div>
))}
</div>
)}
{change.resolution === 'reschedule' && (
<div className="flex items-end gap-4 bg-amber-50/50 p-6 rounded-[2rem] border border-amber-100">
<div className="flex-1">
<label className="text-[8px] font-black uppercase tracking-widest text-amber-600 mb-2 block">New Target Date</label>
<input
type="date"
value={change.targetDate}
onChange={(e) => {
const updated = [...proposedChanges];
updated[idx].targetDate = e.target.value;
setProposedChanges(updated);
}}
className="w-full bg-white border border-amber-200 rounded-xl p-3 text-xs font-bold text-text-dark focus:outline-none focus:ring-2 focus:ring-amber-500/20"
/>
</div>
<div className="flex flex-col gap-2">
<button
onClick={() => {
const d = new Date(change.targetDate);
d.setDate(d.getDate() + 1);
const updated = [...proposedChanges];
updated[idx].targetDate = format(d, 'yyyy-MM-dd');
setProposedChanges(updated);
}}
className="p-2 bg-white border border-amber-200 rounded-lg hover:bg-amber-100 transition-all"
>
<ChevronUp className="w-4 h-4 text-amber-600" />
</button>
<button
onClick={() => {
const d = new Date(change.targetDate);
d.setDate(d.getDate() - 1);
const updated = [...proposedChanges];
updated[idx].targetDate = format(d, 'yyyy-MM-dd');
setProposedChanges(updated);
}}
className="p-2 bg-white border border-amber-200 rounded-lg hover:bg-amber-100 transition-all"
>
<ChevronDown className="w-4 h-4 text-amber-600" />
</button>
</div>
<div className="flex flex-col gap-1 px-4 py-2 bg-white rounded-2xl border border-amber-100">
<span className="text-[7px] font-black uppercase text-amber-400">Shift Result</span>
<span className="text-[10px] font-black text-amber-700">{format(new Date(change.targetDate), 'EEEE')}</span>
</div>
</div>
)}
</motion.div>
)}
</div>
))}
</div>
</div>
<div className="p-8 border-t border-slate-100 bg-slate-50/50 flex justify-end gap-4">
<button
onClick={() => setShowReviewModal(false)}
className="px-8 py-4 text-xs font-black uppercase tracking-widest text-text-muted hover:text-text-dark transition-colors"
>
Cancel
</button>
<button
onClick={handleExecuteSwap}
className="px-10 py-4 bg-brand-navy text-white rounded-2xl font-black text-xs uppercase tracking-widest premium-shadow hover:scale-105 active:scale-95 transition-all"
>
Commit Automation Swap
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -0,0 +1,435 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Plus,
Search,
Trash2,
Layers,
RefreshCw,
AlertTriangle,
Building2,
GraduationCap,
ChevronRight,
X,
CheckCircle2
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { Pagination } from './Pagination';
interface ClassMapping {
id: number;
institution: string;
department: string;
academicYear: string;
sections: string[];
status: string;
}
const ritDepartments = ['AI&DS', 'AI&ML', 'CSE', 'CCE', 'CSBS', 'ECE', 'MECH', 'EE(VLSI)', 'BIOTECH', 'Placement Department', 'H&S Dept', 'Club', 'Centre'];
const rsbDepartments = ['PGDM'];
const years = ['1st Year', '2nd Year', '3rd Year', '4th Year'];
import rsbLogo from '../../assets/images/rsb_logo.png';
import ritLogo from '../../assets/images/college-logo.png';
export const ClassManagement: React.FC = () => {
const [classes, setClasses] = useState<ClassMapping[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [newSection, setNewSection] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [formData, setFormData] = useState({
institution: 'RIT',
department: 'AI&DS',
academicYear: '1st Year',
sections: [] as string[]
});
const currentDepartments = formData.institution === 'RIT' ? ritDepartments : rsbDepartments;
useEffect(() => {
if (formData.institution === 'RSB' && formData.department !== 'PGDM') {
setFormData(prev => ({ ...prev, department: 'PGDM', sections: [] }));
}
}, [formData.institution]);
useEffect(() => {
fetchClasses();
}, []);
const fetchClasses = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/classes');
if (response.ok) {
const data = await response.json();
setClasses(data);
}
} catch (error) {
console.error('Failed to fetch classes:', error);
} finally {
setIsLoading(false);
}
};
const handleAddSection = (e: React.FormEvent) => {
e.preventDefault();
if (newSection && !formData.sections.includes(newSection.toUpperCase())) {
setFormData({
...formData,
sections: [...formData.sections, newSection.toUpperCase()]
});
setNewSection('');
}
};
const removeSection = (section: string) => {
setFormData({
...formData,
sections: formData.sections.filter(s => s !== section)
});
};
const handleSave = async () => {
if (formData.sections.length === 0) {
alert('Please add at least one section');
return;
}
try {
const response = await fetch(API_BASE_URL + '/api/classes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
fetchClasses();
setFormData({ ...formData, sections: [] });
}
} catch (error) {
console.error('Failed to save class mapping:', error);
}
};
const handleDelete = async (id: number) => {
if (window.confirm('Are you sure you want to delete this mapping?')) {
try {
const response = await fetch(`${API_BASE_URL}/api/classes/${id}`, {
method: 'DELETE'
});
if (response.ok) {
setClasses(prev => prev.filter(c => c.id !== id));
}
} catch (error) {
console.error('Delete failed:', error);
}
}
};
const handlePromote = async () => {
const confirmed = window.confirm(
`⚠️ WARNING: ${formData.institution} ACADEMIC YEAR PROMOTION\n\n` +
`This action will shift all academic structures for ${formData.institution} forward:\n` +
'• 1st Year → 2nd Year\n' +
'• 2nd Year → 3rd Year\n' +
'• 3rd Year → 4th Year\n' +
'• 4th Year mappings will be PERMANENTLY DELETED.\n\n' +
'Do you wish to proceed with this irreversible operation?'
);
if (confirmed) {
try {
const response = await fetch(`${API_BASE_URL}/api/classes/promote?institution=${formData.institution}`, {
method: 'POST'
});
if (response.ok) {
fetchClasses();
alert(`${formData.institution} academic year promotion completed successfully!`);
}
} catch (error) {
console.error('Promotion failed:', error);
}
}
};
const filteredClasses = classes
.filter(c =>
c.institution === formData.institution && (
c.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.academicYear.toLowerCase().includes(searchTerm.toLowerCase())
)
)
.sort((a, b) => b.id - a.id);
const currentClasses = filteredClasses.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-10">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="text-4xl font-black text-text-dark tracking-tight mb-1">Class Management</h1>
<p className="text-text-muted font-black uppercase tracking-widest text-[10px]">Define institutional academic structures & sections</p>
</div>
<div className="flex items-center gap-4">
<button
onClick={handlePromote}
className="flex items-center gap-3 bg-[#FFF9ED] text-[#D97706] border border-[#FDE68A] px-6 py-3 rounded-2xl font-black text-[11px] uppercase tracking-widest hover:scale-105 transition-all"
>
<RefreshCw className="w-4 h-4" />
Promote Academic Year
<AlertTriangle className="w-4 h-4" />
</button>
<div className="w-12 h-12 bg-brand-glow rounded-2xl flex items-center justify-center text-brand-indigo premium-shadow">
<Building2 className="w-6 h-6" />
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10">
{/* Left Column - Form */}
<div className="lg:col-span-4">
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow p-8 space-y-8 sticky top-10"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-brand-indigo rounded-xl flex items-center justify-center text-white">
<Plus className="w-6 h-6" />
</div>
<h2 className="text-xl font-black text-text-dark tracking-tight">Add New Mapping</h2>
</div>
<div className="space-y-6">
{/* Institution */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Select Institution</label>
<div className="flex gap-4">
{[
{ id: 'RIT', logo: ritLogo, label: 'RIT' },
{ id: 'RSB', logo: rsbLogo, label: 'RSB' }
].map(inst => (
<button
key={inst.id}
type="button"
onClick={() => setFormData({...formData, institution: inst.id})}
className={cn(
"flex-1 h-20 rounded-2xl transition-all border-2 flex items-center justify-center relative overflow-hidden group",
formData.institution === inst.id
? "bg-white border-brand-indigo premium-shadow"
: "bg-slate-50 border-transparent grayscale opacity-50 hover:opacity-100 hover:grayscale-0 hover:bg-white hover:border-slate-200"
)}
>
<img
src={inst.logo}
alt={inst.label}
className={cn(
"transition-all",
inst.id === 'RIT' ? "h-10" : "h-7"
)}
/>
</button>
))}
</div>
</div>
{/* Department */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Department</label>
<select
value={formData.department}
onChange={e => setFormData({...formData, department: e.target.value})}
className="w-full bg-slate-50 border-transparent rounded-2xl py-4 px-6 text-sm font-bold focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
{currentDepartments.map(dept => <option key={dept} value={dept}>{dept}</option>)}
</select>
</div>
{/* Academic Year */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Academic Year</label>
<select
value={formData.academicYear}
onChange={e => setFormData({...formData, academicYear: e.target.value})}
className="w-full bg-slate-50 border-transparent rounded-2xl py-4 px-6 text-sm font-bold focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
{years.map(year => <option key={year} value={year}>{year}</option>)}
</select>
</div>
{/* Sections */}
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Sections</label>
<div className="bg-slate-50 rounded-2xl p-4 space-y-4">
<div className="flex flex-wrap gap-2">
<AnimatePresence>
{formData.sections.map(section => (
<motion.span
key={section}
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
className="px-3 py-1.5 bg-white rounded-lg text-xs font-black text-brand-indigo border border-brand-indigo/10 flex items-center gap-2 group"
>
{section}
<button onClick={() => removeSection(section)} className="hover:text-status-danger transition-colors">
<X className="w-3 h-3" />
</button>
</motion.span>
))}
</AnimatePresence>
</div>
<form onSubmit={handleAddSection} className="flex gap-2">
<input
type="text"
placeholder="Type section (eg. A)..."
value={newSection}
onChange={e => setNewSection(e.target.value)}
className="flex-1 bg-transparent border-none text-xs font-bold focus:ring-0 p-0"
/>
<button type="submit" className="w-8 h-8 bg-brand-glow rounded-lg flex items-center justify-center text-brand-indigo hover:bg-brand-indigo hover:text-white transition-all">
<Plus className="w-4 h-4" />
</button>
</form>
</div>
</div>
<button
onClick={handleSave}
className="w-full bg-brand-indigo text-white rounded-2xl py-5 font-black text-[12px] uppercase tracking-[0.2em] premium-shadow hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-3"
>
<Layers className="w-5 h-5" />
Save Configuration
</button>
</div>
</motion.div>
</div>
{/* Right Column - Table */}
<div className="lg:col-span-8">
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden"
>
<div className="p-8 border-b border-slate-50 flex items-center justify-between gap-6">
<div className="flex items-center gap-4">
<div className="w-2 h-8 bg-brand-indigo rounded-full" />
<h3 className="text-xl font-black text-text-dark tracking-tight">Academic Structure</h3>
</div>
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="text"
placeholder="Search structures..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
className="w-full bg-slate-50 border-transparent rounded-2xl py-3 pl-12 pr-4 text-sm font-bold focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50/50">
<th className="px-8 py-5 text-[9px] font-black uppercase tracking-[0.2em] text-text-muted">Institution</th>
<th className="px-8 py-5 text-[9px] font-black uppercase tracking-[0.2em] text-text-muted">Department \ Year</th>
<th className="px-8 py-5 text-[9px] font-black uppercase tracking-[0.2em] text-text-muted">Configured Sections</th>
<th className="px-8 py-5 text-[9px] font-black uppercase tracking-[0.2em] text-text-muted text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{isLoading ? (
<tr><td colSpan={4} className="p-20 text-center text-text-muted italic">Loading structure...</td></tr>
) : filteredClasses.length === 0 ? (
<tr><td colSpan={4} className="p-20 text-center text-text-muted italic">No academic structures defined.</td></tr>
) : (
currentClasses.map((item, idx) => (
<motion.tr
key={item.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
className="group hover:bg-slate-50/30 transition-colors"
>
<td className="px-8 py-6">
<div className="w-16 h-10 bg-slate-50 rounded-xl border border-slate-100 flex items-center justify-center p-1.5">
<img
src={item.institution === 'RIT' ? ritLogo : rsbLogo}
alt={item.institution}
className="h-full object-contain"
/>
</div>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-5">
<div className="w-12 h-12 rounded-2xl bg-slate-50 border border-slate-100 flex flex-col items-center justify-center">
<span className="text-[9px] font-black text-text-muted uppercase">{item.department.substring(0, 3)}</span>
<div className="w-4 h-0.5 bg-brand-indigo/30 rounded-full mt-0.5" />
</div>
<div>
<h4 className="text-sm font-black text-text-dark">{item.department}</h4>
<div className="flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest text-brand-indigo mt-0.5">
<GraduationCap className="w-3.5 h-3.5" />
{item.academicYear}
</div>
</div>
</div>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-2">
{item.sections.map(section => (
<span key={section} className="w-7 h-7 rounded-lg bg-white border border-slate-200 flex items-center justify-center text-[10px] font-black text-text-muted group-hover:border-brand-indigo group-hover:text-brand-indigo transition-all">
{section}
</span>
))}
<div className="ml-4 flex items-center gap-1.5 px-3 py-1 bg-emerald-50 text-status-success rounded-lg border border-emerald-100/50">
<CheckCircle2 className="w-3.5 h-3.5" />
<span className="text-[10px] font-black uppercase tracking-widest">Ready</span>
</div>
</div>
</td>
<td className="px-8 py-6 text-right">
<div className="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-all">
<button className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:text-brand-indigo hover:border-brand-indigo premium-shadow-sm transition-all">
<Layers className="w-4.5 h-4.5" />
</button>
<button onClick={() => handleDelete(item.id)} className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-400 hover:text-status-danger hover:border-status-danger premium-shadow-sm transition-all">
<Trash2 className="w-4.5 h-4.5" />
</button>
</div>
</td>
</motion.tr>
))
)}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredClasses.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 20, 50]}
/>
</motion.div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,121 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Calendar, CheckCircle2, Circle } from 'lucide-react';
import { cn } from '../../lib/utils';
import { INSTITUTIONAL_EVENTS } from '../../constants/institutionalEvents';
import { getCurrentAcademicYear } from '../../lib/dateUtils';
import { API_BASE_URL } from '../../lib/config';
interface ClubEvent {
id: string;
name: string;
month: string;
semester: 'ODD' | 'EVEN';
}
interface Event {
id: number;
title: string;
status: string;
academicYears: string[];
}
interface ClubInstitutionalChecklistProps {
onEventClick: (data: { eventName: string, isClubEvent: boolean }) => void;
}
export const ClubInstitutionalChecklist: React.FC<ClubInstitutionalChecklistProps> = ({ onEventClick }) => {
const [events, setEvents] = useState<Event[]>([]);
const currentAcademicYear = getCurrentAcademicYear();
useEffect(() => {
fetch(API_BASE_URL + '/api/events')
.then(res => res.json())
.then(data => setEvents(data))
.catch(err => console.error('Failed to fetch events:', err));
}, []);
const getEventStatus = (eventName: string) => {
const matching = events.filter(e =>
e.title.toLowerCase() === eventName.toLowerCase() &&
e.academicYears?.includes(currentAcademicYear) &&
(e.status?.toUpperCase() === 'APPROVED' || e.status?.toUpperCase() === 'COMPLETED')
);
return matching.length > 0;
};
const oddSemesterEvents = INSTITUTIONAL_EVENTS.filter(e => e.semester === 'ODD');
const evenSemesterEvents = INSTITUTIONAL_EVENTS.filter(e => e.semester === 'EVEN');
const SemesterCard = ({ title, subtitle, events, gradient }: { title: string, subtitle: string, events: ClubEvent[], gradient: string }) => (
<div className="flex-1 min-w-[400px] bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden">
<div className={cn("p-8 text-white flex justify-between items-start", gradient)}>
<div>
<h3 className="text-2xl font-black tracking-tight">{title}</h3>
<p className="text-[10px] font-black uppercase tracking-widest opacity-80 mt-1">{subtitle}</p>
</div>
<Calendar className="w-6 h-6 opacity-60" />
</div>
<div className="p-8 space-y-6 max-h-[600px] overflow-y-auto custom-scrollbar">
{events.map((event, i) => {
const isScheduled = getEventStatus(event.name);
return (
<motion.button
key={event.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
onClick={() => onEventClick({ eventName: event.name, isClubEvent: true })}
className="w-full flex items-center gap-6 group text-left"
>
<div className="relative">
<Circle className={cn("w-8 h-8 fill-white", isScheduled ? "text-emerald-500" : "text-slate-100")} />
<div className="absolute inset-0 flex items-center justify-center">
{isScheduled ? (
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
) : (
<div className="w-4 h-4 rounded-full border-2 border-slate-200 group-hover:border-brand-indigo transition-colors" />
)}
</div>
</div>
<div>
<p className={cn("text-sm font-black transition-colors", isScheduled ? "text-emerald-600" : "text-text-dark group-hover:text-brand-indigo")}>{event.name}</p>
<p className="text-[9px] font-bold text-text-muted uppercase tracking-widest mt-0.5">
{isScheduled ? 'Scheduled & Approved' : `Target: ${event.month}`}
</p>
</div>
</motion.button>
);
})}
</div>
</div>
);
return (
<div className="space-y-10">
<div className="flex items-center gap-4">
<div className="w-1.5 h-10 bg-brand-indigo rounded-full" />
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tighter">Institutional Checklists</h2>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted mt-1">Track Institutional Club Requirements</p>
</div>
</div>
<div className="flex flex-col xl:flex-row gap-8">
<SemesterCard
title="ODD Semester"
subtitle="Club & Institutional Events"
events={oddSemesterEvents}
gradient="bg-gradient-to-r from-blue-600 to-indigo-600"
/>
<SemesterCard
title="Even Semester"
subtitle="Club & Institutional Events"
events={evenSemesterEvents}
gradient="bg-gradient-to-r from-indigo-600 to-purple-600"
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,190 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
X,
Calendar as CalendarIcon,
MapPin,
Clock,
Building2,
Users,
Tag,
Target
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { format } from 'date-fns';
interface EventDetailsModalProps {
isOpen: boolean;
onClose: () => void;
event: any;
}
export const EventDetailsModal: React.FC<EventDetailsModalProps> = ({ isOpen, onClose, event }) => {
if (!isOpen || !event) return null;
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const startDate = parseDate(event.startDate);
const endDate = parseDate(event.endDate);
return (
<AnimatePresence>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="relative w-full max-w-2xl bg-white rounded-[2.5rem] shadow-2xl overflow-hidden flex flex-col max-h-[90vh]"
>
{/* Header */}
<div className="p-8 bg-brand-navy text-white flex justify-between items-start relative overflow-hidden">
<div className="relative z-10 pr-12">
<div className="flex items-center gap-3 mb-3">
<span className="px-3 py-1 bg-white/10 rounded-full text-[10px] font-black uppercase tracking-widest border border-white/10">
{event.department}
</span>
<span className="px-3 py-1 bg-brand-indigo/30 rounded-full text-[10px] font-black uppercase tracking-widest text-brand-glow border border-brand-indigo/50">
{event.type}
</span>
</div>
<h2 className="text-2xl sm:text-3xl font-black tracking-tight leading-tight">
{event.title}
</h2>
</div>
<button
onClick={onClose}
className="absolute top-8 right-8 p-2 bg-white/10 hover:bg-white/20 rounded-xl transition-colors z-10"
>
<X className="w-5 h-5" />
</button>
{/* Background Decoration */}
<div className="absolute -right-10 -bottom-10 w-40 h-40 bg-brand-indigo/20 rounded-full blur-3xl" />
</div>
{/* Content */}
<div className="p-8 overflow-y-auto custom-scrollbar space-y-8">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="flex items-start gap-4 p-4 rounded-2xl bg-slate-50 border border-slate-100">
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center shadow-sm text-brand-indigo shrink-0">
<CalendarIcon className="w-5 h-5" />
</div>
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">Date & Time</p>
<p className="text-sm font-bold text-text-dark">
{startDate ? format(startDate, 'MMMM d, yyyy') : 'TBD'}
</p>
<p className="text-xs font-bold text-text-muted mt-0.5">
{startDate ? format(startDate, 'hh:mm a') : 'TBD'} - {endDate ? format(endDate, 'hh:mm a') : 'TBD'}
</p>
</div>
</div>
<div className="flex items-start gap-4 p-4 rounded-2xl bg-slate-50 border border-slate-100">
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center shadow-sm text-emerald-500 shrink-0">
<MapPin className="w-5 h-5" />
</div>
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">Venue</p>
<p className="text-sm font-bold text-text-dark">
{event.location || 'TBD'}
</p>
<p className="text-xs font-bold text-text-muted mt-0.5">
{event.institution || 'RIT'}
</p>
</div>
</div>
</div>
{event.description && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3">Event Description</h3>
<div className="p-5 rounded-2xl bg-slate-50 border border-slate-100">
<p className="text-sm font-medium text-text-muted leading-relaxed whitespace-pre-wrap">
{event.description}
</p>
</div>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{event.academicYears && event.academicYears.length > 0 && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3 flex items-center gap-2">
<Target className="w-4 h-4 text-brand-indigo" />
Target Audience
</h3>
<div className="flex flex-wrap gap-2">
{event.academicYears.map((year: string) => (
<span key={year} className="px-3 py-1.5 bg-brand-glow text-brand-indigo text-xs font-bold rounded-xl border border-brand-indigo/10">
{year}
</span>
))}
</div>
</div>
)}
{event.targetedSections && event.targetedSections.length > 0 && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3 flex items-center gap-2">
<Users className="w-4 h-4 text-emerald-500" />
Sections
</h3>
<div className="flex flex-wrap gap-2">
{event.targetedSections.map((sec: string) => (
<span key={sec} className="px-3 py-1.5 bg-emerald-50 text-emerald-600 text-xs font-bold rounded-xl border border-emerald-100">
Section {sec}
</span>
))}
</div>
</div>
)}
</div>
{event.guestName && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3">Special Guest</h3>
<div className="p-4 rounded-2xl bg-slate-50 border border-slate-100 flex items-center justify-between">
<div>
<p className="text-sm font-bold text-text-dark">{event.guestName}</p>
{event.guestSocialProfile && (
<a href={event.guestSocialProfile} target="_blank" rel="noopener noreferrer" className="text-xs font-bold text-brand-indigo hover:underline mt-0.5 inline-block">
View Profile
</a>
)}
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="p-6 border-t border-slate-100 bg-slate-50/50 flex justify-end">
<button
onClick={onClose}
className="px-6 py-3 bg-white border border-slate-200 text-text-dark rounded-xl text-sm font-black hover:bg-slate-50 hover:border-slate-300 transition-all shadow-sm"
>
Close
</button>
</div>
</motion.div>
</div>
</AnimatePresence>
);
};

View File

@@ -0,0 +1,387 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
History,
Search,
Calendar,
MapPin,
CheckCircle2,
XCircle,
Clock,
Download,
Filter,
AlertTriangle,
ChevronRight,
User,
Heart,
FileText,
X,
Building2,
Users,
Wallet,
Ticket
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { format } from 'date-fns';
import { Pagination } from './Pagination';
interface Event {
id: number;
title: string;
startDate: string;
endDate: string;
type: string;
status: string;
department: string;
location: string;
budget: number;
conflictMessage?: string;
rejectionReason?: string;
institution: string;
category: string;
academicYears: string[];
proposer?: {
fullName: string;
};
description?: string;
sponsors?: string[];
hasRegistrationFee: boolean;
registrationFee: number;
}
export const EventHistory: React.FC = () => {
const [events, setEvents] = useState<Event[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('ALL');
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
useEffect(() => {
fetch(API_BASE_URL + '/api/events')
.then(res => res.json())
.then(data => setEvents(data))
.catch(err => console.error('History fetch failed:', err));
}, []);
const getStatusStyle = (status: string, hasConflict: boolean) => {
if (hasConflict && status !== 'APPROVED') return {
bg: 'bg-red-50',
text: 'text-red-600',
border: 'border-red-100',
icon: AlertTriangle,
label: 'CONFLICT'
};
switch (status) {
case 'APPROVED': return { bg: 'bg-emerald-50', text: 'text-emerald-600', border: 'border-emerald-100', icon: CheckCircle2, label: 'APPROVED' };
case 'REQUESTED': return { bg: 'bg-blue-50', text: 'text-blue-600', border: 'border-blue-100', icon: Clock, label: 'REQUESTED' };
case 'PENDING_PR': return { bg: 'bg-purple-50', text: 'text-purple-600', border: 'border-purple-100', icon: ShieldCheck, label: 'PENDING PRINCIPAL' };
case 'HOD_REJECTED':
case 'PRINCIPAL_REJECTED': return { bg: 'bg-red-50', text: 'text-red-600', border: 'border-red-100', icon: XCircle, label: 'REJECTED' };
default: return { bg: 'bg-slate-50', text: 'text-slate-600', border: 'border-slate-100', icon: Clock, label: status };
}
};
const filteredEvents = events
.filter(e => {
const matchesSearch = e.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
e.department.toLowerCase().includes(searchQuery.toLowerCase());
if (statusFilter === 'ALL') return matchesSearch;
if (statusFilter === 'PENDING') return matchesSearch && (e.status === 'REQUESTED' || e.status === 'PENDING_PR');
if (statusFilter === 'REJECTED') return matchesSearch && e.status.includes('REJECTED');
return matchesSearch && e.status === statusFilter;
})
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
const currentEvents = filteredEvents.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-8 pb-10">
{/* Header Section */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="flex items-center gap-5">
<div className="w-12 h-12 bg-brand-navy rounded-2xl flex items-center justify-center text-white premium-shadow">
<History className="w-6 h-6" />
</div>
<div>
<h2 className="text-2xl font-black text-text-dark tracking-tight">Institutional Event History</h2>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted">Audit-ready comprehensive event ledger</p>
</div>
</div>
<div className="flex items-center gap-3">
<button className="flex items-center gap-2 px-4 py-2 bg-white border border-slate-100 rounded-xl text-[10px] font-black uppercase tracking-widest text-text-muted hover:bg-slate-50 transition-all">
<Download className="w-4 h-4" />
Export CSV
</button>
</div>
</div>
{/* Filters Bar */}
<div className="flex flex-col lg:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-300" />
<input
type="text"
placeholder="Search by event title or department..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-16 pl-16 pr-8 bg-white border border-slate-100 rounded-3xl text-sm font-bold text-text-dark focus:outline-none focus:ring-4 focus:ring-brand-indigo/5 focus:border-brand-indigo transition-all"
/>
</div>
<div className="flex items-center gap-2 bg-white p-2 rounded-3xl border border-slate-100">
{['ALL', 'APPROVED', 'PENDING', 'REJECTED'].map(status => (
<button
key={status}
onClick={() => setStatusFilter(status)}
className={cn(
"px-6 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all",
statusFilter === status
? "bg-brand-indigo text-white premium-shadow-sm"
: "text-text-muted hover:bg-slate-50"
)}
>
{status}
</button>
))}
</div>
</div>
{/* Events Ledger */}
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b border-slate-50">
<th className="px-8 py-6 text-left text-[10px] font-black uppercase tracking-widest text-text-muted">Event Details</th>
<th className="px-8 py-6 text-left text-[10px] font-black uppercase tracking-widest text-text-muted">Logistics</th>
<th className="px-8 py-6 text-left text-[10px] font-black uppercase tracking-widest text-text-muted">Status</th>
<th className="px-8 py-6 text-right text-[10px] font-black uppercase tracking-widest text-text-muted">Budget</th>
<th className="px-8 py-6 text-right text-[10px] font-black uppercase tracking-widest text-text-muted"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
<AnimatePresence mode="popLayout">
{currentEvents.map((event, idx) => {
const statusStyle = getStatusStyle(event.status, !!event.conflictMessage);
const StatusIcon = statusStyle.icon;
return (
<motion.tr
key={event.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.03 }}
onClick={() => setSelectedEvent(event)}
className="group hover:bg-slate-50/50 transition-colors cursor-pointer"
>
<td className="px-8 py-6">
<div className="flex items-center gap-4">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center font-black text-[10px]",
event.conflictMessage ? "bg-red-50 text-red-500" : "bg-brand-indigo/5 text-brand-indigo"
)}>
{event.department?.slice(0, 3).toUpperCase()}
</div>
<div>
<p className="text-sm font-black text-text-dark group-hover:text-brand-indigo transition-colors">{event.title}</p>
<p className="text-[10px] font-bold text-text-muted uppercase tracking-widest mt-0.5">{event.type}</p>
</div>
</div>
</td>
<td className="px-8 py-6">
<div className="space-y-2">
<div className="flex items-center gap-2 text-text-muted">
<Calendar className="w-3.5 h-3.5" />
<span className="text-[10px] font-bold">{format(new Date(event.startDate), 'MMM d, yyyy')}</span>
</div>
<div className="flex items-center gap-2 text-text-muted">
<MapPin className="w-3.5 h-3.5" />
<span className="text-[10px] font-bold">{event.location}</span>
</div>
</div>
</td>
<td className="px-8 py-6">
<div className={cn(
"inline-flex items-center gap-2 px-3 py-1.5 rounded-full border text-[9px] font-black uppercase tracking-widest",
statusStyle.bg, statusStyle.text, statusStyle.border
)}>
<StatusIcon className="w-3.5 h-3.5" />
{statusStyle.label}
</div>
</td>
<td className="px-8 py-6 text-right">
<span className="text-sm font-black text-text-dark">{event.budget?.toLocaleString() || '0'}</span>
</td>
<td className="px-8 py-6 text-right">
<button className="p-2 hover:bg-white hover:premium-shadow-sm rounded-lg transition-all text-slate-300 group-hover:text-brand-indigo">
<ChevronRight className="w-5 h-5" />
</button>
</td>
</motion.tr>
);
})}
</AnimatePresence>
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredEvents.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 20, 50]}
/>
</div>
{/* Detail Modal */}
<AnimatePresence>
{selectedEvent && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedEvent(null)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-2xl bg-white rounded-[2.5rem] premium-shadow overflow-hidden max-h-[90vh] overflow-y-auto"
>
<div className="p-8 bg-brand-navy text-white flex justify-between items-start">
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">Event History</span>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">{selectedEvent.institution}</span>
</div>
<h3 className="text-2xl font-black tracking-tight">{selectedEvent.title}</h3>
</div>
<button
onClick={() => setSelectedEvent(null)}
className="p-2 hover:bg-white/10 rounded-xl transition-all"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="p-8 space-y-8">
{/* Description */}
{selectedEvent.description && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<FileText className="w-3.5 h-3.5" />
Description
</div>
<p className="text-sm font-medium text-text-dark leading-relaxed bg-slate-50 p-4 rounded-2xl border border-slate-100">
{selectedEvent.description}
</p>
</div>
)}
<div className="grid grid-cols-2 gap-8">
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Calendar className="w-3.5 h-3.5" />
Date & Time
</div>
<p className="text-sm font-bold text-text-dark">{new Date(selectedEvent.startDate).toLocaleString()}</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<MapPin className="w-3.5 h-3.5" />
Location
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.location}</p>
</div>
</div>
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Building2 className="w-3.5 h-3.5" />
Department
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.department}</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Users className="w-3.5 h-3.5" />
Target Batches
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.academicYears?.join(', ') || 'N/A'}</p>
</div>
</div>
</div>
{/* Sponsors */}
{selectedEvent.sponsors && selectedEvent.sponsors.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Heart className="w-3.5 h-3.5 text-brand-indigo" />
Event Sponsors
</div>
<div className="flex flex-wrap gap-2">
{selectedEvent.sponsors.map((sponsor, idx) => (
<span key={idx} className="px-3 py-1 bg-brand-glow text-brand-indigo rounded-lg text-[10px] font-black uppercase tracking-widest border border-brand-indigo/10">
{sponsor}
</span>
))}
</div>
</div>
)}
<div className="grid grid-cols-2 gap-8 pt-4 border-t border-slate-50">
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Wallet className="w-3.5 h-3.5" />
Budget
</div>
<p className="text-lg font-black text-text-dark">{selectedEvent.budget?.toLocaleString() || '0'}</p>
</div>
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Ticket className="w-3.5 h-3.5" />
Status
</div>
<p className="text-xs font-black text-brand-indigo uppercase">{selectedEvent.status}</p>
</div>
</div>
{selectedEvent.rejectionReason && (
<div className="p-4 bg-red-50 border border-red-100 rounded-2xl">
<p className="text-[10px] font-black uppercase text-red-500 mb-1">Rejection Reason</p>
<p className="text-xs font-bold text-red-700">{selectedEvent.rejectionReason}</p>
</div>
)}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};
const ShieldCheck = (props: any) => (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" />
<path d="m9 12 2 2 4-4" />
</svg>
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { CheckCircle2, Circle, AlertCircle, Clock, XCircle, ShieldCheck, Flag } from 'lucide-react';
import { cn } from '../../lib/utils';
export type Event = {
id: number;
title: string;
startDate: string;
endDate: string;
location: string;
category: string;
type: string;
institution: string;
department: string;
status: 'REQUESTED' | 'APPROVED' | 'COMPLETED' | 'CANCELLED' | 'PENDING_PR' | 'HOD_REJECTED' | 'PRINCIPAL_REJECTED';
rejectionReason?: string;
guestName?: string;
proposer?: {
email: string;
fullName: string;
};
createdAt?: string;
updatedAt?: string;
}
export const StatusTimeline: React.FC<{ event: Event; compact?: boolean }> = ({ event, compact }) => {
const stages = [
{ id: 'REQUESTED', label: 'Proposal Raised', icon: Clock },
{ id: 'PENDING_PR', label: 'HoD Review', icon: ShieldCheck },
{ id: 'APPROVED', label: 'Principal Approval', icon: CheckCircle2 },
{ id: 'COMPLETED', label: 'Event Success', icon: Flag }
];
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const now = new Date();
const endDate = parseDate(event.endDate);
const isAutoCompleted = event.status === 'APPROVED' && endDate && now > endDate;
const isManuallyCompleted = event.status === 'COMPLETED';
const effectiveStatus = (isAutoCompleted || isManuallyCompleted) ? 'COMPLETED' : event.status;
const isHODRejected = effectiveStatus === 'HOD_REJECTED';
const isPRRejected = effectiveStatus === 'PRINCIPAL_REJECTED';
const isCancelled = effectiveStatus === 'CANCELLED';
const isRejected = isHODRejected || isPRRejected || isCancelled;
let currentStageIdx = stages.findIndex(s => s.id === effectiveStatus);
if (currentStageIdx === -1) {
if (isHODRejected) currentStageIdx = 1;
else if (isPRRejected) currentStageIdx = 2;
else if (isCancelled) currentStageIdx = 0;
else currentStageIdx = 0;
}
return (
<div className={cn("w-full space-y-8", compact ? "py-2" : "py-6")}>
<div className="relative flex items-center justify-between w-full px-4">
<div className="absolute left-10 right-10 h-[2px] bg-slate-100 top-[18px]" />
{!isRejected && (
<motion.div
initial={{ width: 0 }}
animate={{ width: `calc(${(currentStageIdx / (stages.length - 1)) * 100}%)` }}
className="absolute left-10 h-[2px] bg-emerald-500 top-[18px] origin-left transition-all duration-1000"
/>
)}
{stages.map((stage, i) => {
const isPast = i < currentStageIdx;
const isCurrent = i === currentStageIdx;
const isFuture = i > currentStageIdx;
const stageRejected = isRejected && isCurrent;
const stageCompleted = isPast || (isCurrent && !isRejected);
return (
<div key={stage.id} className="relative z-10 flex flex-col items-center">
<motion.div
initial={false}
animate={{
scale: isCurrent ? 1.1 : 1,
backgroundColor: stageRejected ? "#ef4444" : stageCompleted ? "#10b981" : "#f1f5f9",
borderColor: stageRejected ? "#fee2e2" : stageCompleted ? "#d1fae5" : "#fff"
}}
className={cn(
"w-9 h-9 rounded-full flex items-center justify-center border-4 shadow-sm transition-all duration-500",
stageRejected ? "text-white" : stageCompleted ? "text-white" : "text-slate-300"
)}
>
{stageRejected ? <XCircle className="w-5 h-5" /> :
stageCompleted ? <CheckCircle2 className="w-5 h-5" /> :
<stage.icon className="w-4 h-4" />}
</motion.div>
<div className="absolute top-12 flex flex-col items-center min-w-[100px]">
<span className={cn(
"text-[8px] font-black uppercase tracking-[0.2em] whitespace-nowrap",
stageRejected ? "text-red-500" : stageCompleted ? "text-emerald-600" : "text-slate-400"
)}>
{stageRejected ? (isHODRejected ? 'HoD Rejected' : isPRRejected ? 'PR Rejected' : 'Cancelled') : stage.label}
</span>
{isCurrent && !isRejected && (
<motion.span
animate={{ opacity: [0.4, 1, 0.4] }}
transition={{ repeat: Infinity, duration: 2 }}
className="text-[6px] font-bold text-brand-indigo mt-1 uppercase tracking-tighter"
>
Current Phase
</motion.span>
)}
</div>
</div>
);
})}
</div>
<AnimatePresence>
{isRejected && event.rejectionReason && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-16 p-6 bg-red-50/50 border border-red-100 rounded-[2rem] flex gap-4 items-start"
>
<div className="w-10 h-10 bg-red-500 rounded-xl flex items-center justify-center shrink-0 shadow-lg shadow-red-200">
<AlertCircle className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-red-500 mb-1">Feedback Message</p>
<p className="text-sm font-bold text-red-700 leading-relaxed italic">"{event.rejectionReason}"</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,244 @@
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Clock,
CheckCircle2,
AlertCircle,
Edit3,
Calendar,
MapPin,
ChevronRight,
ShieldCheck,
XCircle,
Timer,
ChevronLeft,
ChevronDown
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { format } from 'date-fns';
interface Event {
id: number;
title: string;
startDate: any;
endDate: any;
location: string;
status: 'REQUESTED' | 'APPROVED' | 'COMPLETED' | 'CANCELLED' | 'PENDING_PR' | 'HOD_REJECTED' | 'PRINCIPAL_REJECTED';
rejectionReason?: string;
category?: string;
}
interface FacultyEventManagementProps {
events: Event[];
onEditEvent: (event: Event) => void;
}
export const FacultyEventManagement: React.FC<FacultyEventManagementProps> = ({ events, onEditEvent }) => {
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(3);
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const sortedEvents = useMemo(() => {
return [...events].sort((a, b) => {
const dateA = parseDate(a.startDate)?.getTime() || 0;
const dateB = parseDate(b.startDate)?.getTime() || 0;
return dateB - dateA;
});
}, [events]);
const totalPages = Math.ceil(sortedEvents.length / itemsPerPage);
const currentEvents = sortedEvents.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
const rangeStart = events.length === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
const rangeEnd = Math.min(currentPage * itemsPerPage, events.length);
const getStatusConfig = (status: string) => {
switch (status) {
case 'APPROVED':
return { color: 'text-emerald-500', bg: 'bg-emerald-50', border: 'border-emerald-100', icon: CheckCircle2, label: 'Approved' };
case 'PENDING_PR':
return { color: 'text-brand-indigo', bg: 'bg-brand-glow', border: 'border-brand-indigo/10', icon: ShieldCheck, label: 'At Principal Desk' };
case 'REQUESTED':
return { color: 'text-amber-500', bg: 'bg-amber-50', border: 'border-amber-100', icon: Timer, label: 'At HoD Desk' };
case 'HOD_REJECTED':
case 'PRINCIPAL_REJECTED':
return { color: 'text-red-500', bg: 'bg-red-50', border: 'border-red-100', icon: XCircle, label: 'Rejected' };
case 'CANCELLED':
return { color: 'text-slate-400', bg: 'bg-slate-50', border: 'border-slate-200', icon: XCircle, label: 'Cancelled' };
default:
return { color: 'text-slate-500', bg: 'bg-slate-50', border: 'border-slate-100', icon: Clock, label: status };
}
};
return (
<div className="card-widget h-full flex flex-col">
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-xl font-black text-brand-navy tracking-tight flex items-center gap-2">
<Calendar className="w-5 h-5 text-brand-indigo" />
My Event Proposals
</h3>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400 mt-1">
Track and manage your submitted requests
</p>
</div>
<div className="flex items-center gap-3">
<div className="bg-slate-50 px-3 py-1.5 rounded-xl border border-slate-100">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
{events.length} Total
</span>
</div>
</div>
</div>
<div className="flex-1 space-y-4 overflow-hidden">
<AnimatePresence mode="wait">
{events.length === 0 ? (
<motion.div
key="empty"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center justify-center py-12 text-center"
>
<div className="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center mb-4">
<Calendar className="w-8 h-8 text-slate-200" />
</div>
<p className="text-sm font-bold text-slate-400 italic">No event proposals yet.</p>
</motion.div>
) : (
<motion.div
key={currentPage + '-' + itemsPerPage}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="space-y-4"
>
{currentEvents.map((event, idx) => {
const config = getStatusConfig(event.status);
const eventDate = parseDate(event.startDate);
const canEdit = ['REQUESTED', 'PENDING_PR'].includes(event.status);
return (
<div
key={event.id}
className="group relative bg-white border border-slate-100 rounded-2xl p-4 hover:border-brand-indigo/30 hover:shadow-md transition-all"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<div className={cn("px-2 py-0.5 rounded-md text-[8px] font-black uppercase tracking-widest border", config.bg, config.color, config.border)}>
{config.label}
</div>
<span className="text-[9px] font-black text-slate-300 uppercase tracking-widest"></span>
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">ID: #{event.id}</span>
</div>
<h4 className="text-sm font-black text-brand-navy truncate group-hover:text-brand-indigo transition-colors mb-2">
{event.title}
</h4>
<div className="flex flex-wrap gap-x-4 gap-y-2">
<div className="flex items-center gap-1.5 text-slate-500">
<Clock className="w-3.5 h-3.5 text-brand-indigo/60" />
<span className="text-[10px] font-bold">
{eventDate ? format(eventDate, 'MMM d, h:mm a') : 'TBD'}
</span>
</div>
<div className="flex items-center gap-1.5 text-slate-500">
<MapPin className="w-3.5 h-3.5 text-brand-indigo/60" />
<span className="text-[10px] font-bold truncate max-w-[120px]">
{event.location}
</span>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
{canEdit ? (
<button
onClick={() => onEditEvent(event)}
className="p-2.5 bg-brand-glow text-brand-indigo rounded-xl hover:bg-brand-indigo hover:text-white transition-all shadow-sm group/btn"
title="Edit Proposal"
>
<Edit3 className="w-4 h-4 transition-transform group-hover/btn:scale-110" />
</button>
) : (
<div className="p-2.5 bg-slate-50 text-slate-300 rounded-xl cursor-not-allowed" title="Cannot edit once in review">
<Edit3 className="w-4 h-4" />
</div>
)}
<button className="p-2.5 bg-white border border-slate-100 text-slate-400 rounded-xl hover:bg-slate-50 hover:text-brand-indigo transition-all shadow-sm">
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
{event.rejectionReason && (
<div className="mt-3 p-3 bg-red-50/50 border border-red-100 rounded-xl flex items-start gap-2">
<AlertCircle className="w-3.5 h-3.5 text-red-500 shrink-0 mt-0.5" />
<p className="text-[10px] font-medium text-red-700 italic">
"{event.rejectionReason}"
</p>
</div>
)}
</div>
);
})}
</motion.div>
)}
</AnimatePresence>
</div>
<div className="mt-6 flex items-center justify-end gap-8 pt-4 border-t border-slate-50">
<div className="flex items-center gap-3">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Rows per page</span>
<div className="relative group">
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
className="bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-[11px] font-black text-brand-navy appearance-none pr-10 cursor-pointer hover:border-brand-indigo/30 transition-all focus:outline-none"
>
{[3, 5, 10, 20].map(val => (
<option key={val} value={val}>{val}</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-400 pointer-events-none" />
</div>
</div>
<div className="text-[11px] font-black text-brand-navy tracking-tight min-w-[80px] text-center">
{rangeStart}-{rangeEnd} <span className="text-slate-300 mx-1">of</span> {events.length}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="p-2 text-slate-400 hover:text-brand-indigo disabled:opacity-20 disabled:cursor-not-allowed transition-all hover:bg-slate-50 rounded-lg"
>
<ChevronLeft className="w-5 h-5" />
</button>
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || events.length === 0}
className="p-2 text-slate-400 hover:text-brand-indigo disabled:opacity-20 disabled:cursor-not-allowed transition-all hover:bg-slate-50 rounded-lg"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,488 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import {
Calendar as CalendarIcon,
ChevronLeft,
ChevronRight,
Plus,
MessageSquare,
Loader2,
X,
FilePlus,
Clock
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '../../lib/utils';
import {
format,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
eachDayOfInterval,
isSameMonth,
isSameDay,
addMonths,
subMonths,
isWeekend
} from 'date-fns';
import { useAuth } from '../../context/AuthContext';
interface Event {
id: number;
title: string;
type: string;
status: string;
department: string;
startDate: string;
endDate: string;
}
interface Note {
id: number;
content: string;
targetDate: string;
authorName: string;
}
interface InstitutionalCalendarProps {
onProposeEvent?: (date: string) => void;
compact?: boolean;
}
export const InstitutionalCalendar: React.FC<InstitutionalCalendarProps> = ({ onProposeEvent, compact }) => {
const { user } = useAuth();
const [currentDate, setCurrentDate] = useState(new Date());
const [events, setEvents] = useState<Event[]>([]);
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState(true);
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
const [newNote, setNewNote] = useState('');
const [isSubmittingNote, setIsSubmittingNote] = useState(false);
// Advanced Filters
const [filters, setFilters] = useState({
department: '',
batch: '',
section: ''
});
// Rescheduling State
const [reschedulingEvent, setReschedulingEvent] = useState<Event | null>(null);
const [newTargetDate, setNewTargetDate] = useState('');
const [startTime, setStartTime] = useState('10:00');
const [endTime, setEndTime] = useState('12:00');
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
// Handle Spring Boot LocalDateTime array: [yyyy, mm, dd, hh, mm, ss]
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const fetchData = async () => {
try {
const [eventsRes, notesRes] = await Promise.all([
fetch(API_BASE_URL + '/api/events'),
fetch(API_BASE_URL + '/api/notes')
]);
if (eventsRes.ok && notesRes.ok) {
const eventsData = await eventsRes.json();
const notesData = await notesRes.json();
// Robust filtering and status check
const filteredEvents = eventsData.filter((e: any) => {
const status = e.status?.toUpperCase();
return status === 'APPROVED' || status === 'COMPLETED';
});
setEvents(filteredEvents);
setNotes(notesData);
}
} catch (error) {
console.error('Failed to fetch calendar data:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const handleAddNote = async () => {
if (!newNote.trim() || !selectedDay) return;
setIsSubmittingNote(true);
try {
const response = await fetch(API_BASE_URL + '/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: newNote,
targetDate: format(selectedDay, 'yyyy-MM-dd'),
authorName: user?.fullName || 'Anonymous',
authorEmail: user?.email
})
});
if (response.ok) {
setNewNote('');
fetchData();
}
} catch (error) {
console.error('Failed to add note:', error);
} finally {
setIsSubmittingNote(false);
}
};
const handleReschedule = async () => {
if (!reschedulingEvent || !newTargetDate) return;
try {
const response = await fetch(`${API_BASE_URL}/api/events/${reschedulingEvent.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
startDate: `${newTargetDate}T${startTime}:00`,
endDate: `${newTargetDate}T${endTime}:00`
})
});
if (response.ok) {
setReschedulingEvent(null);
setNewTargetDate('');
setSelectedDay(null);
fetchData();
}
} catch (error) {
console.error('Failed to reschedule event:', error);
}
};
const filteredEvents = events.filter(e => {
const matchesDept = !filters.department || e.department === filters.department;
// We'd need to cast to any for targetedSections if not in interface
const matchesBatch = !filters.batch || (e as any).academicYears?.includes(filters.batch);
const matchesSection = !filters.section || (e as any).targetedSections?.includes(filters.section);
return matchesDept && matchesBatch && matchesSection;
});
const days = eachDayOfInterval({
start: startOfWeek(startOfMonth(currentDate)),
end: endOfWeek(endOfMonth(currentDate))
});
if (loading) {
return (
<div className="flex flex-col items-center justify-center p-20 gap-4">
<Loader2 className="w-10 h-10 text-brand-indigo animate-spin" />
<p className="text-sm font-black text-brand-indigo uppercase tracking-widest">Loading Institutional Schedule...</p>
</div>
);
}
return (
<div className={cn("space-y-8", compact && "space-y-4")}>
{/* Header & Advanced Filters */}
{!compact && (
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-brand-indigo rounded-2xl flex items-center justify-center text-white premium-shadow">
<CalendarIcon className="w-6 h-6" />
</div>
<div>
<h2 className="text-2xl font-black text-text-dark tracking-tight">Event & Notes Manager</h2>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted">Master schedule audit & granular management</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-3">
<select
value={filters.department}
onChange={(e) => setFilters({...filters, department: e.target.value})}
className="bg-white border border-slate-100 px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest text-text-dark outline-none premium-shadow-sm"
>
<option value="">All Departments</option>
{Array.from(new Set(events.map(e => e.department))).sort().map(d => (
<option key={d} value={d}>{d}</option>
))}
</select>
<select
value={filters.batch}
onChange={(e) => setFilters({...filters, batch: e.target.value})}
className="bg-white border border-slate-100 px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest text-text-dark outline-none premium-shadow-sm"
>
<option value="">All Batches</option>
{['1st Year', '2nd Year', '3rd Year', '4th Year'].map(b => (
<option key={b} value={b}>{b}</option>
))}
</select>
<select
value={filters.section}
onChange={(e) => setFilters({...filters, section: e.target.value})}
className="bg-white border border-slate-100 px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest text-text-dark outline-none premium-shadow-sm"
>
<option value="">All Sections</option>
{['A', 'B', 'C', 'D'].map(s => (
<option key={s} value={s}>Section {s}</option>
))}
</select>
</div>
</div>
)}
<div className={cn(
"bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden",
compact && "rounded-none shadow-none border-none"
)}>
{/* Calendar Navigation */}
<div className={cn("flex items-center justify-between border-b border-slate-50", compact ? "p-4" : "p-8")}>
<h3 className={cn("font-black text-text-dark tracking-tighter", compact ? "text-xl" : "text-3xl")}>
{format(currentDate, 'MMMM yyyy')}
</h3>
<div className="flex gap-2">
<button
onClick={() => setCurrentDate(subMonths(currentDate, 1))}
className={cn("hover:bg-slate-50 rounded-2xl transition-all border border-slate-100", compact ? "p-2" : "p-3")}
>
<ChevronLeft className={cn("text-text-muted", compact ? "w-4 h-4" : "w-5 h-5")} />
</button>
<button
onClick={() => setCurrentDate(addMonths(currentDate, 1))}
className={cn("hover:bg-slate-50 rounded-2xl transition-all border border-slate-100", compact ? "p-2" : "p-3")}
>
<ChevronRight className={cn("text-text-muted", compact ? "w-4 h-4" : "w-5 h-5")} />
</button>
</div>
</div>
{/* Calendar Grid */}
<div className={cn(compact ? "p-4" : "p-8")}>
<div className="grid grid-cols-7 gap-px bg-slate-100 border border-slate-100 rounded-3xl overflow-hidden">
{['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'].map((day, i) => (
<div key={`${day}-${i}`} className="bg-white py-4 text-center text-[10px] font-black text-text-muted tracking-widest border-b border-slate-50">
{day}
</div>
))}
{days.map((day, idx) => {
const dayStr = format(day, 'yyyy-MM-dd');
const dayEvents = filteredEvents.filter(e => {
const date = parseDate(e.startDate);
return date && format(date, 'yyyy-MM-dd') === dayStr;
});
const dayNotes = notes.filter(n => {
const date = parseDate(n.targetDate);
return date && format(date, 'yyyy-MM-dd') === dayStr;
});
const isToday = isSameDay(day, new Date());
const isCurrentMonth = isSameMonth(day, currentDate);
const weekend = isWeekend(day);
return (
<div
key={day.toString()}
onClick={() => setSelectedDay(day)}
className={cn(
compact ? "min-h-[80px] p-2" : "min-h-[140px] p-4",
"bg-white hover:bg-slate-50/50 transition-all cursor-pointer group relative",
!isCurrentMonth && "opacity-30 grayscale pointer-events-none",
isToday && "bg-brand-glow/10"
)}
>
<div className="flex justify-between items-start mb-2">
<span className={cn(
"text-sm font-black",
isToday ? "text-brand-indigo" : "text-text-dark",
weekend && !isToday && "text-red-400"
)}>
{format(day, 'd')}
</span>
{weekend && isCurrentMonth && (
<span className="text-[8px] font-black text-red-300 uppercase tracking-widest">Holiday</span>
)}
</div>
{/* Day Content */}
<div className="space-y-1.5 overflow-y-auto max-h-[100px] custom-scrollbar pr-1">
{dayEvents.map(event => (
<div key={event.id} className="p-2.5 bg-brand-glow/20 border border-brand-indigo/10 rounded-xl transition-all hover:bg-brand-glow/40">
<p className="text-[10px] font-black text-brand-indigo leading-tight truncate">{event.title}</p>
<p className="text-[8px] font-black text-brand-indigo/60 uppercase tracking-widest mt-0.5">
{event.department === 'AI&DS' ? 'AI&DS' : event.department?.split(' ')[0]}
</p>
</div>
))}
{dayNotes.map(note => (
<div key={note.id} className="p-2 bg-amber-50 border border-amber-200 rounded-lg flex gap-2 items-start">
<MessageSquare className="w-2.5 h-2.5 text-amber-500 mt-0.5 shrink-0" />
<p className="text-[9px] font-bold text-amber-700 leading-tight">{note.content}</p>
</div>
))}
</div>
{/* Hover Action Indicator */}
<div className="absolute inset-0 border-2 border-brand-indigo/0 group-hover:border-brand-indigo/10 rounded-xl transition-all pointer-events-none" />
</div>
);
})}
</div>
</div>
</div>
<AnimatePresence>
{selectedDay && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-6 bg-slate-900/40 backdrop-blur-sm">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white w-full max-w-md rounded-[2.5rem] premium-shadow-2xl overflow-hidden p-10 space-y-8"
>
{/* Modal Header */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-2xl font-black text-text-dark tracking-tight">Add Event Info</h3>
<p className="text-sm font-bold text-text-muted mt-1">Details for {format(selectedDay, 'yyyy-MM-dd')}</p>
</div>
<button onClick={() => setSelectedDay(null)} className="p-2 hover:bg-slate-50 rounded-xl transition-all">
<X className="w-6 h-6 text-slate-300" />
</button>
</div>
{/* Event List in Modal */}
<div className="space-y-4 max-h-[300px] overflow-y-auto custom-scrollbar pr-2">
{filteredEvents.filter(e => {
const date = parseDate(e.startDate);
return date && format(date, 'yyyy-MM-dd') === format(selectedDay, 'yyyy-MM-dd');
}).map(event => (
<div key={event.id} className="flex flex-col gap-4 p-5 bg-slate-50 border border-slate-100 rounded-[2rem] transition-all hover:bg-white hover:premium-shadow group">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-1.5 h-8 bg-brand-indigo rounded-full" />
<div>
<p className="text-sm font-black text-text-dark">{event.title}</p>
<p className="text-[8px] font-black uppercase tracking-widest text-text-muted mt-0.5">{event.department} {event.type}</p>
</div>
</div>
<button
onClick={() => {
setReschedulingEvent(event);
setNewTargetDate(format(selectedDay, 'yyyy-MM-dd'));
const d = parseDate(event.startDate);
if (d) {
setStartTime(format(d, 'HH:mm'));
}
const ed = parseDate(event.endDate);
if (ed) {
setEndTime(format(ed, 'HH:mm'));
}
}}
className="p-2.5 bg-white rounded-xl border border-slate-100 text-brand-indigo hover:bg-brand-indigo hover:text-white transition-all premium-shadow-sm"
>
<Clock className="w-4 h-4" />
</button>
</div>
{reschedulingEvent?.id === event.id && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="pt-4 border-t border-slate-100 space-y-4"
>
<div className="space-y-4">
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Reschedule Date</label>
<input
type="date"
value={newTargetDate}
onChange={(e) => setNewTargetDate(e.target.value)}
className="w-full bg-white border border-slate-200 rounded-xl p-3 text-[10px] font-black text-text-dark focus:outline-none focus:ring-2 focus:ring-brand-indigo/10"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">Start Time</label>
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full bg-white border border-slate-200 rounded-xl p-3 text-[10px] font-black text-text-dark focus:outline-none focus:ring-2 focus:ring-brand-indigo/10"
/>
</div>
<div>
<label className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-2 block">End Time</label>
<input
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full bg-white border border-slate-200 rounded-xl p-3 text-[10px] font-black text-text-dark focus:outline-none focus:ring-2 focus:ring-brand-indigo/10"
/>
</div>
</div>
<button
onClick={handleReschedule}
className="w-full py-4 bg-brand-indigo text-white rounded-xl text-[9px] font-black uppercase tracking-widest premium-shadow active:scale-95 transition-all mt-2"
>
Apply Schedule Change
</button>
</div>
</motion.div>
)}
</div>
))}
</div>
{/* Note Input */}
<div className="space-y-4">
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted">New Note</label>
<input
type="text"
value={newNote}
onChange={(e) => setNewNote(e.target.value)}
placeholder="e.g. AI Workshop"
className="w-full h-16 px-6 bg-white border-2 border-brand-indigo/30 rounded-2xl text-sm font-bold text-text-dark focus:outline-none focus:border-brand-indigo transition-all"
/>
</div>
{/* Action Buttons */}
<div className="flex items-center justify-end gap-6 pt-4">
<button
onClick={() => setSelectedDay(null)}
className="text-sm font-black text-text-dark/60 hover:text-text-dark transition-colors"
>
Cancel
</button>
<button
onClick={handleAddNote}
disabled={!newNote.trim() || isSubmittingNote}
className="px-8 py-4 bg-brand-indigo text-white rounded-3xl font-black text-sm premium-shadow-sm hover:translate-y-[-2px] active:translate-y-[0px] transition-all disabled:opacity-50"
>
{isSubmittingNote ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Save Note'}
</button>
</div>
{/* Propose Action Fallback */}
<div className="pt-6 border-t border-slate-50 text-center">
<button
onClick={() => {
if (onProposeEvent) onProposeEvent(format(selectedDay, 'yyyy-MM-dd'));
setSelectedDay(null);
}}
className="text-[10px] font-black uppercase tracking-widest text-brand-indigo hover:underline"
>
+ Propose New Event for this date
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -0,0 +1,367 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState, useMemo } from 'react';
import {
CheckCircle2,
Circle,
Loader2,
Users,
AlertTriangle,
PlusCircle,
ChevronRight,
ShieldCheck,
Split,
Globe,
Layers,
ArrowUpRight,
Calendar,
ChevronDown
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '../../context/AuthContext';
import { Pagination } from './Pagination';
interface Event {
id: number;
title: string;
type: string;
status: string;
department: string;
institution: string;
academicYears: string[];
targetedSections: string[];
category: string;
createdAt?: string;
updatedAt?: string;
}
interface ClassMapping {
institution: string;
department: string;
academicYear: string;
sections: string[];
}
const CATEGORIES = [
{ id: 'GUEST_LECTURE', label: 'Guest Lectures', matchTypes: ['Guest Lecture', 'Workshop', 'Seminar'] },
{ id: 'ALUMNI_LECTURE', label: 'Alumni Lectures', matchTypes: ['Alumni Lecture'] },
{ id: 'CONFERENCE', label: 'Conference', matchTypes: ['Conference'] },
{ id: 'INDUSTRIAL_VISIT', label: 'Industrial Visit', matchTypes: ['Industrial Visit'] },
{ id: 'TECHFEST', label: 'Techfest', matchTypes: ['Techfest'] },
{ id: 'PROF_SOCIETY', label: 'Prof. Society Activities', matchTypes: ['Club', 'Professional Society'] },
{ id: 'INSTITUTIONAL', label: 'Institutional Events', matchTypes: ['Institutional'] },
];
const ALL_DEPARTMENTS = [
'AI&DS', 'AI&ML', 'CSE', 'CCE', 'CSBS', 'ECE', 'MECH', 'EE(VLSI)', 'BIOTECH', 'Placement Department', 'H&S Dept'
];
const DepartmentCard: React.FC<{
deptName: string;
events: Event[];
selectedBatch: string;
index: number;
isApplicable: boolean;
sections: string[];
onIncompleteClick?: (data: any) => void;
isCompact?: boolean;
}> = ({ deptName, events, selectedBatch, index, isApplicable, sections, onIncompleteClick, isCompact }) => {
const departmentEvents = events.filter(e => {
const status = e.status?.toUpperCase();
const isDeptMatch = e.department?.trim().toLowerCase() === deptName.trim().toLowerCase();
const isInstitutional = e.category?.toUpperCase() === 'INSTITUTIONAL';
return (isDeptMatch || isInstitutional) &&
(status === 'APPROVED' || status === 'COMPLETED' || status === 'PENDING_PR');
});
// Base requirement is 1 event per category per batch, or 2 if multiple sections exist
const getRequiredCount = (catId: string) => {
if (catId === 'INSTITUTIONAL') return 1;
return sections.length > 1 ? 2 : 1;
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05, duration: 0.5 }}
className={cn(
"bg-white rounded-[2rem] flex flex-col h-full relative group transition-all",
isCompact ? "border-none shadow-none p-0" : "border border-slate-100 overflow-hidden premium-shadow p-0"
)}
>
<div className={cn("p-6 border-b border-slate-50", isCompact ? "bg-white p-0 pb-4 border-none" : "bg-slate-50/30 p-8")}>
<div className="flex items-center justify-between gap-4 mb-2">
<h3 className={cn("font-black text-text-dark group-hover:text-brand-indigo transition-colors", isCompact ? "text-lg" : "text-2xl tracking-tight")}>{deptName}</h3>
<div className="flex items-center gap-2">
{!isApplicable ? (
<span className="px-3 py-1 bg-amber-50 text-amber-600 rounded-full border border-amber-100 text-[8px] font-black uppercase tracking-widest">Inert Batch</span>
) : (
<span className="px-3 py-1 bg-brand-glow text-brand-indigo rounded-full border border-brand-indigo/10 text-[8px] font-black uppercase tracking-widest flex items-center gap-1.5">
<Users className="w-2.5 h-2.5" />
{sections.length} Sections
</span>
)}
</div>
</div>
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">
Academic Year Profile: <span className="text-brand-indigo">{selectedBatch}</span>
</p>
</div>
<div className={cn("space-y-6 flex-1 bg-white", isCompact ? "p-0" : "p-8")}>
{CATEGORIES.map((cat) => {
const matchingEvents = departmentEvents.filter(event => {
const matchesType = cat.matchTypes.some(mt => mt.toLowerCase() === event.type?.toLowerCase());
const eventBatches = event.academicYears?.map(y => y.trim().toLowerCase()) || [];
const isInstitutional = event.category?.toUpperCase() === 'INSTITUTIONAL';
const batchMatches = eventBatches.length === 0 || eventBatches.includes(selectedBatch.trim().toLowerCase());
return matchesType && (isInstitutional ? batchMatches : eventBatches.includes(selectedBatch.trim().toLowerCase()));
});
const currentCount = matchingEvents.length;
let coveredSections = new Set<string>();
matchingEvents.forEach(e => {
if (e.category === 'INSTITUTIONAL' || !e.targetedSections || e.targetedSections.length === 0) {
sections.forEach(s => coveredSections.add(s));
} else {
e.targetedSections.forEach(s => coveredSections.add(s));
}
});
const eventsRequired = getRequiredCount(cat.id);
const missingSections = sections.filter(s => !coveredSections.has(s));
const isFullyCovered = (missingSections.length === 0 && currentCount > 0) || (currentCount >= eventsRequired);
return (
<div
key={cat.id}
className="group/item relative"
onClick={() => !isFullyCovered && onIncompleteClick?.({
department: deptName,
eventType: cat.matchTypes[0],
academicYears: [selectedBatch]
})}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-4">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all border shadow-sm",
isFullyCovered ? "bg-emerald-500 border-emerald-400 text-white" : currentCount > 0 ? "bg-brand-indigo border-brand-indigo text-white" : "bg-white border-slate-100 text-slate-300"
)}>
{isFullyCovered ? <ShieldCheck className="w-5 h-5" /> : currentCount > 0 ? <Layers className="w-5 h-5" /> : <Circle className="w-5 h-5" />}
</div>
<div>
<span className={cn(
"text-sm font-black tracking-tight",
isFullyCovered ? "text-text-dark" : currentCount > 0 ? "text-brand-indigo" : "text-text-muted"
)}>
{cat.label}
</span>
<div className="flex items-center gap-2 mt-0.5">
<span className={cn(
"text-[8px] font-black uppercase tracking-[0.1em]",
isFullyCovered ? "text-emerald-500" : currentCount > 0 ? "text-brand-indigo" : "text-slate-400"
)}>
{isFullyCovered ? 'Complete Coverage' : missingSections.length > 0 && currentCount > 0 ? 'Partial Coverage' : 'Awaiting Proposals'}
</span>
</div>
</div>
</div>
<div className="flex flex-col items-end">
<div className={cn(
"px-3 py-1.5 rounded-xl text-[10px] font-black tracking-widest border transition-all",
isFullyCovered ? "bg-emerald-50 text-emerald-600 border-emerald-100" : currentCount > 0 ? "bg-brand-glow text-brand-indigo border-brand-indigo/10" : "bg-slate-50 text-text-muted border-slate-100"
)}>
{currentCount}/{eventsRequired}
</div>
</div>
</div>
<div className="h-1.5 bg-slate-100 rounded-full overflow-hidden mb-3">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${Math.min((currentCount / eventsRequired) * 100, 100)}%` }}
className={cn(
"h-full rounded-full transition-all duration-1000",
isFullyCovered ? "bg-emerald-500" : "bg-brand-indigo"
)}
/>
</div>
{missingSections.length > 0 && currentCount > 0 && (
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} className="flex items-start gap-2 p-3 bg-red-50 rounded-xl border border-red-100 mt-2">
<AlertTriangle className="w-3.5 h-3.5 text-red-500 shrink-0" />
<div>
<p className="text-[9px] font-black text-red-600 uppercase tracking-widest">Sections Left Out</p>
<p className="text-[10px] font-bold text-red-700">{missingSections.join(', ')}</p>
</div>
</motion.div>
)}
</div>
);
})}
</div>
</motion.div>
);
};
export const InstitutionalChecklist: React.FC<{
isCompact?: boolean;
isGlobalView?: boolean;
onIncompleteClick?: (data: any) => void;
}> = ({ isCompact, isGlobalView, onIncompleteClick }) => {
const { user } = useAuth();
const [events, setEvents] = useState<Event[]>([]);
const [classes, setClasses] = useState<ClassMapping[]>([]);
const [loading, setLoading] = useState(true);
const [selectedBatch, setSelectedBatch] = useState<string>('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(6);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const [eventsRes, classesRes] = await Promise.all([
fetch(API_BASE_URL + '/api/events'),
fetch(API_BASE_URL + '/api/classes')
]);
if (eventsRes.ok && classesRes.ok) {
const [eventsData, classesData] = await Promise.all([
eventsRes.json(),
classesRes.json()
]);
setEvents(eventsData);
setClasses(classesData);
const batches = Array.from(new Set(classesData.map((c: any) => c.academicYear.trim()))).sort().reverse();
if (batches.length > 0 && !selectedBatch) {
setSelectedBatch(batches[0] as string);
}
}
} catch (error) {
console.error('Failed to fetch checklist data:', error);
} finally {
setLoading(false);
}
};
const allBatches = useMemo(() => {
return Array.from(new Set(classes.map(c => c.academicYear.trim()))).sort().reverse();
}, [classes]);
const filteredDepartments = useMemo(() => {
let depts = ALL_DEPARTMENTS;
if (user?.role === 'PRINCIPAL' || user?.role === 'ADMIN') depts = ALL_DEPARTMENTS;
else if (isGlobalView && user?.role === 'HOD') depts = ALL_DEPARTMENTS;
else depts = ALL_DEPARTMENTS.filter(d => d.toLowerCase().includes(user?.department?.toLowerCase() || ''));
// Recent items at top logic: Sort departments by the date of their latest event
return [...depts].sort((a, b) => {
const latestA = events.filter(e => e.department === a || e.category === 'INSTITUTIONAL').reduce((max, e) => Math.max(max, new Date(e.updatedAt || e.createdAt || 0).getTime()), 0);
const latestB = events.filter(e => e.department === b || e.category === 'INSTITUTIONAL').reduce((max, e) => Math.max(max, new Date(e.updatedAt || e.createdAt || 0).getTime()), 0);
return latestB - latestA;
});
}, [user, isGlobalView, events]);
const currentDepts = filteredDepartments.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
if (loading) {
return (
<div className="flex items-center justify-center p-20">
<Loader2 className="w-8 h-8 text-brand-indigo animate-spin" />
</div>
);
}
return (
<div className={cn("space-y-8", isCompact ? "space-y-4" : "space-y-8")}>
{!isCompact && (
<div className="bg-white p-8 rounded-[3rem] border border-slate-100 premium-shadow flex flex-col md:flex-row md:items-center justify-between gap-8 relative overflow-hidden">
{/* Adjusted Badge Position to prevent overlapping with dropdown */}
<div className="absolute top-4 left-4">
<div className="px-3 py-1 bg-emerald-50 text-emerald-600 rounded-full border border-emerald-100 flex items-center gap-2">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse" />
<span className="text-[8px] font-black uppercase tracking-widest whitespace-nowrap">Audit Engine Live</span>
</div>
</div>
<div className="pt-6 md:pt-0">
<h2 className="text-3xl font-black text-text-dark tracking-tight mb-2">Academic Audit</h2>
<p className="text-xs font-bold text-text-muted flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-emerald-500" />
Monitoring department-wide compliance and session coverage.
</p>
</div>
<div className="flex items-center gap-4 bg-slate-50 p-4 rounded-3xl group relative min-w-[240px]">
<div className="flex flex-col flex-1">
<span className="text-[9px] font-black uppercase tracking-widest text-text-muted mb-1 ml-2">Audit Year</span>
<div className="relative">
<select
value={selectedBatch}
onChange={(e) => setSelectedBatch(e.target.value)}
className="w-full bg-transparent text-brand-indigo font-black text-lg outline-none cursor-pointer appearance-none relative z-10 pr-10"
>
{allBatches.map(batch => <option key={batch} value={batch}>{batch}</option>)}
</select>
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-5 h-5 text-brand-indigo pointer-events-none transition-transform group-hover:translate-y-[-40%]" />
</div>
</div>
<div className="w-12 h-12 bg-brand-indigo text-white rounded-xl flex items-center justify-center shadow-lg group-hover:scale-110 transition-all shrink-0">
<Calendar className="w-6 h-6" />
</div>
</div>
</div>
)}
<div className={cn(
"grid",
isCompact
? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10"
)}>
{currentDepts.map((deptName, idx) => {
const deptClasses = classes.filter(c => c.department === deptName && c.academicYear.trim() === selectedBatch.trim());
const sections = deptClasses.reduce((acc, c) => [...acc, ...c.sections], [] as string[]);
return (
<DepartmentCard
key={`${deptName}-${selectedBatch}`}
deptName={deptName}
events={events}
selectedBatch={selectedBatch}
index={idx}
isApplicable={deptClasses.length > 0}
sections={sections}
onIncompleteClick={onIncompleteClick}
isCompact={isCompact}
/>
);
})}
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredDepartments.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[3, 6, 9, 12]}
/>
</div>
);
};

View File

@@ -0,0 +1,429 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Calendar as CalendarIcon,
Clock,
CheckCircle2,
AlertCircle,
TrendingUp,
Users,
MapPin,
ChevronRight,
Plus,
ShieldCheck,
Zap,
ArrowUpRight,
Target,
BarChart3,
LayoutDashboard,
Bell,
GraduationCap,
Save,
Check
} from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { InstitutionalChecklist } from './InstitutionalChecklist';
import { StatusTimeline, type Event } from './EventStatusTimeline';
import { InstitutionalCalendar } from './InstitutionalCalendar';
import { VenueTimeline } from './VenueTimeline';
import { PrincipalMasterChecklist } from './PrincipalMasterChecklist';
import { VenuesAtGlance } from './VenuesAtGlance';
import { FacultyEventManagement } from './FacultyEventManagement';
import { cn } from '../../lib/utils';
import { format, formatDistanceToNow } from 'date-fns';
interface OverviewProps {
userEvents: any[];
onNavigate: (id: string) => void;
onIncompleteClick: (data: any) => void;
onCalendarPropose: (date: string) => void;
onClubEventClick: (data: any) => void;
onEditEvent?: (event: any) => void;
}
export const Overview: React.FC<OverviewProps> = ({
userEvents,
onNavigate,
onIncompleteClick,
onCalendarPropose,
onClubEventClick,
onEditEvent
}) => {
const { user } = useAuth();
const [stats, setStats] = useState({
upcoming: 0,
pending: 0,
completed: 0,
budget: '₹0'
});
const [alerts, setAlerts] = useState<Array<{type: string, msg: string, time: string}>>([]);
const { login } = useAuth();
const [strengthInput, setStrengthInput] = useState<number>(user?.classStrength || 0);
const [isSavingStrength, setIsSavingStrength] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const handleSaveStrength = async () => {
if (!user) return;
setIsSavingStrength(true);
try {
const response = await fetch(`${API_BASE_URL}/api/admin/users/${user.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...user, classStrength: strengthInput })
});
if (response.ok) {
const updatedUser = await response.json();
login(updatedUser);
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 2000);
}
} catch (error) {
console.error('Failed to update class strength:', error);
} finally {
setIsSavingStrength(false);
}
};
useEffect(() => {
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const calculateStats = (data: any[]) => {
if (!Array.isArray(data)) return;
const now = new Date();
const upcoming = data.filter(e => {
const d = parseDate(e.startDate);
return e.status === 'APPROVED' && d && d > now;
}).length;
const pending = data.filter(e => e.status === 'REQUESTED' || e.status === 'PENDING_PR').length;
const completed = data.filter(e => {
const d = parseDate(e.endDate);
return e.status === 'APPROVED' && d && d < now;
}).length;
const totalBudget = data
.filter(e => e.status === 'APPROVED')
.reduce((acc, e) => acc + (e.budget || 0), 0);
let budgetDisplay = `${totalBudget.toLocaleString()}`;
if (totalBudget >= 100000) {
budgetDisplay = `${(totalBudget / 100000).toFixed(1)}L`;
} else if (totalBudget >= 1000) {
budgetDisplay = `${(totalBudget / 1000).toFixed(1)}K`;
}
setStats({
upcoming,
pending,
completed,
budget: budgetDisplay
});
const generatedAlerts: any[] = [];
const sortedEvents = [...data].sort((a, b) => {
return new Date(b.updatedAt || b.createdAt || 0).getTime() - new Date(a.updatedAt || a.createdAt || 0).getTime();
});
for (const event of sortedEvents) {
if (generatedAlerts.length >= 4) break;
const dateToUse = event.updatedAt || event.createdAt;
if (!dateToUse) continue;
let timeStr = formatDistanceToNow(new Date(dateToUse), { addSuffix: true })
.replace('about ', '')
.replace(' minutes', 'm')
.replace(' minute', 'm')
.replace(' hours', 'h')
.replace(' hour', 'h')
.replace(' days', 'd')
.replace(' day', 'd');
if (event.conflictMessage) {
generatedAlerts.push({ type: 'warning', msg: `Conflict: ${event.title}`, time: timeStr });
} else if (event.status === 'REJECTED') {
generatedAlerts.push({ type: 'error', msg: `Rejected: ${event.title}`, time: timeStr });
} else if (event.status === 'APPROVED') {
generatedAlerts.push({ type: 'success', msg: `Approved: ${event.title}`, time: timeStr });
} else if (event.status === 'REQUESTED' || event.status === 'PENDING_PR') {
generatedAlerts.push({ type: 'info', msg: `New proposal: ${event.title}`, time: timeStr });
}
}
if (generatedAlerts.length === 0) {
generatedAlerts.push({ type: 'info', msg: 'No recent activity', time: 'Just now' });
}
setAlerts(generatedAlerts);
};
if (userEvents && userEvents.length > 0) {
calculateStats(userEvents);
} else {
fetch(API_BASE_URL + '/api/events')
.then(res => {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
return res.json();
})
.then(data => {
if (Array.isArray(data)) {
const statsData = (user?.role === 'PRINCIPAL' || user?.role === 'ADMIN')
? data
: data.filter(e => e.proposer?.email === user?.email || e.department === user?.department);
calculateStats(statsData);
}
})
.catch(err => console.error("Stats fetch error:", err));
}
}, [userEvents, user]);
return (
<div className="space-y-8 animate-in fade-in duration-700">
{/* Welcome Bar - Subtle */}
<div className="flex items-center justify-between mb-2">
<div>
<h2 className="text-2xl font-black text-brand-navy tracking-tight">
Welcome back, <span className="text-brand-indigo">{user?.fullName?.split(' ')[0]}</span>
</h2>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">
Institutional Dashboard {format(new Date(), 'EEEE, MMMM do')}
</p>
</div>
</div>
{/* Principal Master Checklist - Only for Principal Desk */}
{user?.role === 'PRINCIPAL' && (
<PrincipalMasterChecklist />
)}
{user?.role === 'FACULTY' && user?.isClassIncharge && (
<div className="bg-brand-indigo/5 border border-brand-indigo/10 rounded-[1.5rem] p-6 mb-8 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-brand-indigo text-white flex items-center justify-center shrink-0 shadow-sm">
<GraduationCap className="w-6 h-6" />
</div>
<div>
<h3 className="text-lg font-black text-brand-navy tracking-tight">Class Incharge Dashboard</h3>
<p className="text-xs font-bold text-slate-500 mt-1">
Assigned Class: <span className="text-brand-indigo uppercase tracking-wider">{user.inchargeClass} {user.inchargeBatch} Sec {user.inchargeSection}</span>
</p>
</div>
</div>
<div className="flex items-center gap-4 bg-white p-2 pr-4 rounded-xl shadow-sm border border-slate-100">
<div className="flex flex-col pl-3">
<label className="text-[9px] font-black uppercase tracking-widest text-slate-400">Class Strength</label>
<input
type="number"
min="1"
value={strengthInput || ''}
onChange={(e) => setStrengthInput(parseInt(e.target.value) || 0)}
className="w-20 text-lg font-black text-brand-navy focus:outline-none placeholder:text-slate-200"
placeholder="0"
/>
</div>
<button
onClick={handleSaveStrength}
disabled={isSavingStrength || strengthInput === user.classStrength}
className={cn(
"p-2.5 rounded-lg transition-all flex items-center justify-center text-white min-w-[40px]",
saveSuccess ? "bg-emerald-500" :
strengthInput !== user.classStrength ? "bg-brand-indigo hover:shadow-md hover:scale-105 active:scale-95" :
"bg-slate-200 text-slate-400"
)}
>
{isSavingStrength ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> :
saveSuccess ? <Check className="w-4 h-4" /> :
<Save className="w-4 h-4" />}
</button>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Row 1: Live Status and Analytics */}
<div className="lg:col-span-12 flex flex-col gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="w-full">
<div className="card-widget">
<div className="flex items-center justify-between mb-6">
<h3 className="font-black text-brand-navy flex items-center gap-2">
Live Status
</h3>
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[10px] font-black text-emerald-500 uppercase tracking-widest">Active</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{[
{ label: 'Upcoming', value: stats.upcoming, icon: CalendarIcon, color: 'text-indigo-600', bg: 'bg-indigo-50' },
{ label: 'Pending', value: stats.pending, icon: Bell, color: 'text-amber-600', bg: 'bg-amber-50' },
{ label: 'Finished', value: stats.completed, icon: CheckCircle2, color: 'text-emerald-600', bg: 'bg-emerald-50' },
{ label: 'Budget', value: stats.budget, icon: TrendingUp, color: 'text-slate-600', bg: 'bg-slate-50' },
].map((stat) => (
<div key={stat.label} className={cn("p-4 rounded-2xl border border-slate-100/50", stat.bg)}>
<div className="flex items-center gap-3 mb-2">
<stat.icon className={cn("w-4 h-4", stat.color)} />
<span className="text-[9px] font-black uppercase tracking-widest text-slate-400">{stat.label}</span>
</div>
<p className="text-xl font-black text-brand-navy">{stat.value}</p>
</div>
))}
</div>
<div className="mt-6 p-4 bg-brand-glow rounded-2xl border border-brand-indigo/10">
<div className="flex items-center gap-3 mb-2">
<Zap className="w-4 h-4 text-brand-indigo" />
<span className="text-[10px] font-black uppercase tracking-widest text-brand-indigo">Quick Insight</span>
</div>
<p className="text-[11px] font-bold text-brand-indigo/70 leading-relaxed">
{stats.pending > 0
? `You have ${stats.pending} proposals waiting for your review. Processing them now ensures timely venue booking.`
: "All systems clear. Your departmental compliance is currently at 100% for the current cycle."}
</p>
</div>
</div>
</div>
<div className="w-full">
<div className="card-widget">
<div className="flex items-center justify-between mb-6">
<h3 className="font-black text-brand-navy flex items-center gap-2">
Analytics
</h3>
<BarChart3 className="w-4 h-4 text-slate-300" />
</div>
<div className="flex flex-col items-center justify-center py-4">
<div className="relative w-32 h-32 flex items-center justify-center">
<svg className="w-full h-full -rotate-90">
<circle cx="64" cy="64" r="58" fill="transparent" stroke="#f1f5f9" strokeWidth="12" />
<circle cx="64" cy="64" r="58" fill="transparent" stroke="#4f46e5" strokeWidth="12" strokeDasharray="364.4" strokeDashoffset={364.4 * (1 - 0.65)} strokeLinecap="round" />
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-2xl font-black text-brand-navy">{userEvents.length}</span>
<span className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Events</span>
</div>
</div>
<div className="grid grid-cols-2 gap-x-8 gap-y-3 mt-8 w-full">
{[
{ label: 'Technical', color: 'bg-brand-indigo' },
{ label: 'Cultural', color: 'bg-emerald-500' },
{ label: 'Sports', color: 'bg-amber-500' },
{ label: 'Other', color: 'bg-slate-400' },
].map(cat => (
<div key={cat.label} className="flex items-center gap-2">
<div className={cn("w-2 h-2 rounded-full", cat.color)} />
<span className="text-[10px] font-bold text-slate-500">{cat.label}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Today's Tasks / My Proposals */}
<div className="lg:col-span-12">
{(user?.role === 'PRINCIPAL' || user?.role === 'ADMIN') ? (
<div className="card-widget flex flex-col">
<div className="flex items-center justify-between mb-6">
<h3 className="font-black text-brand-navy flex items-center gap-2">
Today's Tasks
<span className="bg-slate-100 text-slate-500 text-[10px] px-2 py-0.5 rounded-full">
{stats.pending}
</span>
</h3>
<button
onClick={() => onNavigate('checklist')}
className="text-[10px] font-black text-brand-indigo uppercase tracking-widest hover:underline"
>
See All
</button>
</div>
<div className="w-full">
<InstitutionalChecklist isCompact onIncompleteClick={onIncompleteClick} />
</div>
</div>
) : (
<FacultyEventManagement
events={userEvents}
onEditEvent={onEditEvent || (() => {})}
/>
)}
</div>
<div className="lg:col-span-12">
<VenuesAtGlance events={userEvents} />
</div>
{/* Row 2: Upcoming Events & Alerts */}
<div className="lg:col-span-8">
<div className="card-widget">
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-xl font-black text-brand-navy tracking-tight">Institutional Schedule</h3>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400 mt-1">Unified view of all approved events & academic notes</p>
</div>
<div className="flex gap-2">
<div className="px-3 py-1.5 bg-slate-50 rounded-xl border border-slate-100">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Master Feed</span>
</div>
</div>
</div>
<div className="rounded-[1.5rem] overflow-hidden border border-slate-50">
<InstitutionalCalendar compact onProposeEvent={onCalendarPropose} />
</div>
</div>
</div>
<div className="lg:col-span-4">
<div className="card-widget h-full">
<div className="flex items-center justify-between mb-6">
<h3 className="font-black text-brand-navy flex items-center gap-2">
Recent Alerts
</h3>
<button className="text-[10px] font-black text-brand-indigo uppercase tracking-widest hover:underline">
Clear All
</button>
</div>
<div className="space-y-4">
{alerts.map((alert, i) => (
<div key={i} className="flex gap-4 group">
<div className={cn(
"w-1 h-8 rounded-full mt-1 shrink-0",
alert.type === 'success' ? 'bg-emerald-500' :
alert.type === 'warning' ? 'bg-amber-500' :
alert.type === 'error' ? 'bg-red-500' : 'bg-brand-indigo'
)} />
<div>
<p className="text-xs font-bold text-brand-navy leading-snug group-hover:text-brand-indigo transition-colors">{alert.msg}</p>
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">{alert.time}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react';
import { cn } from '../../lib/utils';
interface PaginationProps {
currentPage: number;
totalItems: number;
itemsPerPage: number;
onPageChange: (page: number) => void;
onItemsPerPageChange: (items: number) => void;
itemsPerPageOptions?: number[];
}
export const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalItems,
itemsPerPage,
onPageChange,
onItemsPerPageChange,
itemsPerPageOptions = [5, 10, 20, 50]
}) => {
const totalPages = Math.ceil(totalItems / itemsPerPage);
const rangeStart = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
const rangeEnd = Math.min(currentPage * itemsPerPage, totalItems);
if (totalItems === 0) return null;
return (
<div className="flex items-center justify-end gap-8 py-4 px-8 border-t border-slate-50 bg-slate-50/10">
<div className="flex items-center gap-3">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Rows per page</span>
<div className="relative group">
<select
value={itemsPerPage}
onChange={(e) => onItemsPerPageChange(Number(e.target.value))}
className="bg-white border border-slate-200 rounded-xl px-4 py-2 text-[11px] font-black text-brand-navy appearance-none pr-10 cursor-pointer hover:border-brand-indigo/30 transition-all focus:outline-none premium-shadow-sm"
>
{itemsPerPageOptions.map(val => (
<option key={val} value={val}>{val}</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-3 h-3 text-slate-400 pointer-events-none" />
</div>
</div>
<div className="text-[11px] font-black text-brand-navy tracking-tight min-w-[80px] text-center">
{rangeStart}-{rangeEnd} <span className="text-slate-300 mx-1">of</span> {totalItems}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="p-2 text-slate-400 hover:text-brand-indigo disabled:opacity-20 disabled:cursor-not-allowed transition-all hover:bg-slate-50 rounded-lg"
>
<ChevronLeft className="w-5 h-5" />
</button>
<button
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="p-2 text-slate-400 hover:text-brand-indigo disabled:opacity-20 disabled:cursor-not-allowed transition-all hover:bg-slate-50 rounded-lg"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,152 @@
import React, { useState, useEffect, useMemo } from 'react';
import { motion } from 'framer-motion';
import {
Calendar,
CheckCircle2,
Circle,
Clock,
AlertCircle,
ShieldCheck,
ChevronRight,
Loader2
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { API_BASE_URL } from '../../lib/config';
import { INSTITUTIONAL_EVENTS } from '../../constants/institutionalEvents';
import { getCurrentAcademicYear } from '../../lib/dateUtils';
interface Event {
id: number;
title: string;
status: string;
academicYears: string[];
startDate: string;
}
export const PrincipalMasterChecklist: React.FC = () => {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const currentAcademicYear = getCurrentAcademicYear();
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
setEvents(data);
}
} catch (err) {
console.error('Failed to fetch events:', err);
} finally {
setLoading(false);
}
};
const getEventStatus = (eventName: string) => {
const currentYear = new Date().getFullYear();
const matchingEvents = events.filter(e => {
const matchesName = e.title.toLowerCase().includes(eventName.toLowerCase());
const eventDate = new Date(e.startDate);
// Check if event is within the current academic cycle (roughly current year +/- 1)
const matchesCycle = eventDate.getFullYear() === currentYear ||
(eventDate.getFullYear() === currentYear - 1 && eventDate.getMonth() >= 5) ||
(eventDate.getFullYear() === currentYear + 1 && eventDate.getMonth() < 5);
return matchesName && matchesCycle;
});
if (matchingEvents.some(e => e.status?.toUpperCase() === 'COMPLETED')) return 'COMPLETED';
if (matchingEvents.some(e => e.status?.toUpperCase() === 'APPROVED')) return 'APPROVED';
if (matchingEvents.some(e => e.status?.toUpperCase() === 'PENDING_PR' || e.status?.toUpperCase() === 'REQUESTED')) return 'PENDING';
return 'NOT_SCHEDULED';
};
const oddSemesterEvents = INSTITUTIONAL_EVENTS.filter(e => e.semester === 'ODD');
const evenSemesterEvents = INSTITUTIONAL_EVENTS.filter(e => e.semester === 'EVEN');
const SemesterCard = ({ title, events }: { title: string, events: typeof INSTITUTIONAL_EVENTS }) => (
<div className="flex-1 bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden flex flex-col">
<div className="p-8 border-b border-slate-50 bg-slate-50/30">
<h3 className="text-xl font-black text-brand-navy tracking-tight">{title}</h3>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400 mt-1">Institutional Milestones</p>
</div>
<div className="p-8 space-y-4 flex-1 overflow-y-auto max-h-[500px] custom-scrollbar">
{events.map((event, i) => {
const status = getEventStatus(event.name);
return (
<motion.div
key={event.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
className="flex items-center justify-between group p-4 hover:bg-slate-50 rounded-2xl transition-all"
>
<div className="flex items-center gap-4">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
status === 'COMPLETED' ? "bg-emerald-500 text-white" :
status === 'APPROVED' ? "bg-brand-indigo text-white" :
status === 'PENDING' ? "bg-amber-500 text-white" : "bg-slate-100 text-slate-300"
)}>
{status === 'COMPLETED' ? <CheckCircle2 className="w-5 h-5" /> :
status === 'APPROVED' ? <Calendar className="w-5 h-5" /> :
status === 'PENDING' ? <Clock className="w-5 h-5" /> : <Circle className="w-5 h-5" />}
</div>
<div>
<p className="text-sm font-black text-brand-navy group-hover:text-brand-indigo transition-colors">{event.name}</p>
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">TARGET: {event.month}</p>
</div>
</div>
<div className={cn(
"px-3 py-1 rounded-lg text-[8px] font-black uppercase tracking-widest border transition-all",
status === 'COMPLETED' ? "bg-emerald-50 text-emerald-600 border-emerald-100" :
status === 'APPROVED' ? "bg-brand-glow text-brand-indigo border-brand-indigo/10" :
status === 'PENDING' ? "bg-amber-50 text-amber-600 border-amber-100" : "bg-slate-50 text-slate-400 border-slate-100"
)}>
{status.replace('_', ' ')}
</div>
</motion.div>
);
})}
</div>
</div>
);
if (loading) {
return (
<div className="flex items-center justify-center p-20">
<Loader2 className="w-8 h-8 text-brand-indigo animate-spin" />
</div>
);
}
return (
<div className="space-y-8">
<div className="bg-brand-navy p-10 rounded-[3rem] text-white flex flex-col md:flex-row md:items-center justify-between gap-8 relative overflow-hidden">
<div className="relative z-10">
<h2 className="text-3xl font-black tracking-tight mb-2">Institutional Master Checklist</h2>
<p className="text-sm font-medium text-white/60 flex items-center gap-2">
<ShieldCheck className="w-4 h-4 text-emerald-400" />
Strategic event tracking for Academic Year {currentAcademicYear}
</p>
</div>
<div className="relative z-10 flex items-center gap-4">
<div className="px-5 py-3 bg-white/10 rounded-2xl border border-white/10">
<span className="text-[10px] font-black uppercase tracking-[0.2em]">Principal Desk Control</span>
</div>
</div>
<div className="absolute top-0 right-0 w-64 h-64 bg-brand-indigo/20 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
</div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
<SemesterCard title="ODD Semester Milestones" events={oddSemesterEvents} />
<SemesterCard title="EVEN Semester Milestones" events={evenSemesterEvents} />
</div>
</div>
);
};

View File

@@ -0,0 +1,611 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Users,
UserPlus,
Edit2,
Trash2,
X,
Check,
Search,
Shield,
Mail,
Building2,
Lock,
Zap,
GraduationCap
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { CLUBS } from '../../constants/clubs';
import { Pagination } from './Pagination';
interface User {
id: number;
fullName: string;
email: string;
role: string;
department: string;
password?: string;
isClubCoordinator?: boolean;
isPlacementStaff?: boolean;
isClassIncharge?: boolean;
inchargeClass?: string;
inchargeBatch?: string;
inchargeSection?: string;
classStrength?: number;
assignedClubs?: string[];
}
export const UserManagement: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [classes, setClasses] = useState<any[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [formData, setFormData] = useState({
fullName: '',
email: '',
role: 'FACULTY',
department: '',
password: '',
isClubCoordinator: false,
isPlacementStaff: false,
isClassIncharge: false,
inchargeClass: '',
inchargeBatch: '',
inchargeSection: '',
classStrength: 0,
assignedClubs: [] as string[]
});
useEffect(() => {
fetchUsers();
fetchClasses();
}, []);
const fetchClasses = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/classes');
if (response.ok) {
setClasses(await response.json());
}
} catch (error) {
console.error('Failed to fetch classes:', error);
}
};
const fetchUsers = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/admin/users');
if (response.ok) {
const data = await response.json();
setUsers(data);
}
} catch (error) {
console.error('Failed to fetch users:', error);
} finally {
setIsLoading(false);
}
};
const handleOpenModal = (user: User | null = null) => {
if (user) {
setEditingUser(user);
setFormData({
fullName: user.fullName,
email: user.email,
role: user.role,
department: user.department || '',
password: '',
isClubCoordinator: user.isClubCoordinator || false,
isPlacementStaff: user.isPlacementStaff || false,
isClassIncharge: user.isClassIncharge || false,
inchargeClass: user.inchargeClass || '',
inchargeBatch: user.inchargeBatch || '',
inchargeSection: user.inchargeSection || '',
classStrength: user.classStrength || 0,
assignedClubs: user.assignedClubs || []
});
} else {
setEditingUser(null);
setFormData({
fullName: '',
email: '',
role: 'FACULTY',
department: '',
password: '',
isClubCoordinator: false,
isPlacementStaff: false,
isClassIncharge: false,
inchargeClass: '',
inchargeBatch: '',
inchargeSection: '',
classStrength: 0,
assignedClubs: []
});
}
setIsModalOpen(true);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const url = editingUser
? `${API_BASE_URL}/api/admin/users/${editingUser.id}`
: API_BASE_URL + '/api/admin/users';
const method = editingUser ? 'PUT' : 'POST';
try {
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
fetchUsers();
setIsModalOpen(false);
}
} catch (error) {
console.error('Operation failed:', error);
}
};
const handleDelete = async (id: number) => {
if (window.confirm('Are you sure you want to delete this user?')) {
try {
const response = await fetch(`${API_BASE_URL}/api/admin/users/${id}`, {
method: 'DELETE'
});
if (response.ok) {
setUsers(prev => prev.filter(u => u.id !== id));
}
} catch (error) {
console.error('Delete failed:', error);
}
}
};
const filteredUsers = users
.filter(u =>
u.fullName.toLowerCase().includes(searchTerm.toLowerCase()) ||
u.email.toLowerCase().includes(searchTerm.toLowerCase())
)
.sort((a, b) => b.id - a.id);
const currentUsers = filteredUsers.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-8">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">User Management</h2>
<p className="text-text-muted font-medium">Add and manage institutional roles and permissions.</p>
</div>
<button
onClick={() => handleOpenModal()}
className="flex items-center gap-2 bg-brand-navy text-white px-6 py-3 rounded-xl font-black text-[10px] uppercase tracking-widest premium-shadow hover:scale-105 transition-all"
>
<UserPlus className="w-4 h-4" />
Add New User
</button>
</div>
<div className="bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden">
<div className="p-6 border-b border-slate-50 flex items-center justify-between gap-4">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="text"
placeholder="Search users by name or email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-slate-50 border-transparent rounded-xl py-2 pl-10 pr-4 text-sm focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
<div className="text-[10px] font-black uppercase tracking-widest text-text-muted">
Total Users: {users.length}
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-slate-50/50">
<th className="px-8 py-5 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">User Info</th>
<th className="px-8 py-5 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Role</th>
<th className="px-8 py-5 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Department</th>
<th className="px-8 py-5 text-[10px] font-black uppercase tracking-[0.2em] text-text-muted">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{currentUsers.map((user) => (
<tr key={user.id} className="group hover:bg-slate-50/50 transition-colors">
<td className="px-8 py-5">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-brand-glow flex items-center justify-center text-brand-indigo font-black text-xs">
{user.fullName.split(' ').map(n => n[0]).join('').substring(0, 2)}
</div>
<div className="flex flex-col">
<span className="font-bold text-text-dark text-sm">{user.fullName}</span>
<span className="text-xs text-text-muted">{user.email}</span>
</div>
</div>
</td>
<td className="px-8 py-5">
<div className="flex flex-col gap-2 items-start">
<span className={cn(
"px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border",
user.role === 'ADMIN' ? 'bg-purple-50 text-purple-600 border-purple-100' :
user.role === 'HOD' ? 'bg-amber-50 text-amber-600 border-amber-100' :
user.role === 'PRINCIPAL' ? 'bg-emerald-50 text-emerald-600 border-emerald-100' :
'bg-blue-50 text-blue-600 border-blue-100'
)}>
{user.role}
</span>
{user.isClassIncharge && (
<span className="px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border bg-indigo-50 text-indigo-600 border-indigo-100 flex items-center gap-1">
<GraduationCap className="w-3 h-3" />
Class Incharge ({user.inchargeClass} {user.inchargeBatch} - Sec {user.inchargeSection})
</span>
)}
</div>
</td>
<td className="px-8 py-5 text-sm font-bold text-text-secondary">
{user.department || 'N/A'}
</td>
<td className="px-8 py-5">
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleOpenModal(user)}
className="p-2 hover:bg-white rounded-lg hover:premium-shadow-sm transition-all text-slate-400 hover:text-brand-indigo"
>
<Edit2 className="w-4 h-4" />
</button>
{user.role !== 'ADMIN' && (
<button
onClick={() => handleDelete(user.id)}
className="p-2 hover:bg-white rounded-lg hover:premium-shadow-sm transition-all text-slate-400 hover:text-status-danger"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Pagination
currentPage={currentPage}
totalItems={filteredUsers.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 20, 50]}
/>
</div>
{/* User Modal */}
<AnimatePresence>
{isModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsModalOpen(false)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-lg bg-white rounded-[2.5rem] premium-shadow overflow-hidden"
>
<form onSubmit={handleSubmit}>
<div className="p-8 bg-brand-navy text-white flex justify-between items-start">
<div>
<h3 className="text-2xl font-black tracking-tight">
{editingUser ? 'Edit User' : 'Add New User'}
</h3>
<p className="text-white/60 text-xs font-medium mt-1">Configure institutional access credentials.</p>
</div>
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="p-2 hover:bg-white/10 rounded-xl transition-all"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="p-8 space-y-5">
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Full Name</label>
<div className="relative">
<Shield className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="text"
required
value={formData.fullName}
onChange={(e) => setFormData({...formData, fullName: e.target.value})}
placeholder="E.g. Dr. Jane Smith"
className="w-full bg-slate-50 border-transparent rounded-xl py-3 pl-12 pr-4 text-sm focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
</div>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Email Address</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
placeholder="jane.smith@rit.edu"
className="w-full bg-slate-50 border-transparent rounded-xl py-3 pl-12 pr-4 text-sm focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Role</label>
<select
value={formData.role}
onChange={(e) => setFormData({...formData, role: e.target.value})}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
<option value="FACULTY">Faculty</option>
<option value="HOD">HoD</option>
<option value="PRINCIPAL">Principal</option>
<option value="PLACEMENT">Placement</option>
<option value="ADMIN">Admin</option>
</select>
</div>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Department</label>
<select
required
value={formData.department}
onChange={(e) => setFormData({...formData, department: e.target.value})}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
<option value="">Select Department</option>
{[
"AI&DS", "AI&ML", "CSE", "CCE", "CSBS", "ECE",
"MECH", "EE(VLSI)", "BIOTECH", "Placement Department",
"H&S Dept", "Club", "Centre"
].map(dept => (
<option key={dept} value={dept}>{dept}</option>
))}
</select>
</div>
</div>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">
{editingUser ? 'New Password (Optional)' : 'Password'}
</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
<input
type="password"
required={!editingUser}
value={formData.password}
onChange={(e) => setFormData({...formData, password: e.target.value})}
placeholder="••••••••"
className="w-full bg-slate-50 border-transparent rounded-xl py-3 pl-12 pr-4 text-sm focus:bg-white focus:border-brand-indigo transition-all"
/>
</div>
</div>
<div className="pt-2 grid grid-cols-2 gap-4">
<div className="flex items-center justify-between p-4 bg-slate-50/50 rounded-2xl border border-slate-100">
<div className="flex items-center gap-3">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
formData.isClubCoordinator ? "bg-brand-indigo text-white" : "bg-slate-200 text-text-muted"
)}>
<Shield className="w-5 h-5" />
</div>
<div>
<p className="text-[11px] font-black text-text-dark leading-tight">Club Coordinator</p>
<p className="text-[9px] font-bold text-text-muted">Proposals Permission</p>
</div>
</div>
<button
type="button"
onClick={() => setFormData({...formData, isClubCoordinator: !formData.isClubCoordinator})}
className={cn(
"w-10 h-5 rounded-full p-1 transition-all duration-300",
formData.isClubCoordinator ? "bg-brand-indigo" : "bg-slate-300"
)}
>
<div className={cn(
"w-3 h-3 bg-white rounded-full transition-all duration-300 transform",
formData.isClubCoordinator ? "translate-x-5" : "translate-x-0"
)} />
</button>
</div>
<div className="flex items-center justify-between p-4 bg-slate-50/50 rounded-2xl border border-slate-100">
<div className="flex items-center gap-3">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
formData.isPlacementStaff ? "bg-brand-indigo text-white" : "bg-slate-200 text-text-muted"
)}>
<Zap className="w-5 h-5" />
</div>
<div>
<p className="text-[11px] font-black text-text-dark leading-tight">Placement Coordinator</p>
<p className="text-[9px] font-bold text-text-muted">High Priority Access</p>
</div>
</div>
<button
type="button"
onClick={() => setFormData({...formData, isPlacementStaff: !formData.isPlacementStaff})}
className={cn(
"w-10 h-5 rounded-full p-1 transition-all duration-300",
formData.isPlacementStaff ? "bg-brand-indigo" : "bg-slate-300"
)}
>
<div className={cn(
"w-3 h-3 bg-white rounded-full transition-all duration-300 transform",
formData.isPlacementStaff ? "translate-x-5" : "translate-x-0"
)} />
</button>
</div>
</div>
{formData.role === 'FACULTY' && (
<div className="pt-2">
<div className="flex items-center justify-between p-4 bg-slate-50/50 rounded-2xl border border-slate-100">
<div className="flex items-center gap-3">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center transition-all",
formData.isClassIncharge ? "bg-brand-indigo text-white" : "bg-slate-200 text-text-muted"
)}>
<GraduationCap className="w-5 h-5" />
</div>
<div>
<p className="text-[11px] font-black text-text-dark leading-tight">Class Incharge</p>
<p className="text-[9px] font-bold text-text-muted">Manage Class Operations</p>
</div>
</div>
<button
type="button"
onClick={() => setFormData({...formData, isClassIncharge: !formData.isClassIncharge})}
className={cn(
"w-10 h-5 rounded-full p-1 transition-all duration-300",
formData.isClassIncharge ? "bg-brand-indigo" : "bg-slate-300"
)}
>
<div className={cn(
"w-3 h-3 bg-white rounded-full transition-all duration-300 transform",
formData.isClassIncharge ? "translate-x-5" : "translate-x-0"
)} />
</button>
</div>
{formData.isClassIncharge && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="mt-4 space-y-4"
>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Class / Department</label>
<select
required={formData.isClassIncharge}
value={formData.inchargeClass}
onChange={(e) => setFormData({...formData, inchargeClass: e.target.value, inchargeBatch: '', inchargeSection: ''})}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
<option value="">Select Class</option>
{Array.from(new Set(classes.map(c => c.department))).map(dept => (
<option key={dept as string} value={dept as string}>{dept as string}</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Batch / Year</label>
<select
required={formData.isClassIncharge}
value={formData.inchargeBatch}
onChange={(e) => setFormData({...formData, inchargeBatch: e.target.value, inchargeSection: ''})}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
<option value="">Select Batch</option>
{Array.from(new Set(classes.filter(c => c.department === formData.inchargeClass).map(c => c.academicYear))).map(year => (
<option key={year as string} value={year as string}>{year as string}</option>
))}
</select>
</div>
<div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-2 block">Section</label>
<select
required={formData.isClassIncharge}
value={formData.inchargeSection}
onChange={(e) => setFormData({...formData, inchargeSection: e.target.value})}
className="w-full bg-slate-50 border-transparent rounded-xl py-3 px-4 text-sm focus:bg-white focus:border-brand-indigo transition-all appearance-none"
>
<option value="">Select Section</option>
{(classes.find(c => c.department === formData.inchargeClass && c.academicYear === formData.inchargeBatch)?.sections || []).map((sec: string) => (
<option key={sec} value={sec}>{sec}</option>
))}
</select>
</div>
</div>
</motion.div>
)}
</div>
)}
{formData.isClubCoordinator && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="mt-4 space-y-3"
>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted block">Assigned Clubs</label>
<div className="flex flex-wrap gap-2 p-4 bg-slate-50/50 rounded-2xl border border-slate-100 min-h-[60px]">
{CLUBS.map(club => (
<button
key={club}
type="button"
onClick={() => {
const updated = formData.assignedClubs.includes(club)
? formData.assignedClubs.filter(c => c !== club)
: [...formData.assignedClubs, club];
setFormData({...formData, assignedClubs: updated});
}}
className={cn(
"px-3 py-1.5 rounded-lg text-[9px] font-black uppercase tracking-widest transition-all border",
formData.assignedClubs.includes(club)
? "bg-brand-navy text-white border-brand-navy premium-shadow-sm"
: "bg-white text-text-muted border-slate-200 hover:border-brand-indigo/30"
)}
>
{club}
</button>
))}
</div>
</motion.div>
)}
<div className="pt-4">
<button
type="submit"
className="w-full bg-brand-navy text-white rounded-xl py-4 font-black text-[10px] uppercase tracking-widest premium-shadow hover:scale-[1.02] active:scale-[0.98] transition-all"
>
{editingUser ? 'Update User Credentials' : 'Create System User'}
</button>
</div>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};

View File

@@ -0,0 +1,289 @@
import { API_BASE_URL } from '../../lib/config';
import React, { useState, useEffect } from 'react';
import { Calendar as CalendarIcon, MapPin, Clock, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { cn } from '../../lib/utils';
import { format } from 'date-fns';
import { startOfMonth } from 'date-fns';
import { endOfMonth } from 'date-fns';
import { startOfWeek } from 'date-fns';
import { endOfWeek } from 'date-fns';
import { eachDayOfInterval } from 'date-fns';
import { isSameMonth } from 'date-fns';
import { isSameDay } from 'date-fns';
import { addMonths } from 'date-fns';
import { subMonths } from 'date-fns';
interface Event {
id: number;
title: string;
type: string;
status: string;
department: string;
startDate: string;
endDate: string;
location: string;
academicYears: string[];
}
const CORE_VENUES = [
'GB 4th floor auditorium',
'Wozniak Auditorium',
'C6-02 Indoor Theatre',
'H Block Guest Lecture Theatre',
'Steve Jobs Computer Centre 1',
'Steve Jobs Computer Centre 2'
];
export const VenueTimeline: React.FC<{ compact?: boolean }> = ({ compact }) => {
const [selectedDate, setSelectedDate] = useState(new Date());
const [currentMonth, setCurrentMonth] = useState(new Date());
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
setEvents(data.filter((e: any) =>
e.status === 'APPROVED' || e.status === 'COMPLETED'
));
}
} catch (error) {
console.error('Failed to fetch events for timeline:', error);
} finally {
setLoading(false);
}
};
fetchEvents();
}, []);
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const eventsOnSelectedDate = events.filter(event => {
const eventDate = parseDate(event.startDate);
if (!eventDate) return false;
return format(eventDate, 'yyyy-MM-dd') === format(selectedDate, 'yyyy-MM-dd');
});
// Dynamically discover all unique venues from events on the selected date and combine with CORE_VENUES
const allVenues = (() => {
const dynamicVenues = eventsOnSelectedDate
.map(e => e.location?.trim())
.filter(l => l && l !== 'N/A' && l !== 'Others.') as string[];
const finalVenues = [...CORE_VENUES];
dynamicVenues.forEach(dv => {
if (!finalVenues.some(cv => cv.toLowerCase() === dv.toLowerCase())) {
finalVenues.push(dv);
}
});
return finalVenues;
})();
const calendarDays = (() => {
const start = startOfWeek(startOfMonth(currentMonth));
const end = endOfWeek(endOfMonth(currentMonth));
return eachDayOfInterval({ start, end });
})();
if (loading) {
return (
<div className="flex items-center justify-center p-20">
<Loader2 className="w-8 h-8 text-brand-indigo animate-spin" />
<span className="ml-3 text-sm font-black text-brand-indigo uppercase tracking-widest tracking-widest">Initialising Timeline Audit...</span>
</div>
);
}
return (
<div className={cn(
"flex gap-8 items-start",
compact ? "flex-col" : "flex-col lg:flex-row min-h-[600px]"
)}>
{/* Sidebar Filter - STICKY */}
{!compact && (
<div className="w-full lg:w-80 space-y-6 sticky top-24 self-start">
<div className="bg-white p-8 rounded-[2.5rem] border border-slate-100 premium-shadow">
<div className="flex items-center gap-4 mb-8">
<div className="w-10 h-10 bg-brand-indigo rounded-2xl flex items-center justify-center text-white premium-shadow-sm">
<CalendarIcon className="w-5 h-5" />
</div>
<div>
<p className="text-[10px] font-black uppercase tracking-widest text-text-muted">Timeline Filter</p>
<h3 className="text-lg font-black text-text-dark tracking-tight">View Date</h3>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between px-2">
<h4 className="text-sm font-black text-text-dark">{format(currentMonth, 'MMMM yyyy')}</h4>
<div className="flex gap-2">
<button onClick={() => setCurrentMonth(subMonths(currentMonth, 1))} className="p-1 hover:bg-slate-50 rounded-lg transition-colors">
<ChevronLeft className="w-4 h-4 text-text-muted" />
</button>
<button onClick={() => setCurrentMonth(addMonths(currentMonth, 1))} className="p-1 hover:bg-slate-50 rounded-lg transition-colors">
<ChevronRight className="w-4 h-4 text-text-muted" />
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-1">
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => (
<div key={`${day}-${i}`} className="text-[10px] font-black text-text-muted text-center py-2">{day}</div>
))}
{calendarDays.map(day => {
const hasEvents = events.some(e =>
e.startDate && format(new Date(e.startDate), 'yyyy-MM-dd') === format(day, 'yyyy-MM-dd')
);
const isSelected = isSameDay(day, selectedDate);
return (
<button
key={day.toISOString()}
onClick={() => setSelectedDate(day)}
className={cn(
"aspect-square flex items-center justify-center text-xs font-bold rounded-xl transition-all relative group",
!isSameMonth(day, currentMonth) && "text-slate-200",
isSelected ? "bg-brand-indigo text-white premium-shadow-sm" : "hover:bg-slate-50 text-text-dark",
isSameDay(day, new Date()) && !isSelected && "text-brand-indigo underline decoration-2 underline-offset-4"
)}
>
{format(day, 'd')}
{hasEvents && (
<div className={cn(
"absolute bottom-1 w-1 h-1 rounded-full",
isSelected ? "bg-white/40" : "bg-brand-indigo/30"
)} />
)}
</button>
);
})}
</div>
</div>
<div className="mt-8 p-4 bg-brand-glow/30 rounded-2xl border border-brand-indigo/10">
<p className="text-[9px] font-bold text-brand-indigo/80 leading-relaxed italic">
Tip: This calendar specifically filters the venue bookings timeline below.
</p>
</div>
</div>
</div>
)}
<div className="flex-1 space-y-6">
{!compact && (
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tighter">Institutional Timeline</h2>
<p className="text-sm font-bold text-text-muted mt-1">
Confirmed slots for <span className="text-brand-indigo">{format(selectedDate, 'MMMM d, yyyy')}</span>
</p>
</div>
<div className="flex items-center gap-2 px-4 py-2 bg-emerald-50 text-emerald-600 rounded-full border border-emerald-100">
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse" />
<span className="text-[10px] font-black uppercase tracking-widest">Approved Only</span>
</div>
</div>
)}
<div className="space-y-6">
{allVenues.map((venue, idx) => {
const venueEvents = eventsOnSelectedDate.filter(e =>
e.location?.trim().toLowerCase() === venue.trim().toLowerCase()
);
return (
<motion.div
key={venue}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.05 }}
className={cn(
"bg-white rounded-[2.5rem] border border-slate-100 premium-shadow overflow-hidden",
compact && "rounded-[1.5rem] premium-shadow-sm"
)}
>
<div className={cn(
"p-8 border-b border-slate-50 bg-slate-50/30 flex items-center gap-4",
compact && "p-4"
)}>
<div className={cn(
"w-12 h-12 bg-white rounded-2xl flex items-center justify-center premium-shadow-sm border border-slate-50",
compact && "w-8 h-8 rounded-xl"
)}>
<MapPin className={cn("text-slate-300", compact ? "w-4 h-4" : "w-6 h-6")} />
</div>
<h3 className={cn("font-black text-text-dark tracking-tight", compact ? "text-sm" : "text-xl")}>{venue}</h3>
</div>
<div className={cn("p-8", compact && "p-4")}>
{venueEvents.length > 0 ? (
<div className="space-y-4">
{venueEvents.map(event => (
<div key={event.id} className={cn(
"flex flex-col md:flex-row md:items-center justify-between gap-6 bg-slate-50/50 border border-slate-100 rounded-3xl hover:bg-white hover:premium-shadow transition-all group",
compact ? "p-4" : "p-6"
)}>
<div className="flex items-center gap-4">
<div className={cn(
"bg-white rounded-2xl flex items-center justify-center text-[10px] font-black text-brand-indigo border border-brand-indigo/10",
compact ? "w-8 h-8 rounded-xl text-[8px]" : "w-12 h-12"
)}>
{event.department === 'AI&DS' ? 'AIDS' : event.department?.slice(0, 4).toUpperCase()}
</div>
<div>
<h4 className={cn("font-black text-text-dark group-hover:text-brand-indigo transition-colors", compact ? "text-xs" : "text-lg")}>{event.title}</h4>
{!compact && (
<div className="flex items-center gap-3 mt-1">
<span className="text-[10px] font-black uppercase tracking-widest text-text-muted">{event.department}</span>
<span className="text-[10px] font-black uppercase tracking-widest text-slate-300"></span>
<span className="text-[10px] font-black uppercase tracking-widest text-brand-indigo/60">{event.type}</span>
</div>
)}
</div>
</div>
<div className={cn(
"flex items-center gap-4 bg-white rounded-2xl border border-slate-100 premium-shadow-sm",
compact ? "px-3 py-2" : "px-6 py-4"
)}>
<Clock className={cn("text-brand-indigo", compact ? "w-3 h-3" : "w-4 h-4")} />
<div className="flex flex-col">
<p className="text-[8px] font-black uppercase tracking-widest text-text-muted mb-0.5">Slot</p>
<p className={cn("font-black text-text-dark", compact ? "text-[10px]" : "text-sm")}>
{parseDate(event.startDate) ? format(parseDate(event.startDate)!, 'hh:mm a') : 'TBD'}
</p>
</div>
</div>
</div>
))}
</div>
) : (
<div className={cn(
"flex flex-col items-center justify-center border-2 border-dashed border-slate-50 rounded-3xl",
compact ? "py-4" : "py-12"
)}>
<p className="text-[10px] font-black uppercase tracking-[0.3em] text-slate-200">
{compact ? 'Available' : 'Available for Booking'}
</p>
</div>
)}
</div>
</motion.div>
);
})}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,166 @@
import React, { useState, useMemo } from 'react';
import { motion } from 'framer-motion';
import { MapPin, Clock, Calendar as CalendarIcon, ArrowRight, Building2 } from 'lucide-react';
import { cn } from '../../lib/utils';
import { format, isSameDay } from 'date-fns';
import { EventDetailsModal } from './EventDetailsModal';
const CORE_VENUES = [
'GB 4th floor auditorium',
'Wozniak Auditorium',
'C6-02 Indoor Theatre',
'H Block Guest Lecture Theatre',
'Steve Jobs Computer Centre 1',
'Steve Jobs Computer Centre 2'
];
interface VenuesAtGlanceProps {
events: any[];
}
export const VenuesAtGlance: React.FC<VenuesAtGlanceProps> = ({ events }) => {
const [selectedEvent, setSelectedEvent] = useState<any | null>(null);
const parseDate = (dateSource: any): Date | null => {
if (!dateSource) return null;
if (typeof dateSource === 'string') return new Date(dateSource);
if (Array.isArray(dateSource)) {
return new Date(dateSource[0], dateSource[1] - 1, dateSource[2], dateSource[3] || 0, dateSource[4] || 0);
}
return null;
};
const today = new Date();
// Combine core venues with any dynamically used venues in events today
const allVenues = useMemo(() => {
const todayEvents = events.filter(e => {
if (e.status !== 'APPROVED' && e.status !== 'COMPLETED') return false;
const eventDate = parseDate(e.startDate);
if (!eventDate) return false;
return isSameDay(eventDate, today);
});
const dynamicVenues = todayEvents
.filter(e => e.location && e.location !== 'N/A' && e.location !== 'Others.')
.map(e => e.location.trim());
const finalVenues = [...CORE_VENUES];
dynamicVenues.forEach(dv => {
if (!finalVenues.some(cv => cv.toLowerCase() === dv.toLowerCase())) {
finalVenues.push(dv);
}
});
return finalVenues;
}, [events]);
return (
<div className="card-widget">
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-xl font-black text-brand-navy tracking-tight flex items-center gap-2">
<MapPin className="w-5 h-5 text-brand-indigo" />
Venues At A Glance
</h3>
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400 mt-1">
Today's Scheduled Events • {format(today, 'MMMM d, yyyy')}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{allVenues.map((venue, idx) => {
// Find if there is an approved/completed event for this venue today
const todayEvents = events.filter(e => {
if (e.status !== 'APPROVED' && e.status !== 'COMPLETED') return false;
const eventDate = parseDate(e.startDate);
if (!eventDate) return false;
return isSameDay(eventDate, today) && e.location?.trim().toLowerCase() === venue.trim().toLowerCase();
});
const isOccupied = todayEvents.length > 0;
return (
<motion.div
key={venue}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
className={cn(
"rounded-[1.5rem] p-5 border transition-all relative overflow-hidden",
isOccupied
? "bg-slate-50 border-slate-200 hover:border-brand-indigo/30 premium-shadow-sm group cursor-pointer"
: "bg-white border-slate-100 hover:bg-slate-50 opacity-70 hover:opacity-100"
)}
onClick={() => isOccupied && setSelectedEvent(todayEvents[0])}
>
{isOccupied && (
<div className="absolute top-0 right-0 w-16 h-16 bg-brand-indigo/5 rounded-bl-[100%] transition-transform group-hover:scale-110" />
)}
<div className="flex items-start justify-between gap-4 mb-4 relative z-10">
<div className="flex items-center gap-3">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center shrink-0 transition-colors",
isOccupied ? "bg-white text-brand-indigo shadow-sm" : "bg-slate-50 text-slate-400"
)}>
<Building2 className="w-5 h-5" />
</div>
<div>
<h4 className="text-sm font-black text-brand-navy tracking-tight line-clamp-1" title={venue}>
{venue}
</h4>
<span className={cn(
"text-[9px] font-black uppercase tracking-widest",
isOccupied ? "text-emerald-500" : "text-slate-400"
)}>
{isOccupied ? 'Occupied' : 'Available'}
</span>
</div>
</div>
</div>
{isOccupied ? (
<div className="space-y-3 relative z-10">
<div className="bg-white rounded-xl p-3 border border-slate-100 shadow-sm transition-transform group-hover:-translate-y-1">
<p className="text-xs font-bold text-brand-navy line-clamp-1 mb-2">
{todayEvents[0].title}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-text-muted">
<Clock className="w-3.5 h-3.5 text-brand-indigo" />
<span className="text-[10px] font-black uppercase tracking-widest">
{format(parseDate(todayEvents[0].startDate)!, 'hh:mm a')}
</span>
</div>
<div className="w-6 h-6 rounded-lg bg-brand-glow flex items-center justify-center text-brand-indigo group-hover:bg-brand-indigo group-hover:text-white transition-colors">
<ArrowRight className="w-3 h-3" />
</div>
</div>
</div>
{todayEvents.length > 1 && (
<p className="text-[9px] font-bold text-brand-indigo/60 text-right">
+ {todayEvents.length - 1} more today
</p>
)}
</div>
) : (
<div className="h-16 flex items-center justify-center border border-dashed border-slate-200 rounded-xl">
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-300">
No Events
</p>
</div>
)}
</motion.div>
);
})}
</div>
<EventDetailsModal
isOpen={!!selectedEvent}
onClose={() => setSelectedEvent(null)}
event={selectedEvent}
/>
</div>
);
};

View File

@@ -0,0 +1,19 @@
export const CLUBS = [
'General',
'UBA',
'YUVA Club',
'National Service Scheme (NSS)',
'Youth Red Cross(YRC)',
'Rotaract Club',
'Women Empowerment Club',
'Techsparks Club',
'Infinitus',
'STEAM Club',
'Fusion Language Club',
'Artist League',
'Photography Club',
'Podcast Club',
'Nippon Club',
'Telgu Club',
'Mediastic Hub'
];

View File

@@ -0,0 +1,32 @@
export interface InstitutionalEvent {
id: string;
name: string;
month: string;
semester: 'ODD' | 'EVEN';
}
export const INSTITUTIONAL_EVENTS: InstitutionalEvent[] = [
// EVEN Semester (Jan - June)
{ id: 'ie-1', name: 'New Year & Christmas Celebration', month: 'January', semester: 'EVEN' },
{ id: 'ie-2', name: 'Republic Day Celebration', month: 'January', semester: 'EVEN' },
{ id: 'ie-3', name: 'Pongal Celebration', month: 'January', semester: 'EVEN' },
{ id: 'ie-4', name: 'Yatra Banner Release', month: 'January', semester: 'EVEN' },
{ id: 'ie-5', name: 'Yatra Ethnic Day', month: 'February', semester: 'EVEN' },
{ id: 'ie-6', name: 'Yatra', month: 'February', semester: 'EVEN' },
{ id: 'ie-7', name: 'International Womens Day', month: 'March', semester: 'EVEN' },
{ id: 'ie-8', name: 'Ugathi', month: 'March', semester: 'EVEN' },
{ id: 'ie-9', name: 'Tamil New Year', month: 'April', semester: 'EVEN' },
// ODD Semester (July - Dec)
{ id: 'ie-10', name: 'Techritz', month: 'July', semester: 'ODD' },
{ id: 'ie-11', name: 'Independence Day', month: 'August', semester: 'ODD' },
{ id: 'ie-12', name: 'AI Horizon Week', month: 'August', semester: 'ODD' },
{ id: 'ie-13', name: 'Initiation Day', month: 'September', semester: 'ODD' },
{ id: 'ie-14', name: 'Onam Celebration', month: 'September', semester: 'ODD' },
{ id: 'ie-15', name: 'Teachers Day', month: 'September', semester: 'ODD' },
{ id: 'ie-16', name: 'Engineers Day', month: 'September', semester: 'ODD' },
{ id: 'ie-17', name: 'SDG Golu', month: 'September', semester: 'ODD' },
{ id: 'ie-18', name: 'Ayutha Puja', month: 'October', semester: 'ODD' },
{ id: 'ie-19', name: 'Tech Fest IIT Bombay', month: 'October', semester: 'ODD' },
{ id: 'ie-20', name: 'EDI Conclave - FICCI Flow', month: 'November', semester: 'ODD' },
];

View File

@@ -0,0 +1,61 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
interface User {
id: number;
email: string;
fullName: string;
role: 'FACULTY' | 'HOD' | 'PRINCIPAL' | 'ADMIN' | 'PLACEMENT';
department: string;
isClubCoordinator?: boolean;
isPlacementStaff?: boolean;
isClassIncharge?: boolean;
inchargeClass?: string;
inchargeBatch?: string;
inchargeSection?: string;
classStrength?: number;
assignedClubs?: string[];
}
interface AuthContextType {
user: User | null;
login: (userData: User) => void;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const savedUser = localStorage.getItem('ems_user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
}, []);
const login = (userData: User) => {
setUser(userData);
localStorage.setItem('ems_user', JSON.stringify(userData));
};
const logout = () => {
setUser(null);
localStorage.removeItem('ems_user');
};
return (
<AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -0,0 +1,71 @@
@import "tailwindcss";
@import "./App.css";
@theme {
--color-brand-indigo: #4f46e5;
--color-brand-navy: #0f4475;
--color-brand-glow: rgba(79, 70, 229, 0.1);
--color-surface-background: #f8fafc;
--color-surface-white: #ffffff;
--color-surface-input: #f9fafb;
--color-text-dark: #0f172a;
--color-text-primary: #1e293b;
--color-text-secondary: #475569;
--color-text-muted: #94a3b8;
--color-status-success: #059669;
--color-status-danger: #dc2626;
--color-status-warning: #d97706;
--font-sans: "Inter", sans-serif;
--radius-4xl: 2rem;
--radius-5xl: 2.5rem;
--radius-6xl: 3.5rem;
--shadow-premium: 0 20px 25px -5px rgba(0, 0, 0, 0.05), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
--shadow-widget: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
}
@layer base {
body {
@apply bg-surface-background text-text-primary font-sans antialiased;
}
}
@layer components {
.glass-effect {
@apply bg-white/80 backdrop-blur-md border border-white/20;
}
.premium-shadow {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.premium-shadow-sm {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.02);
}
.sidebar-item {
@apply flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 text-text-secondary font-semibold hover:bg-brand-glow hover:text-brand-indigo cursor-pointer;
}
.sidebar-item-active {
@apply bg-brand-glow text-brand-indigo;
}
.card-widget {
@apply bg-white rounded-[2rem] p-6 border border-slate-100 shadow-sm hover:shadow-md transition-shadow duration-300;
}
.floating-sidebar {
@apply fixed left-4 top-4 bottom-4 w-64 bg-white rounded-[2.5rem] border border-slate-200/60 shadow-xl z-50 flex flex-col p-6;
}
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
@apply bg-slate-300 rounded-full;
}

View File

@@ -0,0 +1,3 @@
// Dynamic API Base URL based on the current hostname
// This allows access from localhost as well as other devices on the same network
export const API_BASE_URL = `http://${window.location.hostname}:8081`;

View File

@@ -0,0 +1,13 @@
export const getCurrentAcademicYear = () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth(); // 0-11
// If we are in June or later, the academic year is currentYear-(currentYear+1)
// Otherwise, it's (currentYear-1)-currentYear
if (month >= 5) { // June is index 5
return `${year}-${(year + 1).toString().slice(-2)}`;
} else {
return `${year - 1}-${year.toString().slice(-2)}`;
}
};

View File

@@ -0,0 +1,781 @@
import { initializeApp, getApp, getApps } from 'firebase/app';
import {
getFirestore,
collection,
getDocs,
doc,
getDoc,
setDoc,
updateDoc,
deleteDoc
} from 'firebase/firestore';
const firebaseConfig = {
apiKey: "AIzaSyBdRUyA7LDtDReUA3TXDys71dSHgD2tOEA",
authDomain: "ems-ritchennai1.firebaseapp.com",
projectId: "ems-ritchennai1",
storageBucket: "ems-ritchennai1.firebasestorage.app",
messagingSenderId: "825363154108",
appId: "1:825363154108:web:7b6d3430aa3b696fef3cb9",
measurementId: "G-3W7ECQ52G1"
};
// Initialize Firebase App
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
const db = getFirestore(app);
// Helper: parse date strings into Date objects
function parseDate(val: any): Date {
if (!val) return new Date();
let str = String(val).trim().replace(" ", "T");
if (str.length === 10) str += "T00:00:00";
if (str.length === 16) str += ":00";
if (str.includes("+")) str = str.substring(0, str.indexOf("+"));
if (str.includes("Z")) str = str.replace("Z", "");
if (str.includes(".")) str = str.substring(0, str.indexOf("."));
return new Date(str);
}
// Helper: parse BodyInit body to JSON
function parseBody(body: any): any {
if (!body) return {};
try {
return JSON.parse(String(body));
} catch {
return {};
}
}
// Conflict checking logic
async function getConflictMessage(event: any, ignoreId: any = null): Promise<string | null> {
const start = parseDate(event.startDate || event.finalDate);
const end = parseDate(event.endDate || (event.startDate ? new Date(parseDate(event.startDate).getTime() + 2 * 60 * 60 * 1000) : null));
const location = String(event.location || event.venue || "").trim();
const institution = String(event.institution || "RIT").trim();
if (!location || !start || !end) return null;
try {
const colRef = collection(db, 'ems_events');
const snap = await getDocs(colRef);
const existingEvents = snap.docs.map(d => d.data());
for (const other of existingEvents) {
if (other.status === 'CANCELLED') continue;
if (ignoreId && String(other.id) === String(ignoreId)) continue;
if (event.id && String(other.id) === String(event.id)) continue;
if (event.groupRequestId && other.groupRequestId === event.groupRequestId) continue;
const oStart = parseDate(other.startDate);
const oEnd = parseDate(other.endDate);
const oLoc = String(other.location || "").trim();
const oInst = String(other.institution || "RIT").trim();
// Check overlap
if (
oInst === institution &&
oLoc.toLowerCase() === location.toLowerCase() &&
start < oEnd &&
end > oStart
) {
const timeFormatter = new Intl.DateTimeFormat('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
return `Venue Conflict: '${location}' is already booked for '${other.title}' from ${timeFormatter.format(oStart)} to ${timeFormatter.format(oEnd)}`;
}
}
} catch (err) {
console.error("Conflict checking error:", err);
}
return null;
}
// Generate sequential IDs
function generateNumericId(): number {
return Date.now() + Math.floor(Math.random() * 1000);
}
// Seeding logic
async function seedDatabaseIfEmpty() {
try {
const usersSnap = await getDocs(collection(db, 'ems_users'));
if (usersSnap.empty) {
console.log("[Firebase Backend] Seeding users...");
const defaultUsers = [
{ id: 1, email: "admin@rit.edu", password: "admin123", fullName: "System Administrator", role: "ADMIN", department: "ADMIN", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] },
{ id: 2, email: "faculty@rit.edu", password: "faculty123", fullName: "Dr. Faculty Member", role: "FACULTY", department: "CSE", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: true, inchargeClass: "CSE", inchargeBatch: "3rd Year", inchargeSection: "A", classStrength: 60, assignedClubs: [] },
{ id: 3, email: "hod@rit.edu", password: "hod123", fullName: "Prof. Head of Dept", role: "HOD", department: "AI&ML", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] },
{ id: 4, email: "principal@rit.edu", password: "principal123", fullName: "Dr. College Principal", role: "PRINCIPAL", department: "ADMIN", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] },
{ id: 5, email: "placement@rit.edu", password: "placement123", fullName: "Placement Coordinator", role: "PLACEMENT", department: "Placement Department", isClubCoordinator: false, isPlacementStaff: true, isClassIncharge: false, assignedClubs: [] },
{ id: 6, email: "admin2@rit.edu", password: "admin123", fullName: "Secondary Admin", role: "ADMIN", department: "ADMIN", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] },
{ id: 7, email: "principal2@rit.edu", password: "principal123", fullName: "Vice Principal", role: "PRINCIPAL", department: "ADMIN", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] },
{ id: 8, email: "hod_cse@rit.edu", password: "hod123", fullName: "CSE HOD", role: "HOD", department: "CSE", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] },
{ id: 9, email: "faculty2@rit.edu", password: "faculty123", fullName: "Assistant Professor CSE", role: "FACULTY", department: "CSE", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] }
];
for (const u of defaultUsers) {
await setDoc(doc(db, 'ems_users', String(u.id)), u);
}
}
const classesSnap = await getDocs(collection(db, 'ems_classes'));
if (classesSnap.empty) {
console.log("[Firebase Backend] Seeding classes...");
const defaultClasses = [
{ id: 1, institution: "RIT", department: "CSE", academicYear: "3rd Year", sections: ["A", "B"], status: "Ready" },
{ id: 2, institution: "RIT", department: "AI&ML", academicYear: "3rd Year", sections: ["A"], status: "Ready" },
{ id: 3, institution: "RIT", department: "ECE", academicYear: "2nd Year", sections: ["A", "B", "C"], status: "Ready" }
];
for (const c of defaultClasses) {
await setDoc(doc(db, 'ems_classes', String(c.id)), c);
}
}
const eventsSnap = await getDocs(collection(db, 'ems_events'));
if (eventsSnap.empty) {
console.log("[Firebase Backend] Seeding events...");
const venues = [
"GB 4th floor auditorium",
"Wozniak Auditorium",
"C6-02 Indoor Theatre",
"H Block Guest Lecture Theatre"
];
const categories = ["ACADEMIC", "CLUB", "PLACEMENT", "SPORTS"];
const types = ["Workshop", "Seminar", "Guest Lecture", "Competition"];
const statuses = ["REQUESTED", "APPROVED", "COMPLETED"];
let evId = 100;
const now = new Date();
for (let month = now.getMonth() - 2; month <= now.getMonth() + 2; month++) {
const targetMonth = (month + 12) % 12;
const targetYear = now.getFullYear() + (month < 0 ? -1 : (month > 11 ? 1 : 0));
for (const dept of ["CSE", "AI&ML"]) {
const start = new Date(targetYear, targetMonth, 15, 10, 0);
const end = new Date(targetYear, targetMonth, 15, 13, 0);
const venue = venues[Math.floor(Math.random() * venues.length)];
const cat = categories[Math.floor(Math.random() * categories.length)];
const type = types[Math.floor(Math.random() * types.length)];
const status = statuses[Math.floor(Math.random() * statuses.length)];
const event = {
id: evId++,
title: `${dept} ${type} - ${targetMonth + 1}/15`,
description: `Sample event for ${dept} department.`,
startDate: start.toISOString(),
endDate: end.toISOString(),
location: venue,
category: cat,
type: type,
institution: "RIT",
department: dept,
academicYears: ["3rd Year"],
status: status,
budget: 5000,
hasRegistrationFee: false,
registrationFee: 0,
proposer: { id: 2, fullName: "Dr. Faculty Member", email: "faculty@rit.edu", role: "FACULTY" }
};
await setDoc(doc(db, 'ems_events', String(event.id)), event);
}
}
}
} catch (err) {
console.error("Seeding failed:", err);
}
}
// Perform initial seed
seedDatabaseIfEmpty();
// Set up the fetch interceptor
const originalFetch = window.fetch;
window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const urlStr = typeof input === 'string' ? input : (input instanceof URL ? input.href : input.url);
// Only intercept endpoints containing '/api/'
if (!urlStr.includes('/api/')) {
return originalFetch(input, init);
}
const parsedUrl = new URL(urlStr, window.location.origin);
const path = parsedUrl.pathname;
const method = (init?.method || 'GET').toUpperCase();
const queryParams = parsedUrl.searchParams;
console.log(`[Firebase Interceptor] ${method} ${path}`);
// Helper response builder
const jsonResponse = (data: any, status = 200) => {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' }
});
};
try {
// ----------------------------------------------------
// AUTH CONTROLLER
// ----------------------------------------------------
if (path === '/api/auth/login' && method === 'POST') {
const { email, password } = parseBody(init?.body);
const snap = await getDocs(collection(db, 'ems_users'));
const userDoc = snap.docs.find(d => d.data().email?.toLowerCase() === email?.toLowerCase());
if (userDoc) {
const u = userDoc.data();
if (u.password === password) {
// Exclude password from response
const { password: _, ...userData } = u;
return jsonResponse({
...userData,
department: userData.department || "N/A",
assignedClubs: userData.assignedClubs || []
});
}
}
return jsonResponse({ message: "Invalid email or passcode" }, 401);
}
// ----------------------------------------------------
// ADMIN USER CONTROLLER
// ----------------------------------------------------
if (path === '/api/admin/users' && method === 'GET') {
const snap = await getDocs(collection(db, 'ems_users'));
return jsonResponse(snap.docs.map(d => d.data()));
}
if (path === '/api/admin/users' && method === 'POST') {
const payload = parseBody(init?.body);
const snap = await getDocs(collection(db, 'ems_users'));
if (snap.docs.some(d => d.data().email?.toLowerCase() === payload.email?.toLowerCase())) {
return jsonResponse({ message: "User with this email already exists" }, 400);
}
const newUser = {
...payload,
id: generateNumericId(),
email: payload.email?.trim().toLowerCase(),
fullName: payload.fullName?.trim(),
role: payload.role || 'FACULTY',
department: payload.department || 'H&S Dept',
assignedClubs: payload.assignedClubs || []
};
await setDoc(doc(db, 'ems_users', String(newUser.id)), newUser);
return jsonResponse(newUser);
}
if (path.startsWith('/api/admin/users/') && method === 'PUT') {
const parts = path.split('/');
const id = parts[parts.length - 1];
const payload = parseBody(init?.body);
const docRef = doc(db, 'ems_users', String(id));
const snap = await getDoc(docRef);
if (!snap.exists()) {
return jsonResponse({ message: "User not found" }, 404);
}
const updated = {
...snap.data(),
...payload,
id: snap.data().id // lock ID
};
await setDoc(docRef, updated);
return jsonResponse(updated);
}
if (path.startsWith('/api/admin/users/') && method === 'DELETE') {
const parts = path.split('/');
const id = parts[parts.length - 1];
await deleteDoc(doc(db, 'ems_users', String(id)));
return jsonResponse({ message: "User deleted successfully" });
}
// ----------------------------------------------------
// CLASSES CONTROLLER
// ----------------------------------------------------
if (path === '/api/classes' && method === 'GET') {
const snap = await getDocs(collection(db, 'ems_classes'));
return jsonResponse(snap.docs.map(d => d.data()));
}
if (path === '/api/classes' && method === 'POST') {
const payload = parseBody(init?.body);
const newClass = {
...payload,
id: generateNumericId(),
status: payload.status || "Ready"
};
await setDoc(doc(db, 'ems_classes', String(newClass.id)), newClass);
return jsonResponse(newClass);
}
if (path.startsWith('/api/classes/') && method === 'DELETE') {
const parts = path.split('/');
const id = parts[parts.length - 1];
await deleteDoc(doc(db, 'ems_classes', String(id)));
return jsonResponse({ message: "Class mapping deleted successfully" });
}
if (path === '/api/classes/promote' && method === 'POST') {
const inst = queryParams.get('institution') || '';
const snap = await getDocs(collection(db, 'ems_classes'));
const classes = snap.docs.map(d => d.data());
for (const cm of classes) {
if (cm.institution?.trim().toLowerCase() !== inst.trim().toLowerCase()) {
continue;
}
const ref = doc(db, 'ems_classes', String(cm.id));
switch (cm.academicYear) {
case "1st Year":
await updateDoc(ref, { academicYear: "2nd Year" });
break;
case "2nd Year":
await updateDoc(ref, { academicYear: "3rd Year" });
break;
case "3rd Year":
await updateDoc(ref, { academicYear: "4th Year" });
break;
case "4th Year":
await deleteDoc(ref);
break;
}
}
return jsonResponse({ message: `Academic year promotion completed for ${inst}` });
}
// ----------------------------------------------------
// NOTES CONTROLLER
// ----------------------------------------------------
if (path === '/api/notes' && method === 'GET') {
const snap = await getDocs(collection(db, 'ems_notes'));
const list = snap.docs.map(d => d.data());
// Sort desc
list.sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime());
return jsonResponse(list);
}
if (path === '/api/notes' && method === 'POST') {
const payload = parseBody(init?.body);
const newNote = {
...payload,
id: generateNumericId(),
createdAt: new Date().toISOString()
};
await setDoc(doc(db, 'ems_notes', String(newNote.id)), newNote);
return jsonResponse(newNote);
}
// ----------------------------------------------------
// EVENTS CONTROLLER
// ----------------------------------------------------
if (path === '/api/events' && method === 'GET') {
const snap = await getDocs(collection(db, 'ems_events'));
const events = snap.docs.map(d => d.data());
// Populate conflicts
for (const event of events) {
if (!['APPROVED', 'COMPLETED', 'CANCELLED'].includes(event.status)) {
const conflictMsg = await getConflictMessage(event);
if (conflictMsg) {
event.conflictMessage = conflictMsg;
}
}
}
return jsonResponse(events);
}
if (path === '/api/events/propose' && method === 'POST') {
const payload = parseBody(init?.body);
const userId = payload.userId;
const userSnap = await getDoc(doc(db, 'ems_users', String(userId)));
if (!userSnap.exists()) {
return jsonResponse({ message: "Proposer user not found" }, 400);
}
const proposer = userSnap.data();
const start = parseDate(payload.startDate);
const end = parseDate(payload.endDate);
if (start < new Date()) {
return jsonResponse({ message: "Cannot schedule events in the past" }, 400);
}
if (end <= start) {
return jsonResponse({ message: "End date must be after start date" }, 400);
}
const conflictMsg = await getConflictMessage(payload);
if (conflictMsg) {
// If placement with cancelConflicting, we cancel other events
if (payload.category === 'PLACEMENT' && payload.cancelConflicting) {
const colRef = collection(db, 'ems_events');
const snap = await getDocs(colRef);
const allEvs = snap.docs.map(d => d.data());
for (const other of allEvs) {
if (other.status === 'CANCELLED') continue;
if (payload.groupRequestId && other.groupRequestId === payload.groupRequestId) continue;
const oStart = parseDate(other.startDate);
const oEnd = parseDate(other.endDate);
const oLoc = String(other.location || "").trim();
const oInst = String(other.institution || "RIT").trim();
if (
oInst === String(payload.institution || "RIT").trim() &&
oLoc.toLowerCase() === String(payload.venue || "").trim().toLowerCase() &&
start < oEnd &&
end > oStart
) {
await updateDoc(doc(db, 'ems_events', String(other.id)), {
status: 'CANCELLED',
rejectionReason: "This event is cancelled due to the placement activity happening at the venue at this timing."
});
}
}
} else {
// Find overlapping
const colRef = collection(db, 'ems_events');
const snap = await getDocs(colRef);
const list = snap.docs.map(d => d.data()).filter(other => {
if (other.status === 'CANCELLED') return false;
const oStart = parseDate(other.startDate);
const oEnd = parseDate(other.endDate);
const oLoc = String(other.location || "").trim();
const oInst = String(other.institution || "RIT").trim();
return (
oInst === String(payload.institution || "RIT").trim() &&
oLoc.toLowerCase() === String(payload.venue || "").trim().toLowerCase() &&
start < oEnd &&
end > oStart
);
});
return jsonResponse({
message: conflictMsg,
conflicts: list,
canOverride: payload.category === 'PLACEMENT'
}, 409);
}
}
// Propose logic status
let status = "REQUESTED";
if (proposer.role === 'PRINCIPAL') {
status = "APPROVED";
} else if (
proposer.role === 'HOD' ||
payload.category === 'CLUB' ||
payload.category === 'PLACEMENT' ||
payload.category === 'INSTITUTIONAL' ||
proposer.role === 'PLACEMENT' ||
proposer.isPlacementStaff ||
proposer.department === 'Placement Department'
) {
status = "PENDING_PR";
}
const budgetStr = payload.budget ? String(payload.budget) : "";
const feeStr = payload.registrationFee ? String(payload.registrationFee) : "";
const event = {
id: generateNumericId(),
title: payload.eventName || "Untitled Event",
description: payload.description || "",
startDate: start.toISOString(),
endDate: end.toISOString(),
location: String(payload.venue || "TBD").trim(),
category: payload.category || "ACADEMIC",
type: payload.eventType || "Institutional",
institution: String(payload.institution || "RIT").trim(),
department: payload.department || "General",
academicYears: payload.academicYears || [],
status: status,
guestName: payload.guestName || null,
guestSocialProfile: payload.socialProfile || null,
requirements: payload.requirements || [],
targetedSections: payload.targetedSections || [],
groupRequestId: payload.groupRequestId || null,
sponsors: payload.sponsors || [],
budget: budgetStr === "" ? 0.0 : parseFloat(budgetStr),
hasRegistrationFee: !!payload.hasRegistrationFee,
registrationFee: feeStr === "" ? 0.0 : parseFloat(feeStr),
centreName: payload.centreName || null,
isPublicEvent: !!payload.isPublicEvent,
proposer: {
id: proposer.id,
fullName: proposer.fullName,
email: proposer.email,
role: proposer.role
}
};
await setDoc(doc(db, 'ems_events', String(event.id)), event);
return jsonResponse({ message: "Event proposed successfully", id: event.id });
}
if (path === '/api/events/batch-create' && method === 'POST') {
const batchList = parseBody(init?.body);
const eventsToSave: any[] = [];
for (const payload of batchList) {
const title = payload.title || "Untitled Event";
const dateObj = payload.startDate || payload.finalDate;
if (!dateObj) {
return jsonResponse({ message: `Missing date for event: ${title}` }, 400);
}
const start = parseDate(dateObj);
let end;
if (payload.startDate && payload.endDate) {
try {
const origStart = parseDate(payload.startDate);
const origEnd = parseDate(payload.endDate);
const durationMs = origEnd.getTime() - origStart.getTime();
end = new Date(start.getTime() + durationMs);
} catch {
end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
}
} else {
end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
}
if (end <= start) {
return jsonResponse({ message: `End date must be after start date for event: ${title}` }, 400);
}
const dept = payload.targetDepartment || payload.department || "General";
const ay = payload.targetBatch ? [payload.targetBatch] : (payload.academicYears || []);
const venue = payload.venue || payload.location || "TBD";
const proposer = payload.proposer || { id: 1, fullName: "Admin", email: "admin@rit.edu", role: "ADMIN" };
const event = {
id: generateNumericId(),
title,
description: payload.description || "",
startDate: start.toISOString(),
endDate: end.toISOString(),
location: String(venue).trim(),
category: payload.category || "ACADEMIC",
type: payload.type || "Seminar",
institution: String(payload.institution || "RIT").trim(),
department: dept,
academicYears: ay,
status: payload.status || "APPROVED",
requirements: payload.requirements || [],
proposer
};
const conflictMsg = await getConflictMessage(event);
if (conflictMsg) {
return jsonResponse({ message: `Conflict in batch item '${title}': ${conflictMsg}` }, 400);
}
eventsToSave.push(event);
}
// Check intra-batch conflicts
for (let i = 0; i < eventsToSave.length; i++) {
const current = eventsToSave[i];
const currentStart = parseDate(current.startDate);
const currentEnd = parseDate(current.endDate);
const currentLoc = current.location.toLowerCase();
for (let j = i + 1; j < eventsToSave.length; j++) {
const other = eventsToSave[j];
const otherStart = parseDate(other.startDate);
const otherEnd = parseDate(other.endDate);
const otherLoc = other.location.toLowerCase();
if (
current.institution === other.institution &&
currentLoc === otherLoc &&
currentStart < otherEnd &&
currentEnd > otherStart
) {
return jsonResponse({ message: `Conflict between imported events '${current.title}' and '${other.title}' at ${current.location}` }, 400);
}
}
}
for (const ev of eventsToSave) {
await setDoc(doc(db, 'ems_events', String(ev.id)), ev);
}
return jsonResponse({ message: "Batch events created successfully", count: eventsToSave.length });
}
if (path.endsWith('/approve') && method === 'POST') {
const parts = path.split('/');
const id = parts[parts.length - 2];
const userId = queryParams.get('userId');
const userSnap = await getDoc(doc(db, 'ems_users', String(userId)));
const eventRef = doc(db, 'ems_events', String(id));
const eventSnap = await getDoc(eventRef);
if (!userSnap.exists() || !eventSnap.exists()) {
return jsonResponse({ message: "User or Event not found" }, 400);
}
const user = userSnap.data();
const event = eventSnap.data();
const conflictMsg = await getConflictMessage(event, id);
if (conflictMsg) {
return jsonResponse({ message: conflictMsg }, 409);
}
let newStatus = event.status;
if (user.role === 'HOD') {
newStatus = 'PENDING_PR';
} else if (user.role === 'PRINCIPAL') {
newStatus = 'APPROVED';
} else {
return jsonResponse({ message: "Only HoD or Principal can approve events" }, 403);
}
await updateDoc(eventRef, { status: newStatus });
return jsonResponse({ message: "Event action completed successfully", status: newStatus });
}
if (path.endsWith('/reject') && method === 'POST') {
const parts = path.split('/');
const id = parts[parts.length - 2];
const userId = queryParams.get('userId');
const { reason } = parseBody(init?.body);
if (!reason || reason.trim() === '') {
return jsonResponse({ message: "Rejection reason is mandatory" }, 400);
}
const userSnap = await getDoc(doc(db, 'ems_users', String(userId)));
const eventRef = doc(db, 'ems_events', String(id));
const eventSnap = await getDoc(eventRef);
if (!userSnap.exists() || !eventSnap.exists()) {
return jsonResponse({ message: "User or Event not found" }, 400);
}
const user = userSnap.data();
let newStatus = eventSnap.data().status;
if (user.role === 'HOD') {
newStatus = 'HOD_REJECTED';
} else if (user.role === 'PRINCIPAL') {
newStatus = 'PRINCIPAL_REJECTED';
} else {
return jsonResponse({ message: "Only HoD or Principal can reject events" }, 403);
}
await updateDoc(eventRef, { status: newStatus, rejectionReason: reason });
return jsonResponse({ message: "Event rejected successfully", status: newStatus });
}
if (path.startsWith('/api/events/') && method === 'PUT') {
const parts = path.split('/');
const id = parts[parts.length - 1];
const payload = parseBody(init?.body);
const eventRef = doc(db, 'ems_events', String(id));
const eventSnap = await getDoc(eventRef);
if (!eventSnap.exists()) {
return jsonResponse({ message: "Event not found" }, 404);
}
const event = eventSnap.data();
const userId = payload.userId;
const userSnap = await getDoc(doc(db, 'ems_users', String(userId)));
if (!userSnap.exists()) {
return jsonResponse({ message: "User not found" }, 400);
}
const user = userSnap.data();
const isAdmin = user.role === 'ADMIN';
if (!isAdmin) {
if (String(event.proposer?.id) !== String(userId)) {
return jsonResponse({ message: "Only the proposer can edit this event" }, 403);
}
if (event.status !== 'REQUESTED') {
return jsonResponse({ message: "Event cannot be edited once it moves past the initial request stage" }, 403);
}
}
const start = payload.startDate ? parseDate(payload.startDate) : parseDate(event.startDate);
const end = payload.endDate ? parseDate(payload.endDate) : parseDate(event.endDate);
if (end <= start) {
return jsonResponse({ message: "End date must be after start date" }, 400);
}
const testEvent = {
...event,
...payload,
startDate: start.toISOString(),
endDate: end.toISOString()
};
const conflictMsg = await getConflictMessage(testEvent, id);
if (conflictMsg) {
return jsonResponse({ message: conflictMsg }, 409);
}
const updatedFields: any = {};
if (payload.title !== undefined) updatedFields.title = payload.title;
if (payload.startDate !== undefined) updatedFields.startDate = start.toISOString();
if (payload.endDate !== undefined) updatedFields.endDate = end.toISOString();
if (payload.eventType !== undefined) updatedFields.type = payload.eventType;
if (payload.institution !== undefined) updatedFields.institution = payload.institution;
if (payload.department !== undefined) updatedFields.department = payload.department;
if (payload.venue !== undefined) updatedFields.location = payload.venue;
if (payload.guestName !== undefined) updatedFields.guestName = payload.guestName;
if (payload.socialProfile !== undefined) updatedFields.guestSocialProfile = payload.socialProfile;
if (payload.academicYears !== undefined) updatedFields.academicYears = payload.academicYears;
if (payload.targetedSections !== undefined) updatedFields.targetedSections = payload.targetedSections;
if (payload.requirements !== undefined) updatedFields.requirements = payload.requirements;
if (payload.sponsors !== undefined) updatedFields.sponsors = payload.sponsors;
if (payload.description !== undefined) updatedFields.description = payload.description;
if (payload.budget !== undefined) {
const b = String(payload.budget);
updatedFields.budget = b === "" ? 0.0 : parseFloat(b);
}
if (payload.hasRegistrationFee !== undefined) updatedFields.hasRegistrationFee = !!payload.hasRegistrationFee;
if (payload.registrationFee !== undefined) {
const f = String(payload.registrationFee);
updatedFields.registrationFee = f === "" ? 0.0 : parseFloat(f);
}
if (payload.centreName !== undefined) updatedFields.centreName = payload.centreName;
if (payload.isPublicEvent !== undefined) updatedFields.isPublicEvent = !!payload.isPublicEvent;
if (payload.status !== undefined && isAdmin) {
updatedFields.status = payload.status;
}
await updateDoc(eventRef, updatedFields);
return jsonResponse({ message: "Event updated successfully" });
}
if (path.startsWith('/api/events/') && method === 'DELETE') {
const parts = path.split('/');
const id = parts[parts.length - 1];
await deleteDoc(doc(db, 'ems_events', String(id)));
return jsonResponse({ message: "Event deleted successfully" });
}
} catch (err: any) {
console.error(`Interceptor routing error:`, err);
return jsonResponse({ message: "Internal server error: " + err.message }, 500);
}
// Fallback to original fetch
return originalFetch(input, init);
};
console.log("[Firebase Backend] Fetch interceptor active.");

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,11 @@
import './lib/firebaseBackend'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,147 @@
import { API_BASE_URL } from '../lib/config';
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { User, Lock, Eye, EyeOff, ArrowRight } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { cn } from '../lib/utils';
import ritLogo from '../assets/images/college-logo.png';
import ritBuilding from '../assets/images/ritchennai.jpg';
export const LoginPage: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { login } = useAuth();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
try {
const response = await fetch(API_BASE_URL + '/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
const data = await response.json();
login(data);
} else {
const errData = await response.json();
setError(errData.message || 'Login failed');
}
} catch (err) {
setError('Connection failed. Please ensure the backend is running.');
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-slate-50 flex items-center justify-center p-4">
<div className="max-w-6xl w-full flex flex-col md:flex-row bg-white rounded-[2.5rem] overflow-hidden premium-shadow min-h-[700px]">
{/* Left Side: Login Form */}
<div className="w-full md:w-[45%] p-10 md:p-16 flex flex-col">
<div className="mb-12">
<img src={ritLogo} alt="RIT Logo" className="h-10 mb-8 object-contain" />
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-brand-indigo mb-2">
RAJALAKSHMI INSTITUTE OF TECHNOLOGY
</p>
<h1 className="text-4xl font-black text-text-dark tracking-tight mb-2">
RIT EMS
</h1>
<p className="text-text-muted font-medium">
Event Management System
</p>
</div>
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label className="block text-[10px] font-black uppercase tracking-widest text-text-muted mb-2">
Institutional Email
</label>
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Username or email"
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 pl-12 pr-4 text-sm font-semibold focus:outline-none focus:bg-white focus:border-brand-indigo/30 transition-all"
required
/>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<label className="block text-[10px] font-black uppercase tracking-widest text-text-muted">
Passcode
</label>
<button type="button" className="text-[10px] font-black uppercase tracking-widest text-brand-indigo hover:opacity-70 transition-all">
Forgot?
</button>
</div>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 pl-12 pr-12 text-sm font-semibold focus:outline-none focus:bg-white focus:border-brand-indigo/30 transition-all"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-text-muted hover:text-brand-indigo"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{error && (
<p className="text-status-danger text-xs font-bold bg-red-50 p-3 rounded-xl border border-red-100 animate-shake">
{error}
</p>
)}
<button
type="submit"
disabled={isLoading}
className={cn(
"w-full bg-brand-navy text-white rounded-2xl py-4 flex items-center justify-center gap-3 font-black text-[11px] uppercase tracking-widest transition-all hover:scale-[1.01] active:scale-[0.99] premium-shadow",
isLoading && "opacity-70 cursor-not-allowed"
)}
>
{isLoading ? 'Processing...' : 'Enter Dashboard'}
{!isLoading && <ArrowRight className="w-4 h-4" />}
</button>
</form>
</div>
{/* Right Side: Building Image */}
<div className="hidden md:block flex-1 relative">
<img
src={ritBuilding}
alt="College Building"
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div className="absolute bottom-10 left-10 right-10">
<div className="bg-white/10 backdrop-blur-md border border-white/20 p-6 rounded-[2rem] inline-block">
<p className="text-white font-bold text-lg">Rajalakshmi Institute of Technology</p>
<p className="text-white/70 text-sm font-medium">Nurturing Excellence since 2008</p>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})