commit 0a76feafc5fdb66fee5d6cab218490dcc58fa8bf Author: Abiram070207 Date: Thu Jun 18 14:07:24 2026 +0530 Convert backends to Firebase and combine projects diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5416885 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Dependency directories +**/node_modules/ +**/target/ + +# Build outputs +**/dist/ +**/.tmp/ + +# System files +.DS_Store +Thumbs.db + +# Credentials & environment files +**/firebasecred +**/.env +**/.env.local +**/.env.*.local + +# IDE files +**/.vscode/ +**/.idea/ +**/*.iml +**/*.suo +**/*.ntvs* +**/*.njsproj +**/*.sln +**/*.sw? diff --git a/RIT-EMS-main/EMS_DOCUMENTATION.md b/RIT-EMS-main/EMS_DOCUMENTATION.md new file mode 100644 index 0000000..46a5dcb --- /dev/null +++ b/RIT-EMS-main/EMS_DOCUMENTATION.md @@ -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` diff --git a/RIT-EMS-main/EMS_DOCUMENTATION_SUPABASE.md b/RIT-EMS-main/EMS_DOCUMENTATION_SUPABASE.md new file mode 100644 index 0000000..4526239 --- /dev/null +++ b/RIT-EMS-main/EMS_DOCUMENTATION_SUPABASE.md @@ -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. diff --git a/RIT-EMS-main/PROJECT_OVERVIEW.md b/RIT-EMS-main/PROJECT_OVERVIEW.md new file mode 100644 index 0000000..c926342 --- /dev/null +++ b/RIT-EMS-main/PROJECT_OVERVIEW.md @@ -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
Dashboard Option] + ProposalView[EventProposalForm.tsx
Propose Option] + ApprovalsView[ApprovalsView.tsx
Approvals Option] + AllEventsView[AllEvents.tsx
All Events Option] + ChecklistView[InstitutionalChecklist.tsx
Checklist Option] + HistoryView[EventHistory.tsx
History Option] + ClubsView[ClubInstitutionalChecklist.tsx
Clubs Option] + AutomationView[AutomationView.tsx
Automation Option] + ExcelImportView[ExcelImport.tsx
Excel Import Option] + ClassesView[ClassManagement.tsx
Classes Option] + UsersView[UserManagement.tsx
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}` diff --git a/RIT-EMS-main/backend/.gitattributes b/RIT-EMS-main/backend/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/RIT-EMS-main/backend/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/RIT-EMS-main/backend/.gitignore b/RIT-EMS-main/backend/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/RIT-EMS-main/backend/.gitignore @@ -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/ diff --git a/RIT-EMS-main/backend/.mvn/wrapper/maven-wrapper.properties b/RIT-EMS-main/backend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c595b00 --- /dev/null +++ b/RIT-EMS-main/backend/.mvn/wrapper/maven-wrapper.properties @@ -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 diff --git a/RIT-EMS-main/backend/mvnw b/RIT-EMS-main/backend/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/RIT-EMS-main/backend/mvnw @@ -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-,maven-mvnd--}/ +[ -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 "$@" diff --git a/RIT-EMS-main/backend/mvnw.cmd b/RIT-EMS-main/backend/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/RIT-EMS-main/backend/mvnw.cmd @@ -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-,maven-mvnd--}/ +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" diff --git a/RIT-EMS-main/backend/pom.xml b/RIT-EMS-main/backend/pom.xml new file mode 100644 index 0000000..80b6e47 --- /dev/null +++ b/RIT-EMS-main/backend/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.ems + backend + 0.0.1-SNAPSHOT + backend + + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + com.mysql + mysql-connector-j + runtime + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.projectlombok + lombok + 1.18.36 + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/BackendApplication.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/BackendApplication.java new file mode 100644 index 0000000..12aa683 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/BackendApplication.java @@ -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); + } + +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/CustomUserDetailsService.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/CustomUserDetailsService.java new file mode 100644 index 0000000..3590a71 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/CustomUserDetailsService.java @@ -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())) + ); + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/DataInitializer.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/DataInitializer.java new file mode 100644 index 0000000..dc9f517 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/DataInitializer.java @@ -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); + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/DataSeeder.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/DataSeeder.java new file mode 100644 index 0000000..347c499 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/DataSeeder.java @@ -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."); + }; + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/SecurityConfig.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/SecurityConfig.java new file mode 100644 index 0000000..81e2287 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/config/SecurityConfig.java @@ -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; + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/AdminController.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/AdminController.java new file mode 100644 index 0000000..f6dc757 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/AdminController.java @@ -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> 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")); + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/AuthController.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/AuthController.java new file mode 100644 index 0000000..e67d90d --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/AuthController.java @@ -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 userOptional = userRepository.findByEmail(loginRequest.getEmail()); + + if (userOptional.isPresent()) { + User user = userOptional.get(); + if (passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) { + java.util.Map 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")); + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/ClassController.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/ClassController.java new file mode 100644 index 0000000..af284c9 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/ClassController.java @@ -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> 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 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)); + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/EventController.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/EventController.java new file mode 100644 index 0000000..b25898b --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/EventController.java @@ -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 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) 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) payload.get("requirements")); + event.setTargetedSections((List) 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) 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 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 getAllEvents() { + List events = eventRepository.findAll(); + populateConflictMessages(events); + return events; + } + + @PostMapping("/batch-create") + @Transactional + public ResponseEntity batchCreateEvents(@RequestBody List> eventsPayload) { + try { + if (eventsPayload == null || eventsPayload.isEmpty()) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("message", "No events provided for import")); + } + + List eventsToSave = new ArrayList<>(); + for (Map 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) 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) payload.get("requirements")); + } + + if (payload.get("proposer") != null) { + try { + Object proposerObj = payload.get("proposer"); + if (proposerObj instanceof Map) { + Map prop = (Map) 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 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 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) payload.get("academicYears")); + if (payload.containsKey("targetedSections")) event.setTargetedSections((List) payload.get("targetedSections")); + if (payload.containsKey("requirements")) event.setRequirements((List) payload.get("requirements")); + if (payload.containsKey("sponsors")) event.setSponsors((List) 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 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 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; + } + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/HealthController.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/HealthController.java new file mode 100644 index 0000000..eb3d3a1 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/HealthController.java @@ -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 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 + ); + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/NoteController.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/NoteController.java new file mode 100644 index 0000000..c4ef0d5 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/controller/NoteController.java @@ -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 getAllNotes() { + return noteRepository.findAll(); + } + + @PostMapping + public InstitutionalNote createNote(@RequestBody InstitutionalNote note) { + return noteRepository.save(note); + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/dto/LoginRequest.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/dto/LoginRequest.java new file mode 100644 index 0000000..cef1e9a --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/dto/LoginRequest.java @@ -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; } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/BaseEntity.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/BaseEntity.java new file mode 100644 index 0000000..4706370 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/BaseEntity.java @@ -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; } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/ClassMapping.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/ClassMapping.java new file mode 100644 index 0000000..c16340e --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/ClassMapping.java @@ -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 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 getSections() { return sections; } + public void setSections(java.util.List sections) { this.sections = sections; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/Event.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/Event.java new file mode 100644 index 0000000..0e40986 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/Event.java @@ -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 academicYears; + + @Column(nullable = false) + private String status; // PENDING, APPROVED, COMPLETED, CANCELLED + + private String guestName; + private String guestSocialProfile; + + @jakarta.persistence.ElementCollection + private List requirements; + + @jakarta.persistence.ElementCollection + private List targetedSections; + + private Double budget; + @Column(columnDefinition = "boolean default false") + private boolean hasRegistrationFee; + private Double registrationFee; + + @jakarta.persistence.ElementCollection + private List 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 academicYears, String status, String guestName, String guestSocialProfile, List requirements, List 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 getAcademicYears() { return academicYears; } + public void setAcademicYears(List 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 getRequirements() { return requirements; } + public void setRequirements(List requirements) { this.requirements = requirements; } + public List getTargetedSections() { return targetedSections; } + public void setTargetedSections(List 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 getSponsors() { return sponsors; } + public void setSponsors(List 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 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 requirements) { event.setRequirements(requirements); return this; } + public EventBuilder targetedSections(List 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 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; + } + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/InstitutionalNote.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/InstitutionalNote.java new file mode 100644 index 0000000..d5e3d97 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/InstitutionalNote.java @@ -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(); + } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/User.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/User.java new file mode 100644 index 0000000..ad7f4d4 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/model/User.java @@ -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 assignedClubs; + + public User() {} + + public User(String email, String password, String fullName, String role, String department, boolean isClubCoordinator, boolean isPlacementStaff, List 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 getAssignedClubs() { return assignedClubs; } + public void setAssignedClubs(List assignedClubs) { this.assignedClubs = assignedClubs; } +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/ClassRepository.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/ClassRepository.java new file mode 100644 index 0000000..28aba4b --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/ClassRepository.java @@ -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 { +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/EventRepository.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/EventRepository.java new file mode 100644 index 0000000..932e93f --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/EventRepository.java @@ -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 { + List findByCategory(String category); + List 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 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); +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/InstitutionalNoteRepository.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/InstitutionalNoteRepository.java new file mode 100644 index 0000000..af3162c --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/InstitutionalNoteRepository.java @@ -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 { + List findByTargetDate(LocalDate targetDate); +} diff --git a/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/UserRepository.java b/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/UserRepository.java new file mode 100644 index 0000000..b0ce4b9 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/java/com/ems/backend/repository/UserRepository.java @@ -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 { + Optional findByEmail(String email); +} diff --git a/RIT-EMS-main/backend/src/main/resources/application.properties b/RIT-EMS-main/backend/src/main/resources/application.properties new file mode 100644 index 0000000..ed888a6 --- /dev/null +++ b/RIT-EMS-main/backend/src/main/resources/application.properties @@ -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 diff --git a/RIT-EMS-main/backend/src/test/java/com/ems/backend/BackendApplicationTests.java b/RIT-EMS-main/backend/src/test/java/com/ems/backend/BackendApplicationTests.java new file mode 100644 index 0000000..426c179 --- /dev/null +++ b/RIT-EMS-main/backend/src/test/java/com/ems/backend/BackendApplicationTests.java @@ -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() { + } + +} diff --git a/RIT-EMS-main/frontend/.gitignore b/RIT-EMS-main/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/RIT-EMS-main/frontend/.gitignore @@ -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? diff --git a/RIT-EMS-main/frontend/Hall Booking & Event Requirements 2024 (Responses).xlsx b/RIT-EMS-main/frontend/Hall Booking & Event Requirements 2024 (Responses).xlsx new file mode 100644 index 0000000..e9a3c86 Binary files /dev/null and b/RIT-EMS-main/frontend/Hall Booking & Event Requirements 2024 (Responses).xlsx differ diff --git a/RIT-EMS-main/frontend/README.md b/RIT-EMS-main/frontend/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/RIT-EMS-main/frontend/README.md @@ -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... + }, + }, +]) +``` diff --git a/RIT-EMS-main/frontend/eslint.config.js b/RIT-EMS-main/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/RIT-EMS-main/frontend/eslint.config.js @@ -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, + }, + }, +]) diff --git a/RIT-EMS-main/frontend/excel_preview.csv b/RIT-EMS-main/frontend/excel_preview.csv new file mode 100644 index 0000000..1e6c4af --- /dev/null +++ b/RIT-EMS-main/frontend/excel_preview.csv @@ -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,,,, diff --git a/RIT-EMS-main/frontend/fix_urls.py b/RIT-EMS-main/frontend/fix_urls.py new file mode 100644 index 0000000..ddce66b --- /dev/null +++ b/RIT-EMS-main/frontend/fix_urls.py @@ -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)) diff --git a/RIT-EMS-main/frontend/index.html b/RIT-EMS-main/frontend/index.html new file mode 100644 index 0000000..46fd13b --- /dev/null +++ b/RIT-EMS-main/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + RIT EMS - Event Management System + + +
+ + + diff --git a/RIT-EMS-main/frontend/package-lock.json b/RIT-EMS-main/frontend/package-lock.json new file mode 100644 index 0000000..a749584 --- /dev/null +++ b/RIT-EMS-main/frontend/package-lock.json @@ -0,0 +1,4579 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "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" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@firebase/ai": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.13.1.tgz", + "integrity": "sha512-RhT/VViTPBSplhQSuEp62HhLvfsV+LowMh8ZUo5MMRDzG7oFtSget4Kmg5oHP50hDVyWQuQj6to9iPFEZk08Tw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.22", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.22.tgz", + "integrity": "sha512-8BSaq/QRGU1+xyi8L2PTLTJU7MH9aMA72RQdIxrbhWFauOZY9OXo8f2YDN/972xA8d588tlnNVEQ2Mo69pT9Ow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.28", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.28.tgz", + "integrity": "sha512-lIAlqUUbBu93FJMlQfslryQtBwwzdzvp23ePC6FNgymXk6Ook5v4Uvc0vdutvoIeqmyA3LfP0ZeRFK8+11kOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.22", + "@firebase/analytics-types": "0.8.4", + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.4.tgz", + "integrity": "sha512-zQ+XTgkwH6CY/eUSHJRP7e4LxM30RCxlCmob5sy2axs25GE3Ny0XdgpDscMTHHQIGqWkxPXad4w2Mw9sCgT8zQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.15.0.tgz", + "integrity": "sha512-soIskolmGgbpi0K/MfrjtdpO1220qRCbXA4Z8Qx3lM+fVwA3q40m+OM+7zBHd2nuQCrLXb33L6Oc1aBH3Y26AQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.12.0.tgz", + "integrity": "sha512-wMeT6HLWRAuW7Cp/5UjWBGKgjPNxWNOoNf4PRIv0weljoGMZVeqbUY7wNBWTI2/31cX1NlXx8gQruDLsUShB3Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.5.tgz", + "integrity": "sha512-JI17mVcZs34zO6ZeSCrw4U2iohqy+n6GIzkbmsA+TbVjmvFLkUKt3bs5M+qRBteQm/0IWzqSHYFzEQLzDTQebg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.12.0", + "@firebase/app-check-types": "0.5.4", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.4.tgz", + "integrity": "sha512-zz3i6e13B8BfWiLy8MABtTh8aGIACgKbf9UVnyHcWs+yQzJXgQcl8A46b0zfaiJHdQ+niF0ouAfcpuf+3LMPQg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.4.tgz", + "integrity": "sha512-xV7JsIyzVr15aA7f3Pi0rB9gdBuVubs89FGA8VkRYA4g0l78poADgdfrScgf7NndSg9mm7cR7PJyY0+t22KaGw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.14", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.14.tgz", + "integrity": "sha512-rgFmiofYsdS9ZG/Bht3OBxJtPD3zWE1cffShWubEm+4+qZeyzCbmtb1q6jOEjN9fB7uufe4rQmWOPXouR3758Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.15.0", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.5.tgz", + "integrity": "sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.1" + } + }, + "node_modules/@firebase/auth": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.3.tgz", + "integrity": "sha512-bqiq4uubDN2YyQkdvSWPQeJyXAv2O76ImF41En9b6UhV5JuBVYDoHYrrrE3NzIuGkpFMKagfhMRP4Vz6t+yQSQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.8.tgz", + "integrity": "sha512-llcBREUC4iSNKZ6rvwud7Oz9Q7aAWU6KuQLa6pdu7Q+QAQsy4JLw6yFgxwtmzabsgznHmmcsX2UjHLLzqUxi3Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.13.3", + "@firebase/auth-types": "0.13.1", + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.5.tgz", + "integrity": "sha512-1Li/YuBDBAXcKv7BzY4U28gontUmAaw53sYiqbaVOMCFb2lFKK/c3CGMUWqtwe7+TXrl3poWnTCL5umYBg85Eg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.1.tgz", + "integrity": "sha512-0c1Mnid0uMDfGJHeUS4zfvBa4/CedJXotGy/n/NZJnBjwiJawt0ZYU+wH2VAVLiRCEfG2ncCkAX3yd1/2nrB7g==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.3.tgz", + "integrity": "sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.7.1.tgz", + "integrity": "sha512-2LbUU8mmSA63HknxQMmWHjpzuNLBKflvVwQc2tpoVKg0biWleNEJX031ELks0vzFs+dDjOUkCJR72RP6mQHFOg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.3.tgz", + "integrity": "sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.4.tgz", + "integrity": "sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/database": "1.1.3", + "@firebase/database-types": "1.0.20", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.20.tgz", + "integrity": "sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.5", + "@firebase/util": "1.15.1" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.16.0.tgz", + "integrity": "sha512-qdHMHMvMr0nRMuZyWNR/ArWa0YlPE3C4eAbmxTASJMYXAesKPL0Y54p70moggrNPzaK7MSIIq5RDJJyntQyIYA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "@firebase/webchannel-wrapper": "1.0.6", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "re2js": "^0.4.2", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.11.tgz", + "integrity": "sha512-W7o1WdwWq5aABK5Up2ncSvTQs/QGLR/fy7cVpFBNqhsXtxoMtflHf2xBIG6+aoptcuGAobddq4g2Sq27wqHaYw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/firestore": "4.16.0", + "@firebase/firestore-types": "3.0.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.4.tgz", + "integrity": "sha512-jGn+JSS4X9zZsrfu7Yw66v5YRdOLD1oyQh4USR0xWl4CUqV/DA6bNIXRPpxH/cUl3iVTNiP6MN7g+EL42A4qfA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.5.tgz", + "integrity": "sha512-bWCx713f4kE/uFV7gdFOLBS7lDoiZj48MRkbAqe35gkXcCeWF4QjRNO07Jhmve7EJIoQOBczL29y2r8VRuN1kw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/messaging-interop-types": "0.2.5", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.5.tgz", + "integrity": "sha512-10qlUXGY25G5/1g9UihqksPp2po+ZqSE7LEizsrdUP7vrTmkysXxGSZCDyojSEp6mQe/ecRDdDDI+z4XRdb4wQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/functions": "0.13.5", + "@firebase/functions-types": "0.6.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.4.tgz", + "integrity": "sha512-zV6kgqtduR4rUAdC/ilS7kmb93XD7bEZoJDlVBZqlOw2uGGGCNBQBuleww2rr0Ulr3L9o2TDjumEt68/l1f9DQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.22", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.22.tgz", + "integrity": "sha512-ef6nn3GGQTdReCfotRMG77PJZu8CqEbiK5pEoBnM0gTu/Z9v0i/az2p3HABsa/1beQmmyh1OsOjf7P5+pgwdZw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.22.tgz", + "integrity": "sha512-C/zpAuTP5S9OgKSPvXRupw3hoY/JZSlA1wFjD/Sb7LIQE0FNbcMdO8Y4KXVEkjVzma/DDDDIAzxEXqKMAzc88w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/installations-types": "0.5.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.4.tgz", + "integrity": "sha512-U2eFapdHwjb43Vx9o+Pmj4dFfvcHEK1IirEFLqMtWrTHvmdrS3gBpBD1kmJk/9HjsOtoHZxJ2Paoe79e+L1ZPg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.1.tgz", + "integrity": "sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.13.0.tgz", + "integrity": "sha512-GZoo0uGRvEbszo83xcgbjJp4FpkmBEr4l8Z4hi8gl+P1Spn/MTK3HapanMzSX4yUHuTEiF5hasWRxOaz+o5sxQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/messaging-interop-types": "0.2.5", + "@firebase/util": "1.15.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.27.tgz", + "integrity": "sha512-JNOiu1PPgdHzEPEtoFiNxQuu0x9bm4bfETSQCpGfcTlgWkhlSK7uh7nlsjC10TQLUNgYetLmuutaYTh8aeYLVA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/messaging": "0.13.0", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.5.tgz", + "integrity": "sha512-tUEKnaAP2Y/MNIqgnriPpV6e5l13Vs/+p2yrd6NGlncPJT9O3a8muYZtdnWe+IJ4fgKLHJVC79n/asxk/N5Msw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.12.tgz", + "integrity": "sha512-fe7nV8teUU3OBHlMUZ9Lw4gLhCW2k4m5Uc3pfWGV+fl8uwJQBGp9Q3lqsJ+HSrFu3Q2pJyLAgrClPGSKyDeYgQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.25.tgz", + "integrity": "sha512-q6NjTXpIPoFuUmCmMN/maCdTgzT6aExs9xZo+PxfVLj6uLVGvpyAD6XWjmcrb7jChsFBYbq7E5dyNDF7Zhy9kA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/performance": "0.7.12", + "@firebase/performance-types": "0.2.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.4.tgz", + "integrity": "sha512-kJSEk7b0uhpcPRyL4SQ/GPujLqk52XNKcXlnsKDbWGAb9vugcLvOU3u6zfEdwd+d8hWJb5S5ZizV1JFFI0nkKg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.5.tgz", + "integrity": "sha512-zb+7CDGFP2wYVF1LXQoYIFdoESIQM3p0+uiW1welw8+zvDxAL50K75PKTXXtunJADUrksTVpV7mD0pn54vzJRA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.26", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.26.tgz", + "integrity": "sha512-uC57Tc7GYYOCnMgLkGIVf999XlaYaPDONoa54c93YTKDctlvCZI89z0zQ2RbhGR8Zf+QuCbQHs/99vqoE84a7g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/remote-config": "0.8.5", + "@firebase/remote-config-types": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.1.tgz", + "integrity": "sha512-cX/1LT6KQwkXzck2eSzeKnuvXZCyr8qaPpDcikoJs7jmI+oBOXixpDLeDtWj1U6GNMkIoXrEDNoyT2Ypcyp5/A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.3.tgz", + "integrity": "sha512-YX4/YL6P6/fufSSeGnVhjWddcIXbFq2cWIhMKFTZo1E/Rtcl2mJj/BYUQTwJfcE1Tl8un1FOya4L05jcSLN/Eg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.3.tgz", + "integrity": "sha512-gruVqjtUGX8tEoeNbaWXZm0Zfcfcb7fvmDmBxV8yPAbWvExRnZYLO2+qw9idxNE7BvPXt5csyjSYHy//dAizxw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/storage": "0.14.3", + "@firebase/storage-types": "0.8.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.4.tgz", + "integrity": "sha512-BT7cwxJOx8SWwlQfrlC+bD/Sk3Cw+1odCi8UZNFNWTVZoPsBnA5W+mqtZzVnvsdJpXCFGSGQ7R7vOR6dtM/BRA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.1.tgz", + "integrity": "sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.6.tgz", + "integrity": "sha512-Vr/Mqu79dMwGRAyGbJ4uN4+BtXB3/mRTdzetD1daWNeG8QaWuzhhbG77GltO5c0yYmYls8i250iX73624GJd7Q==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.126.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", + "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz", + "integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz", + "integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz", + "integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz", + "integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz", + "integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz", + "integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz", + "integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz", + "integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz", + "integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz", + "integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.4.tgz", + "integrity": "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "postcss": "^8.5.6", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", + "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.343", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz", + "integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase": { + "version": "12.15.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.15.0.tgz", + "integrity": "sha512-p0YTLcRSTiBXMx9sGr4ZNSfLjc/RVBEw4C/TXjVMtw65+6E1Pbm47UY3F4/AqRoDobEcNX3gsbPGy7jPjxbgSQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.13.1", + "@firebase/analytics": "0.10.22", + "@firebase/analytics-compat": "0.2.28", + "@firebase/app": "0.15.0", + "@firebase/app-check": "0.12.0", + "@firebase/app-check-compat": "0.4.5", + "@firebase/app-compat": "0.5.14", + "@firebase/app-types": "0.9.5", + "@firebase/auth": "1.13.3", + "@firebase/auth-compat": "0.6.8", + "@firebase/data-connect": "0.7.1", + "@firebase/database": "1.1.3", + "@firebase/database-compat": "2.1.4", + "@firebase/firestore": "4.16.0", + "@firebase/firestore-compat": "0.4.11", + "@firebase/functions": "0.13.5", + "@firebase/functions-compat": "0.4.5", + "@firebase/installations": "0.6.22", + "@firebase/installations-compat": "0.2.22", + "@firebase/messaging": "0.13.0", + "@firebase/messaging-compat": "0.2.27", + "@firebase/performance": "0.7.12", + "@firebase/performance-compat": "0.2.25", + "@firebase/remote-config": "0.8.5", + "@firebase/remote-config-compat": "0.2.26", + "@firebase/storage": "0.14.3", + "@firebase/storage-compat": "0.4.3", + "@firebase/util": "1.15.1" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz", + "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/re2js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-0.4.3.tgz", + "integrity": "sha512-EuNmh7jurhHEE8Ge/lBo9JuMLb3qf866Xjjfyovw3wPc7+hlqDkZq4LwhrCQMEI+ARWfrKrHozEndzlpNT0WDg==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", + "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.126.0", + "@rolldown/pluginutils": "1.0.0-rc.16" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.16", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", + "@rolldown/binding-darwin-x64": "1.0.0-rc.16", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.16", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", + "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", + "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.16", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/RIT-EMS-main/frontend/package.json b/RIT-EMS-main/frontend/package.json new file mode 100644 index 0000000..c269d48 --- /dev/null +++ b/RIT-EMS-main/frontend/package.json @@ -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" + } +} diff --git a/RIT-EMS-main/frontend/postcss.config.js b/RIT-EMS-main/frontend/postcss.config.js new file mode 100644 index 0000000..14502dc --- /dev/null +++ b/RIT-EMS-main/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + autoprefixer: {}, + }, +} diff --git a/RIT-EMS-main/frontend/public/favicon.svg b/RIT-EMS-main/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/RIT-EMS-main/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/RIT-EMS-main/frontend/public/icons.svg b/RIT-EMS-main/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/RIT-EMS-main/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RIT-EMS-main/frontend/src/App.css b/RIT-EMS-main/frontend/src/App.css new file mode 100644 index 0000000..0274904 --- /dev/null +++ b/RIT-EMS-main/frontend/src/App.css @@ -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); + } +} \ No newline at end of file diff --git a/RIT-EMS-main/frontend/src/App.tsx b/RIT-EMS-main/frontend/src/App.tsx new file mode 100644 index 0000000..051cd33 --- /dev/null +++ b/RIT-EMS-main/frontend/src/App.tsx @@ -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([]); + const [preFillData, setPreFillData] = useState(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 ( + + + + + + ); + } + + const renderContent = () => { + const isDashboard = ( + + ); + + switch (activeItem) { + case 'user-management': + return user?.role === 'ADMIN' ? : isDashboard; + case 'approvals': + return (user?.role === 'HOD' || user?.role === 'PRINCIPAL') ? : isDashboard; + case 'clubs': + return isClubAuthorized ? : isDashboard; + case 'events': + return ; + case 'propose': + return ; + case 'checklist': + return ; + case 'history': + return ; + case 'classes': + return user?.role === 'ADMIN' ? : isDashboard; + case 'automation': + return (user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? : isDashboard; + case 'import-excel': + return ; + case 'dashboard': + return isDashboard; + default: + return isDashboard; + } + }; + + return ( + + + + {renderContent()} + + + + ); +}; + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/RIT-EMS-main/frontend/src/assets/hero.png b/RIT-EMS-main/frontend/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/RIT-EMS-main/frontend/src/assets/hero.png differ diff --git a/RIT-EMS-main/frontend/src/assets/images/college-logo.png b/RIT-EMS-main/frontend/src/assets/images/college-logo.png new file mode 100644 index 0000000..876b707 Binary files /dev/null and b/RIT-EMS-main/frontend/src/assets/images/college-logo.png differ diff --git a/RIT-EMS-main/frontend/src/assets/images/ritchennai.jpg b/RIT-EMS-main/frontend/src/assets/images/ritchennai.jpg new file mode 100644 index 0000000..a374d21 Binary files /dev/null and b/RIT-EMS-main/frontend/src/assets/images/ritchennai.jpg differ diff --git a/RIT-EMS-main/frontend/src/assets/images/rsb_logo.png b/RIT-EMS-main/frontend/src/assets/images/rsb_logo.png new file mode 100644 index 0000000..715aa62 Binary files /dev/null and b/RIT-EMS-main/frontend/src/assets/images/rsb_logo.png differ diff --git a/RIT-EMS-main/frontend/src/assets/react.svg b/RIT-EMS-main/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/RIT-EMS-main/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/RIT-EMS-main/frontend/src/assets/vite.svg b/RIT-EMS-main/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/RIT-EMS-main/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/RIT-EMS-main/frontend/src/components/DashboardLayout.tsx b/RIT-EMS-main/frontend/src/components/DashboardLayout.tsx new file mode 100644 index 0000000..bae41d3 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/DashboardLayout.tsx @@ -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 = ({ + children, + activeItem, + onItemClick +}) => { + const { user, logout } = useAuth(); + const [searchQuery, setSearchQuery] = useState(''); + const [isSearchOpen, setIsSearchOpen] = useState(false); + const [events, setEvents] = useState([]); + const searchRef = useRef(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 ( +
+ +
+ {/* Header */} +
+
+ + { + 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" + /> + + + {isSearchOpen && (filteredNav.length > 0 || filteredEvents.length > 0) && ( + + {filteredNav.length > 0 && ( +
+

Navigation

+ {filteredNav.map(item => ( + + ))} +
+ )} + + {filteredEvents.length > 0 && ( +
+

Recent Events

+ {filteredEvents.map(event => ( + + ))} +
+ )} +
+ )} +
+
+ +
+ + +
+ +
+ +
+
+ +
+
+ + {user?.fullName} + + + {user?.role} + +
+
+ + +
+
+ +
+ {children} +
+
+
+ ); +}; diff --git a/RIT-EMS-main/frontend/src/components/Sidebar.tsx b/RIT-EMS-main/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..14dee55 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/Sidebar.tsx @@ -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 = ({ 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 ( + + ); +}; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/AllEvents.tsx b/RIT-EMS-main/frontend/src/components/dashboard/AllEvents.tsx new file mode 100644 index 0000000..718ddbf --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/AllEvents.tsx @@ -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 = ({ onEditEvent }) => { + const { user } = useAuth(); + const [events, setEvents] = useState([]); + 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 ( +
+ {/* Header & Controls */} +
+
+

All Events

+

Overview of all institutional activities and their status.

+
+ +
+
+ + 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" + /> +
+ {(user?.role === 'ADMIN' || user?.role === 'PRINCIPAL') && ( + + )} + +
+
+ + {/* Events List */} +
+ {isLoading ? ( +
Loading events...
+ ) : filteredEvents.length === 0 ? ( +
+
+ +
+

No events found matching your search.

+
+ ) : ( + <> +
+ + + + + + + + + + + + {currentEvents.map((event, idx) => ( + + + + + + + + ))} + +
Event DetailsDate & VenueDepartmentStatusActions
+
+ {event.title} + {event.type} • {event.institution} +
+
+
+
+ + {parseDate(event.startDate) ? parseDate(event.startDate)!.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }) : 'N/A'} +
+
+ + {event.location} +
+
+
+
+
+ {event.department.substring(0, 2)} +
+ {event.department} +
+
+ {user?.role === 'FACULTY' && event.proposer?.email === user?.email ? ( + + ) : ( + + {event.status === 'PENDING_PR' ? 'PENDING PRINCIPAL' : event.status} + + )} + +
+ {(user?.role === 'ADMIN' || (event.status === 'REQUESTED' && event.proposer?.email === user?.email)) && onEditEvent && ( + + )} + +
+
+
+ + { + setItemsPerPage(val); + setCurrentPage(1); + }} + itemsPerPageOptions={[5, 10, 20, 50]} + /> + + )} +
+
+ ); +}; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/ApprovalsView.tsx b/RIT-EMS-main/frontend/src/components/dashboard/ApprovalsView.tsx new file mode 100644 index 0000000..20f126b --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/ApprovalsView.tsx @@ -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([]); + const [isLoading, setIsLoading] = useState(true); + const [processingId, setProcessingId] = useState(null); + const [selectedEvent, setSelectedEvent] = useState(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 ( +
+
+

Pending Approvals

+

Review and take action on event proposals from your {user?.role === 'HOD' ? 'department' : 'institution'}.

+
+ +
+ {isLoading ? ( +
+
+
+ ) : events.length === 0 ? ( +
+ +

Queue is Empty

+

All proposals have been processed. Great work!

+
+ ) : ( + + {currentEvents.map((event) => ( + 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" + )} + > +
+
+ {event.title[0]} +
+
+
+ {event.category === 'CLUB' && ( + + Institutional Club + + )} + + {event.type} + +
+

{event.title}

+ + {event.conflictMessage ? ( +
+ + {event.conflictMessage} +
+ ) : ( +
+
+ + {event.proposer?.fullName || 'Faculty Member'} +
+
+ + {new Date(event.startDate).toLocaleDateString()} +
+
+ )} +
+
+ +
e.stopPropagation()}> + + +
+
+ ))} +
+ )} +
+ + {events.length > 0 && ( + { + setItemsPerPage(val); + setCurrentPage(1); + }} + itemsPerPageOptions={[5, 10, 15, 20]} + /> + )} + + {/* Detail Modal */} + + {selectedEvent && ( +
+ setSelectedEvent(null)} + className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" + /> + +
+
+
+ Proposal Details + + {selectedEvent.institution} + {selectedEvent.category === 'CLUB' && ( + <> + + Institutional Club Event + + )} +
+

{selectedEvent.title}

+
+ +
+ +
+ {/* Description Section */} + {selectedEvent.description && ( +
+
+ + Event Description +
+

+ {selectedEvent.description} +

+
+ )} + +
+
+
+
+ + Schedule +
+

+ {new Date(selectedEvent.startDate).toLocaleString()} +

+
+
+
+ + Venue +
+

{selectedEvent.location}

+
+
+ +
+
+
+ + Proposed By +
+

{selectedEvent.proposer?.fullName || 'Faculty'}

+

{selectedEvent.department}

+
+
+
+ + Target Audience +
+

{selectedEvent.academicYears.join(', ')} Batches

+
+
+
+ + {/* Sponsors Section */} + {selectedEvent.sponsors && selectedEvent.sponsors.length > 0 && ( +
+
+ + Event Sponsors +
+
+ {selectedEvent.sponsors.map((sponsor, idx) => ( + + {sponsor} + + ))} +
+
+ )} + +
+
+
+ + Estimated Budget +
+

₹{selectedEvent.budget?.toLocaleString() || '0'}

+
+
+
+ + Registration +
+

+ {selectedEvent.hasRegistrationFee ? `₹${selectedEvent.registrationFee}` : 'FREE'} +

+
+
+ + {selectedEvent.conflictMessage && ( +
+ +
+

Critical Conflict Detected

+

{selectedEvent.conflictMessage}

+
+
+ )} + +
+ + +
+
+
+
+ )} +
+
+ ); +}; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/AutomationView.tsx b/RIT-EMS-main/frontend/src/components/dashboard/AutomationView.tsx new file mode 100644 index 0000000..4328270 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/AutomationView.tsx @@ -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 ( +
+
+
+

{title}

+

{subtitle}

+
+
+ + +
+
+
+
+ {format(date, 'MMMM yyyy')} +
+
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => ( +
{d}
+ ))} + {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 ( +
+ {format(day, 'd')} +
+ {dayEvents.map(e => ( +
onEventClick?.(e)} + className="bg-brand-glow border border-brand-indigo/10 rounded-lg p-2 transition-all hover:scale-[1.02] cursor-pointer" + > +
{e.title}
+

{e.department}

+
+ ))} +
+
+ ); + })} +
+
+
+ ); +}; + +export const AutomationView: React.FC = () => { + const [sourceDate, setSourceDate] = useState(new Date()); + const [targetDate, setTargetDate] = useState(addMonths(new Date(), 6)); + const [events, setEvents] = useState([]); + 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([]); + const [showReviewModal, setShowReviewModal] = useState(false); + const [selectedEvent, setSelectedEvent] = useState(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 ( +
+
+
+

Event Automation

+

Configure and manage automated department event workflows.

+
+
+ +
+
+ +
+
+
+
+ +
+
+

Source Selection

+

Select branch to clone events from

+
+
+
+
+ +
+ RIT + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+ +
+
+

Target Workspace

+

Automation output destination

+
+
+
+
+ +
+ RIT + +
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+ +
+
+

Automation Safety Protocols Active

+

Cloning institutional events shifts schedules by +/- 6 months (Semester Swap).

+
+
+
+ + +
+
+ +
+ + +
+ + + {selectedEvent && ( +
+ setSelectedEvent(null)} + className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" + /> + +
+
+ + {selectedEvent.category || 'Academic'} Event + +

{selectedEvent.title}

+
+ +
+ +
+
+
+ Start Date +

+ {parseDate(selectedEvent.startDate) ? format(parseDate(selectedEvent.startDate)!, 'PPP') : 'N/A'} +

+
+
+ Venue +

{selectedEvent.location || 'Seminar Hall'}

+
+
+ Department +

{selectedEvent.department}

+
+
+ Academic Year +

{selectedEvent.academicYears?.join(', ') || 'N/A'}

+
+
+ +
+ Proposer Information +
+
+ {selectedEvent.proposer?.fullName?.charAt(0) || 'F'} +
+
+

{selectedEvent.proposer?.fullName || 'Faculty Member'}

+

{selectedEvent.proposer?.email || 'faculty@rit.edu'}

+
+
+
+
+ +
+ +
+
+
+ )} +
+ + + {showReviewModal && ( +
+ setShowReviewModal(false)} + className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" + /> + +
+
+

Review Automated Changes

+

Audit and adjust planned semester-swap dates.

+
+ +
+ +
+
+ {proposedChanges.map((change, idx) => ( +
0 ? "bg-red-50/30 border-red-100" : + change.isWeekend ? "bg-amber-50 border-amber-100" : "bg-white border-slate-100" + )}> +
+
+
+ Event +

{change.title}

+
+
+
+ + + {parseDate(change.originalDate) ? format(parseDate(change.originalDate)!, 'MMM d') : 'N/A'} + + + + {parseDate(change.targetDate) ? format(parseDate(change.targetDate)!, 'MMM d, yyyy') : 'N/A'} + +
+ + {change.isAlreadySwapped && ( + + Already Swapped + + )} +
+
+ +
+ {change.targetConflicts?.length > 0 && change.resolution !== 'skip' && ( +
+ +
+ Conflict Detected + + {change.targetConflicts.length} event(s) already on this date + {change.hasTimeConflict && " (Time Overlap!)"} + +
+
+ )} + +
+ + + +
+ + {/* Weekend Quick-Shift */} + {change.isWeekend && change.resolution !== 'skip' && change.resolution !== 'reschedule' && ( +
+ + +
+ )} +
+
+ + {/* Expanded Conflict / Reschedule Details */} + {(change.resolution === 'reschedule' || (change.targetConflicts?.length > 0 && change.resolution !== 'skip')) && ( + + {change.targetConflicts?.length > 0 && ( +
+ Conflicts at Destination + {change.targetConflicts.map((conf: any) => ( +
+
+
+ {conf.department.substring(0, 2)} +
+ {conf.title} +
+
+ + {format(new Date(conf.startDate), 'hh:mm a')} +
+
+ ))} +
+ )} + + {change.resolution === 'reschedule' && ( +
+
+ + { + 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" + /> +
+
+ + +
+
+ Shift Result + {format(new Date(change.targetDate), 'EEEE')} +
+
+ )} +
+ )} +
+ ))} +
+
+ +
+ + +
+
+
+ )} +
+
+ ); +}; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/ClassManagement.tsx b/RIT-EMS-main/frontend/src/components/dashboard/ClassManagement.tsx new file mode 100644 index 0000000..675a9bf --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/ClassManagement.tsx @@ -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([]); + 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 ( +
+ {/* Header */} +
+
+

Class Management

+

Define institutional academic structures & sections

+
+ +
+ +
+ +
+
+
+ +
+ {/* Left Column - Form */} +
+ +
+
+ +
+

Add New Mapping

+
+ +
+ {/* Institution */} +
+ +
+ {[ + { id: 'RIT', logo: ritLogo, label: 'RIT' }, + { id: 'RSB', logo: rsbLogo, label: 'RSB' } + ].map(inst => ( + + ))} +
+
+ + {/* Department */} +
+ + +
+ + {/* Academic Year */} +
+ + +
+ + {/* Sections */} +
+ +
+
+ + {formData.sections.map(section => ( + + {section} + + + ))} + +
+
+ setNewSection(e.target.value)} + className="flex-1 bg-transparent border-none text-xs font-bold focus:ring-0 p-0" + /> + +
+
+
+ + +
+
+
+ + {/* Right Column - Table */} +
+ +
+
+
+

Academic Structure

+
+ +
+ + 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" + /> +
+
+ +
+ + + + + + + + + + + {isLoading ? ( + + ) : filteredClasses.length === 0 ? ( + + ) : ( + currentClasses.map((item, idx) => ( + + + + + + + )) + )} + +
InstitutionDepartment \ YearConfigured SectionsActions
Loading structure...
No academic structures defined.
+
+ {item.institution} +
+
+
+
+ {item.department.substring(0, 3)} +
+
+
+

{item.department}

+
+ + {item.academicYear} +
+
+
+
+
+ {item.sections.map(section => ( + + {section} + + ))} +
+ + Ready +
+
+
+
+ + +
+
+
+ + { + setItemsPerPage(val); + setCurrentPage(1); + }} + itemsPerPageOptions={[5, 10, 20, 50]} + /> + +
+
+
+ ); +}; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/ClubInstitutionalChecklist.tsx b/RIT-EMS-main/frontend/src/components/dashboard/ClubInstitutionalChecklist.tsx new file mode 100644 index 0000000..dc74336 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/ClubInstitutionalChecklist.tsx @@ -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 = ({ onEventClick }) => { + const [events, setEvents] = useState([]); + 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 }) => ( +
+
+
+

{title}

+

{subtitle}

+
+ +
+ +
+ {events.map((event, i) => { + const isScheduled = getEventStatus(event.name); + return ( + onEventClick({ eventName: event.name, isClubEvent: true })} + className="w-full flex items-center gap-6 group text-left" + > +
+ +
+ {isScheduled ? ( + + ) : ( +
+ )} +
+
+
+

{event.name}

+

+ {isScheduled ? 'Scheduled & Approved' : `Target: ${event.month}`} +

+
+ + ); + })} +
+
+ ); + + return ( +
+
+
+
+

Institutional Checklists

+

Track Institutional Club Requirements

+
+
+ +
+ + +
+
+ ); +}; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/EventDetailsModal.tsx b/RIT-EMS-main/frontend/src/components/dashboard/EventDetailsModal.tsx new file mode 100644 index 0000000..9f8b2e9 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/EventDetailsModal.tsx @@ -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 = ({ 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 ( + +
+ + + + {/* Header */} +
+
+
+ + {event.department} + + + {event.type} + +
+

+ {event.title} +

+
+ + + + {/* Background Decoration */} +
+
+ + {/* Content */} +
+
+
+
+ +
+
+

Date & Time

+

+ {startDate ? format(startDate, 'MMMM d, yyyy') : 'TBD'} +

+

+ {startDate ? format(startDate, 'hh:mm a') : 'TBD'} - {endDate ? format(endDate, 'hh:mm a') : 'TBD'} +

+
+
+ +
+
+ +
+
+

Venue

+

+ {event.location || 'TBD'} +

+

+ {event.institution || 'RIT'} +

+
+
+
+ + {event.description && ( +
+

Event Description

+
+

+ {event.description} +

+
+
+ )} + +
+ {event.academicYears && event.academicYears.length > 0 && ( +
+

+ + Target Audience +

+
+ {event.academicYears.map((year: string) => ( + + {year} + + ))} +
+
+ )} + + {event.targetedSections && event.targetedSections.length > 0 && ( +
+

+ + Sections +

+
+ {event.targetedSections.map((sec: string) => ( + + Section {sec} + + ))} +
+
+ )} +
+ + {event.guestName && ( +
+

Special Guest

+
+
+

{event.guestName}

+ {event.guestSocialProfile && ( + + View Profile + + )} +
+
+
+ )} +
+ + {/* Footer */} +
+ +
+ +
+ + ); +}; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/EventHistory.tsx b/RIT-EMS-main/frontend/src/components/dashboard/EventHistory.tsx new file mode 100644 index 0000000..646e85f --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/EventHistory.tsx @@ -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([]); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('ALL'); + const [selectedEvent, setSelectedEvent] = useState(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 ( +
+ {/* Header Section */} +
+
+
+ +
+
+

Institutional Event History

+

Audit-ready comprehensive event ledger

+
+
+ +
+ +
+
+ + {/* Filters Bar */} +
+
+ + 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" + /> +
+ +
+ {['ALL', 'APPROVED', 'PENDING', 'REJECTED'].map(status => ( + + ))} +
+
+ + {/* Events Ledger */} +
+
+ + + + + + + + + + + + + {currentEvents.map((event, idx) => { + const statusStyle = getStatusStyle(event.status, !!event.conflictMessage); + const StatusIcon = statusStyle.icon; + + return ( + setSelectedEvent(event)} + className="group hover:bg-slate-50/50 transition-colors cursor-pointer" + > + + + + + + + ); + })} + + +
Event DetailsLogisticsStatusBudget
+
+
+ {event.department?.slice(0, 3).toUpperCase()} +
+
+

{event.title}

+

{event.type}

+
+
+
+
+
+ + {format(new Date(event.startDate), 'MMM d, yyyy')} +
+
+ + {event.location} +
+
+
+
+ + {statusStyle.label} +
+
+ ₹{event.budget?.toLocaleString() || '0'} + + +
+
+ + { + setItemsPerPage(val); + setCurrentPage(1); + }} + itemsPerPageOptions={[5, 10, 20, 50]} + /> +
+ + {/* Detail Modal */} + + {selectedEvent && ( +
+ setSelectedEvent(null)} + className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" + /> + +
+
+
+ Event History + + {selectedEvent.institution} +
+

{selectedEvent.title}

+
+ +
+ +
+ {/* Description */} + {selectedEvent.description && ( +
+
+ + Description +
+

+ {selectedEvent.description} +

+
+ )} + +
+
+
+
+ + Date & Time +
+

{new Date(selectedEvent.startDate).toLocaleString()}

+
+
+
+ + Location +
+

{selectedEvent.location}

+
+
+ +
+
+
+ + Department +
+

{selectedEvent.department}

+
+
+
+ + Target Batches +
+

{selectedEvent.academicYears?.join(', ') || 'N/A'}

+
+
+
+ + {/* Sponsors */} + {selectedEvent.sponsors && selectedEvent.sponsors.length > 0 && ( +
+
+ + Event Sponsors +
+
+ {selectedEvent.sponsors.map((sponsor, idx) => ( + + {sponsor} + + ))} +
+
+ )} + +
+
+
+ + Budget +
+

₹{selectedEvent.budget?.toLocaleString() || '0'}

+
+
+
+ + Status +
+

{selectedEvent.status}

+
+
+ + {selectedEvent.rejectionReason && ( +
+

Rejection Reason

+

{selectedEvent.rejectionReason}

+
+ )} +
+
+
+ )} +
+
+ ); +}; + +const ShieldCheck = (props: any) => ( + + + + +); diff --git a/RIT-EMS-main/frontend/src/components/dashboard/EventProposalForm.tsx b/RIT-EMS-main/frontend/src/components/dashboard/EventProposalForm.tsx new file mode 100644 index 0000000..6b8cfa8 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/EventProposalForm.tsx @@ -0,0 +1,1287 @@ +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Calendar, + MapPin, + Building2, + Users, + PlusCircle, + CheckCircle2, + AlertCircle, + ChevronRight, + ShieldCheck, + X, + Split, + Layers, + ArrowRight, + Globe, + Briefcase, + FileText, + Heart, + Wallet, + Zap, + Ticket, + ChevronDown, + AlertTriangle, + FileSpreadsheet +} from 'lucide-react'; +import { useAuth } from '../../context/AuthContext'; +import { cn } from '../../lib/utils'; +import { API_BASE_URL } from '../../lib/config'; +import { INSTITUTIONAL_EVENTS } from '../../constants/institutionalEvents'; +import { getCurrentAcademicYear } from '../../lib/dateUtils'; + +const eventTypes = ['Workshop', 'Seminar', 'Conference', 'Guest Lecture', 'Industrial Visit', 'Techfest', 'Cultural Event', 'Other']; +const ritDepartments = ['AI&DS', 'AI&ML', 'CSE', 'CCE', 'CSBS', 'ECE', 'MECH', 'EE(VLSI)', 'BIOTECH', 'Placement Department', 'H&S Dept']; +const rsbDepartments = ['PGDM']; +const VENUE_LIST = [ + { name: 'N/A', capacity: null }, + { name: 'GB 4th floor auditorium', capacity: 500 }, + { name: 'Wozniak Auditorium', capacity: 180 }, + { name: 'C6-02 Indoor Theatre', capacity: 180 }, + { name: 'H Block Guest Lecture Theatre', capacity: 60 }, + { name: 'Steve Jobs Computer Centre 1', capacity: 120 }, + { name: 'Steve Jobs Computer Centre 2', capacity: 120 }, + { name: 'Others.', capacity: null } +]; +const amenitiesList = [ + 'Sound System', 'Projector', 'Wi-Fi', 'Refreshments', 'Photography', + 'Stage Decor', 'Mementos', 'Certificates', 'Mike/PA System', 'White Board', + 'Lunch', 'Breakfast', 'Guest Transport', 'Remuneration', 'Bouquet', + 'LED Screen', 'Flower decoration', 'Computer lab', 'Board Room', + 'Guest Hospitality', 'Transportation for Guest' +]; + +const CENTRE_MAPPING: Record = { + 'CSE': ['AI Centre', 'Apple iOS App Development Centre', 'Cyber Security Centre'], + 'AI&DS': ['AR, VR & MR Centre', 'Cloud Computing', 'ZF Data Analytics [COE]'], + 'CCE': ['Data Analytics Centre', 'Space Tech Centre'], + 'ECE': ['Centre for IoT', 'RADAR', 'Centre for Image Processing', 'Quantum Computing'], + 'MECH': ['Electric Vehicle & Energy', 'Advanced Manufacturing Centre', 'Robotics and Drone Tech'], + 'Placement Department': ['Placement Cell', 'Incubation cell'] +}; + +interface ClassMapping { + id: number; + institution: string; + department: string; + academicYear: string; + sections: string[]; +} + +interface Event { + id: number; + title: string; + type: string; + status: string; + department: string; + academicYears: string[]; + targetedSections: string[]; +} + +interface EventProposalFormProps { + initialData?: { + department?: string; + eventType?: string; + academicYears?: string[]; + startDate?: string; + isClubEvent?: boolean; + eventName?: string; + id?: number; + isEditMode?: boolean; + endDate?: string; + category?: string; + institution?: string; + venue?: string; + guestName?: string; + socialProfile?: string; + requirements?: string[]; + targetedSections?: string[]; + budget?: string | number; + hasRegistrationFee?: boolean; + registrationFee?: string | number; + targetDepartments?: string[]; + description?: string; + sponsors?: string[]; + centreName?: string; + isPublicEvent?: boolean; + status?: string; + } | null; +} + +export const EventProposalForm: React.FC = ({ initialData }) => { + const { user } = useAuth(); + const isClubVariant = initialData?.isClubEvent || false; + const isFaculty = user?.role === 'FACULTY'; + const isPlacementCell = user?.isPlacementStaff || user?.role === 'PLACEMENT' || user?.department === 'Placement Department'; + + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(''); + const [classes, setClasses] = useState([]); + const [existingEvents, setExistingEvents] = useState([]); + const [classStrengths, setClassStrengths] = useState([]); + + const [eventScope, setEventScope] = useState<'INSTITUTIONAL' | 'DEPARTMENT' | 'CLUB' | 'PLACEMENT' | 'CENTRE'>( + initialData?.isClubEvent ? 'INSTITUTIONAL' : (isPlacementCell ? 'PLACEMENT' : 'DEPARTMENT') + ); + const [conflictData, setConflictData] = useState<{ message: string, conflicts: any[], canOverride: boolean } | null>(null); + + const currentAcademicYear = getCurrentAcademicYear(); + + // Filter institutional events that haven't been approved/completed this year + const availableInstitutionalEvents = INSTITUTIONAL_EVENTS.filter(ie => { + return !existingEvents.some(e => + e.title.toLowerCase() === ie.name.toLowerCase() && + (e.status?.toUpperCase() === 'APPROVED' || e.status?.toUpperCase() === 'COMPLETED') && + e.academicYears?.includes(currentAcademicYear) + ); + }); + + const [formData, setFormData] = useState({ + eventName: initialData?.eventName || '', + startDate: initialData?.startDate || '', + endDate: initialData?.endDate || '', + eventType: isClubVariant ? 'CLUB' : (initialData?.eventType || 'Guest Lecture'), + category: isClubVariant ? 'CLUB' : (initialData?.category || 'ACADEMIC'), + institution: initialData?.institution || 'RIT', + department: initialData?.department || (isFaculty ? (user?.department || 'CSE') : 'CSE'), + academicYears: initialData?.academicYears || [] as string[], + venue: initialData?.venue || 'N/A', + customVenue: '', + guestName: initialData?.guestName || '', + socialProfile: initialData?.socialProfile || '', + requirements: initialData?.requirements || [] as string[], + targetedSections: initialData?.targetedSections || [] as string[], + budget: initialData?.budget || '' as string | number, + customEventType: '', + hasRegistrationFee: initialData?.hasRegistrationFee || false, + registrationFee: initialData?.registrationFee || '' as string | number, + targetDepartments: initialData?.targetDepartments || (isFaculty ? (isPlacementCell ? [] : [user?.department || 'CSE']) : [] as string[]), + allDepts: false, + allBatches: false, + allSections: false, + description: initialData?.description || '', + sponsors: initialData?.sponsors || [] as string[], + centreName: initialData?.centreName || (eventScope === 'CENTRE' ? ((CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || [])[0] || '') : ''), + isPublicEvent: initialData?.isPublicEvent || false, + status: initialData?.status || 'REQUESTED' + }); + + const [newSponsor, setNewSponsor] = useState(''); + const [showAlternateEvent, setShowAlternateEvent] = useState(false); + const [alternateEvent, setAlternateEvent] = useState({ + eventName: '', + startDate: '', + endDate: '', + venue: 'N/A', + customVenue: '', + }); + + useEffect(() => { + // Reset event name and update default event type on scope change + const availableCentres = CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || []; + setFormData(prev => ({ + ...prev, + eventName: '', + eventType: eventScope === 'INSTITUTIONAL' ? 'Institutional' : + eventScope === 'CLUB' ? 'Club' : + eventScope === 'PLACEMENT' ? 'Placement' : '', + category: eventScope === 'CENTRE' ? 'CENTRE' : (eventScope === 'CLUB' ? 'CLUB' : 'ACADEMIC'), + centreName: eventScope === 'CENTRE' ? (availableCentres[0] || '') : prev.centreName + })); + }, [eventScope, user?.department]); + + useEffect(() => { + fetchClasses(); + fetchExistingEvents(); + fetchClassStrengths(); + }, []); + + const fetchClassStrengths = async () => { + try { + const response = await fetch(API_BASE_URL + '/api/admin/users'); + if (response.ok) { + const data = await response.json(); + const strengths = data + .filter((u: any) => u.isClassIncharge) + .map((u: any) => ({ + dept: u.inchargeClass, + year: u.inchargeBatch, + sec: u.inchargeSection, + strength: u.classStrength || 0 + })); + setClassStrengths(strengths); + } + } catch (err) { + console.error('Failed to fetch class strengths:', err); + } + }; + + const fetchClasses = async () => { + try { + const response = await fetch(API_BASE_URL + '/api/classes'); + if (response.ok) { + const data = await response.json(); + setClasses(data); + } + } catch (err) { + console.error('Failed to fetch classes:', err); + } + }; + + const fetchExistingEvents = async () => { + try { + const response = await fetch(API_BASE_URL + '/api/events'); + if (response.ok) { + const data = await response.json(); + setExistingEvents(data); + } + } catch (err) { + console.error('Failed to fetch events:', err); + } + }; + + const availableYears = Array.from(new Set( + classes + .filter(c => c.institution.trim() === formData.institution.trim() && + (eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell) ? true : c.department.trim() === formData.department.trim())) + .map(c => c.academicYear) + )).sort(); + + const availableSections = classes + .filter(c => + c.institution.trim() === formData.institution.trim() && + ((eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') ? ( + eventScope === 'DEPARTMENT' ? c.department.trim() === formData.department.trim() : formData.targetDepartments.includes(c.department.trim()) + ) : true) && + formData.academicYears.includes(c.academicYear.trim()) + ) + .reduce((acc, curr) => [...acc, ...curr.sections], [] as string[]); + + const toggleSelection = (field: 'academicYears' | 'targetedSections' | 'targetDepartments' | 'requirements', value: string) => { + setFormData(prev => { + const current = prev[field] as string[]; + const exists = current.includes(value); + const updated = exists ? current.filter(v => v !== value) : [...current, value]; + + if (field === 'academicYears') return { ...prev, academicYears: updated, allBatches: false }; + if (field === 'targetDepartments') return { ...prev, targetDepartments: updated, allDepts: false }; + if (field === 'targetedSections') return { ...prev, targetedSections: updated, allSections: false }; + return { ...prev, [field]: updated }; + }); + }; + + const addSponsor = () => { + if (newSponsor.trim()) { + setFormData(prev => ({ ...prev, sponsors: [...prev.sponsors, newSponsor.trim()] })); + setNewSponsor(''); + } + }; + + const removeSponsor = (index: number) => { + setFormData(prev => ({ ...prev, sponsors: prev.sponsors.filter((_, i) => i !== index) })); + }; + + const isSectionDisabled = (section: string) => { + if (formData.eventType === 'Other') return false; + return existingEvents.some(e => { + const status = e.status?.toUpperCase(); + const isActive = status === 'APPROVED' || status === 'COMPLETED' || status === 'PENDING_PR' || status === 'REQUESTED'; + return e.type?.toLowerCase() === formData.eventType?.toLowerCase() && + isActive && + e.targetedSections?.includes(section) && + formData.academicYears.some(y => e.academicYears?.includes(y)); + }); + }; + + const leftOutSections = availableSections.filter(s => + !formData.targetedSections.includes(s) && + !isSectionDisabled(s) + ); + + const totalExpectedStrength = React.useMemo(() => { + const deptsToTarget = (eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? formData.targetDepartments : [formData.department]; + const batchesToTarget = formData.allBatches ? availableYears : formData.academicYears; + let total = 0; + + for (const d of deptsToTarget) { + for (const y of batchesToTarget) { + const availableSecsForThisCombo = classes + .filter(c => c.department?.trim() === d?.trim() && c.academicYear?.trim() === y?.trim() && c.institution?.trim() === formData.institution?.trim()) + .reduce((acc, curr) => [...acc, ...curr.sections], [] as string[]); + + const sectionsToTarget = formData.allSections ? availableSecsForThisCombo : formData.targetedSections.filter(s => availableSecsForThisCombo.includes(s)); + + for (const s of sectionsToTarget) { + const match = classStrengths.find(cs => cs.dept === d && cs.year === y && cs.sec === s); + if (match) { + total += match.strength; + } + } + } + } + return total; + }, [formData.targetDepartments, formData.department, formData.academicYears, formData.targetedSections, formData.allBatches, formData.allSections, formData.institution, classStrengths, eventScope, isPlacementCell, availableYears, classes]); + + const filteredVenues = VENUE_LIST.filter(v => v.capacity === null || v.capacity >= totalExpectedStrength); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setError(''); + + const now = new Date(); + const start = new Date(formData.startDate); + const end = new Date(formData.endDate); + + if (start < now) { + setError('Cannot schedule events in the past. Please select a future date and time.'); + setIsSubmitting(false); + return; + } + + if (end <= start) { + setError('End date must be after the start date.'); + setIsSubmitting(false); + return; + } + + const groupRequestId = crypto.randomUUID(); + const batchesToAssign = formData.allBatches ? availableYears : formData.academicYears; + let deptsToCreate = (eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? formData.targetDepartments : [formData.department]; + + // For institutional events, create only ONE global event record + if (eventScope === 'INSTITUTIONAL') { + deptsToCreate = ['Institutional']; + } + + if (eventScope === 'CENTRE' && !formData.centreName) { + setError('Please select a centre.'); + setIsSubmitting(false); + return; + } + + if (deptsToCreate.length === 0) { + setError(`Please select at least one target department for this ${eventScope.toLowerCase()} event.`); + setIsSubmitting(false); + return; + } + + if (eventScope !== 'CENTRE' && batchesToAssign.length === 0) { + setError('Please select at least one target batch.'); + setIsSubmitting(false); + return; + } + + try { + if (initialData?.isEditMode && initialData.id) { + const updatePayload = { + ...formData, + title: formData.eventName, + userId: user?.id, + budget: formData.budget === '' ? 0 : Number(formData.budget), + registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0, + status: formData.status + }; + + const res = await fetch(`${API_BASE_URL}/api/events/${initialData.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updatePayload), + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.message || 'Update failed'); + } + } else { + for (const dept of deptsToCreate) { + // Filter sections that belong to THIS department + const deptSections = (eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') + ? formData.targetedSections.filter(s => + classes.some(c => c.department.trim() === dept.trim() && c.sections.includes(s)) + ) + : []; + + // If it's a placement event and we're selective, only create for depts with selected sections + if (eventScope === 'PLACEMENT' && formData.targetedSections.length > 0 && deptSections.length === 0) { + continue; + } + + const primaryPayload = { + ...formData, + department: dept, + venue: formData.venue === 'Others.' ? formData.customVenue : formData.venue, + groupRequestId, + academicYears: batchesToAssign, + targetedSections: deptSections, + userId: user?.id, + budget: formData.budget === '' ? 0 : Number(formData.budget), + registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0, + category: eventScope === 'CLUB' ? 'CLUB' : (eventScope === 'INSTITUTIONAL' ? 'INSTITUTIONAL' : (eventScope === 'CENTRE' ? 'CENTRE' : 'ACADEMIC')), + eventType: formData.eventType, + centreName: eventScope === 'CENTRE' ? formData.centreName : undefined, + isPublicEvent: eventScope === 'CENTRE' ? formData.isPublicEvent : undefined + }; + + const res = await fetch(API_BASE_URL + '/api/events/propose', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...primaryPayload, + cancelConflicting: false + }), + }); + + if (!res.ok) { + const err = await res.json(); + if (res.status === 409 && err.canOverride) { + setConflictData(err); + setIsSubmitting(false); + return; + } + throw new Error(`[${dept}] ${err.message || 'Submission failed'}`); + } + + if (!formData.allSections && showAlternateEvent && (eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') && leftOutSections.length > 0) { + const deptLeftOut = leftOutSections.filter(s => + classes.some(c => c.department.trim() === dept.trim() && c.sections.includes(s)) + ); + + if (deptLeftOut.length > 0) { + const altPayload = { + ...formData, + department: dept, + groupRequestId, + eventName: alternateEvent.eventName || `${formData.eventName} (Division 2)`, + startDate: alternateEvent.startDate || formData.startDate, + endDate: alternateEvent.endDate || formData.endDate, + venue: alternateEvent.venue === 'Others.' ? alternateEvent.customVenue : alternateEvent.venue, + targetedSections: deptLeftOut, + academicYears: batchesToAssign, + userId: user?.id, + budget: 0, + registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0, + eventType: formData.eventType + }; + + await fetch(API_BASE_URL + '/api/events/propose', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(altPayload), + }); + } + } + } + } + setSubmitted(true); + } catch (err: any) { + setError(err.message); + } finally { + setIsSubmitting(false); + } + }; + + if (submitted) { + return ( +
+ +

Proposals Submitted!

+

Your event proposals have been successfully queued for approval.

+ +
+ ); + } + + return ( +
+ {(user?.role === 'ADMIN' || user?.role === 'PRINCIPAL') && ( + +
+
+ +
+
+

Bulk Import via Excel

+

Have many events? Upload a spreadsheet instead.

+
+
+ +
+ )} + +
+ {!isPlacementCell && ( + + )} + {(isFaculty || isPlacementCell) && ( + + )} + {!isPlacementCell && ( + + )} + {user?.isClubCoordinator && ( + + )} + {isPlacementCell && ( + + )} +
+ +
+
+
+

+ {eventScope === 'INSTITUTIONAL' ? 'Institutional Proposal' : + eventScope === 'CLUB' ? 'Club Proposal' : + eventScope === 'CENTRE' ? 'Centre Event Proposal' : + eventScope === 'PLACEMENT' ? 'Placement Coordinator Schedule' : 'Departmental Proposal'} +

+

+ {isFaculty ? `Faculty of ${user?.department}` : 'Administrative Portal'} +

+
+ +
+ +
+
+ {(eventScope === 'DEPARTMENT' || eventScope === 'CENTRE') ? ( + <> +
+ +
+ + +
+
+
+ + setFormData({...formData, eventName: e.target.value})} + className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all" + placeholder="e.g. Workshop on Generative AI" + /> +
+ + ) : eventScope === 'INSTITUTIONAL' ? ( +
+ +
+ + +
+
+ ) : ( +
+ + setFormData({...formData, eventName: e.target.value, eventType: eventScope === 'CLUB' ? 'Club' : 'Placement'})} + className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all" + placeholder={eventScope === 'PLACEMENT' ? "Enter placement activity name..." : "Enter event title..."} + /> +
+ )} + + {initialData?.isEditMode && user?.role === 'ADMIN' && ( +
+ +
+ + +
+
+ )} + +
+ +
+ + +
+
+ +
+ +
+ + {imagePreview ? ( + Preview + ) : ( +
+
+ +
+ Upload Cover + Max. 2MB (16:9 Recommended) +
+ )} +
+
+
+ + + +
+ ); +}; + +export default CreateDomainView; diff --git a/RIT-EVENT-MANAGEMENT--main/components/CreateEventForm.tsx b/RIT-EVENT-MANAGEMENT--main/components/CreateEventForm.tsx new file mode 100644 index 0000000..8b16803 --- /dev/null +++ b/RIT-EVENT-MANAGEMENT--main/components/CreateEventForm.tsx @@ -0,0 +1,1083 @@ +import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react'; +import { Event, EventDay, Batch } from '../types'; +import { CLUBS } from '../constants'; +import { uploadToSupabase, supabase } from '../supabase'; + +const DEPARTMENTS = ['AIDS', 'CSBS', 'CSE', 'CCE', 'MECH', 'VLSI', 'BIO-TECH', 'AIML', 'ECE', 'H&S']; + +interface CreateEventFormProps { + onCancel: () => void; + onSuccess: (event: Event) => void; + eventToEdit?: Event; + onSupabaseError?: () => void; +} + +const CreateEventForm: React.FC = ({ onCancel, onSuccess, eventToEdit }) => { + const parseDateForInput = (dateStr: string) => { + if (!dateStr) return ''; + try { + const date = new Date(dateStr); + if (isNaN(date.getTime())) return ''; + const yyyy = date.getFullYear(); + const mm = String(date.getMonth() + 1).padStart(2, '0'); + const dd = String(date.getDate()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; + } catch { + return ''; + } + }; + + const mapDBRowToEvent = (row: any): Event => ({ + ...row, + registrationDeadline: row.registration_deadline, + maxParticipants: row.max_participants, + durationDays: row.duration_days, + isTeamEvent: row.is_team_event, + teamSizeLimit: row.team_size_limit, + teamComposition: row.team_composition, + participantType: row.participant_type, + verificationStatus: row.verification_status, + pricingType: row.pricing_type, + deptLimits: deptLimits, + deptSectionLimits: deptSectionLimits, + }); + + // Build dayConfigs from normalized schedule data when editing + const buildDayConfigsFromSchedule = (): EventDay[] => { + if (!eventToEdit?.schedule || eventToEdit.schedule.length === 0) { + return [{ + date: eventToEdit ? parseDateForInput(eventToEdit.date) : '', + batches: [{ id: 1, startTime: '', endTime: '' }], + startTime: '', + endTime: '', + }]; + } + // Group schedule entries by day_idx + const dayMap = new Map(); + eventToEdit.schedule.forEach(s => { + if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []); + dayMap.get(s.day_idx)!.push(s); + }); + const days: EventDay[] = []; + const sortedDayIdxs = Array.from(dayMap.keys()).sort((a, b) => a - b); + sortedDayIdxs.forEach(dayIdx => { + const slots = dayMap.get(dayIdx)!.sort((a, b) => a.batch_idx - b.batch_idx); + const batches: Batch[] = slots.map((s, i) => ({ + id: i + 1, + startTime: s.start_time || '', + endTime: s.end_time || '', + resourcePerson: s.resource_person || undefined, + })); + days.push({ + date: slots[0]?.date ? parseDateForInput(slots[0].date) : '', + batches, + startTime: '', + endTime: '', + }); + }); + return days; + }; + + const [formData, setFormData] = useState({ + title: eventToEdit?.title || '', + location: eventToEdit?.location || '', + date: eventToEdit ? parseDateForInput(eventToEdit.date) : '', + category: eventToEdit?.category || 'TECHNICAL' as Event['category'], + domain: eventToEdit?.domain || '', + pricingType: eventToEdit?.pricingType || 'FREE' as Event['pricingType'], + coordinator: eventToEdit?.coordinator || '', + club: eventToEdit?.club || '', + image: eventToEdit?.image || '', + registrationDeadline: eventToEdit?.registrationDeadline || '', + maxParticipants: eventToEdit?.maxParticipants?.toString() || '', + durationDays: eventToEdit?.durationDays?.toString() || '1', + event_summary: eventToEdit?.event_summary || '', + isTeamEvent: eventToEdit?.isTeamEvent || false, + teamSizeLimit: eventToEdit?.teamSizeLimit?.toString() || '', + teamComposition: eventToEdit?.teamComposition || 'MIXED' as Event['teamComposition'], + participantType: eventToEdit?.participantType || 'INTERNAL' as Event['participantType'], + conducting_dept: eventToEdit?.conducting_dept || '', + refreshment_expense: eventToEdit?.refreshment_expense?.toString() || '', + transportation_expense: eventToEdit?.transportation_expense?.toString() || '', + session_coverage_fee: eventToEdit?.session_coverage_fee?.toString() || '', + total_expense: eventToEdit?.total_expense?.toString() || '0', + dayConfigs: buildDayConfigsFromSchedule(), + }); + + const [allLocations, setAllLocations] = useState([]); + const [bookedEvents, setBookedEvents] = useState([]); + const [locationDropdownOpen, setLocationDropdownOpen] = useState(false); + const locationDropdownRef = useRef(null); + + // Close dropdown on click outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (locationDropdownRef.current && !locationDropdownRef.current.contains(e.target as Node)) { + setLocationDropdownOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + useEffect(() => { + const total = (parseFloat(formData.refreshment_expense) || 0) + (parseFloat(formData.transportation_expense) || 0) + (parseFloat(formData.session_coverage_fee) || 0); + setFormData(prev => ({ ...prev, total_expense: total.toFixed(2) })); + }, [formData.refreshment_expense, formData.transportation_expense, formData.session_coverage_fee]); + + // Sync Launch Date to Day 1 configuration + useEffect(() => { + if (formData.date && formData.dayConfigs.length > 0) { + setFormData(prev => { + const newConfigs = [...prev.dayConfigs]; + newConfigs[0] = { ...newConfigs[0], date: formData.date }; + return { ...prev, dayConfigs: newConfigs }; + }); + } + }, [formData.date]); + + useEffect(() => { + const fetchLogistics = async () => { + const { data: locs } = await supabase.from('locations').select('*'); + if (locs) setAllLocations(locs); + + // Fetch all events that might overlap (Not rejected) + const { data: evts } = await supabase.from('events') + .select('id, location, date, duration_days, verification_status') + .neq('verification_status', 'REJECTED'); + if (evts) setBookedEvents(evts); + }; + fetchLogistics(); + }, []); + + useEffect(() => { + const days = parseInt(formData.durationDays) || 1; + setFormData(prev => { + const newConfigs = [...prev.dayConfigs]; + if (days > newConfigs.length) { + for (let i = newConfigs.length; i < days; i++) { + // Default to next day + let nextDate = ''; + if (newConfigs[i - 1]?.date) { + const d = new Date(newConfigs[i - 1].date); + d.setDate(d.getDate() + 1); + nextDate = d.toISOString().split('T')[0]; + } + newConfigs.push({ date: nextDate, batches: [{ id: 1, startTime: '', endTime: '' }], startTime: '', endTime: '' }); + } + } else if (days < newConfigs.length) { + newConfigs.splice(days); + } + return { ...prev, dayConfigs: newConfigs }; + }); + }, [formData.durationDays]); + + const [deptLimits, setDeptLimits] = useState>(eventToEdit?.deptLimits || {}); + const [deptSectionLimits, setDeptSectionLimits] = useState>>(eventToEdit?.deptSectionLimits || {}); + const [activeDeptForSections, setActiveDeptForSections] = useState(null); + + const [isSubmitting, setIsSubmitting] = useState(false); + const fileInputRef = useRef(null); + + const [availableDomains, setAvailableDomains] = useState([]); + + useEffect(() => { + const fetchDomains = async () => { + const { data } = await supabase.from('domains').select('*').eq('status', 'APPROVED').eq('category', formData.category); + if (data) { + setAvailableDomains(data.sort((a, b) => a.name.localeCompare(b.name))); + } + }; + fetchDomains(); + }, [formData.category]); + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setFormData(prev => ({ ...prev, image: reader.result as string })); + }; + reader.readAsDataURL(file); + } + }; + + const handleDeptLimitChange = (dept: string, value: string) => { + const numValue = parseInt(value); + setDeptLimits(prev => { + const newLimits = { ...prev }; + if (isNaN(numValue) || numValue <= 0) { + delete newLimits[dept]; + } else { + newLimits[dept] = numValue; + } + return newLimits; + }); + }; + + const handleSectionLimitChange = (dept: string, sec: string, value: string) => { + const numValue = parseInt(value); + + setDeptSectionLimits(prev => { + const newSectionLimits = { ...prev }; + if (!newSectionLimits[dept]) { + newSectionLimits[dept] = {}; + } + + if (isNaN(numValue) || numValue <= 0) { + delete newSectionLimits[dept][sec]; + } else { + newSectionLimits[dept][sec] = numValue; + } + + if (Object.keys(newSectionLimits[dept]).length === 0) { + delete newSectionLimits[dept]; + } + + // Calculate overall department limit as the sum of section limits + const sectionSum: number = newSectionLimits[dept] + ? (Object.values(newSectionLimits[dept]) as any[]).reduce((sum: number, val: any) => sum + (Number(val) || 0), 0) + : 0; + + setDeptLimits(prevDept => { + const newDeptLimits = { ...prevDept }; + if (sectionSum > 0) { + newDeptLimits[dept] = sectionSum; + } else { + delete newDeptLimits[dept]; + } + return newDeptLimits; + }); + + return newSectionLimits; + }); + }; + + const handleNumBatchesChange = (dayIndex: number, value: string) => { + const count = parseInt(value); + setFormData(prev => { + const newConfigs = [...prev.dayConfigs]; + const dayBatches = [...newConfigs[dayIndex].batches]; + + if (count >= 1 && count > dayBatches.length) { + for (let i = dayBatches.length; i < count; i++) { + dayBatches.push({ id: i + 1, startTime: '', endTime: '' }); + } + } else { + // Always keep at least 1 batch + dayBatches.splice(Math.max(count, 1)); + } + + newConfigs[dayIndex] = { ...newConfigs[dayIndex], batches: dayBatches }; + return { ...prev, dayConfigs: newConfigs }; + }); + }; + + const handleBatchTimeChange = (dayIndex: number, batchIndex: number, field: 'startTime' | 'endTime', value: string) => { + setFormData(prev => { + const newConfigs = [...prev.dayConfigs]; + const dayBatches = [...newConfigs[dayIndex].batches]; + dayBatches[batchIndex] = { ...dayBatches[batchIndex], [field]: value }; + newConfigs[dayIndex] = { ...newConfigs[dayIndex], batches: dayBatches }; + return { ...prev, dayConfigs: newConfigs }; + }); + }; + + const handleDayDateChange = (dayIndex: number, value: string) => { + setFormData(prev => { + const newConfigs = [...prev.dayConfigs]; + newConfigs[dayIndex] = { ...newConfigs[dayIndex], date: value }; + return { ...prev, dayConfigs: newConfigs }; + }); + }; + + const handleDayTimeChange = (dayIndex: number, field: 'startTime' | 'endTime', value: string) => { + setFormData(prev => { + const newConfigs = [...prev.dayConfigs]; + newConfigs[dayIndex] = { ...newConfigs[dayIndex], [field]: value }; + return { ...prev, dayConfigs: newConfigs }; + }); + }; + + const handleResourcePersonChange = (dayIndex: number, batchIndex: number | null, field: keyof any, value: any) => { + setFormData(prev => { + const newConfigs = [...prev.dayConfigs]; + if (batchIndex === null) { + // Day level (Unified) + const currentRP = newConfigs[dayIndex].resourcePerson || { type: 'INTERNAL', name: '', phone: '', email: '' }; + newConfigs[dayIndex] = { ...newConfigs[dayIndex], resourcePerson: { ...currentRP, [field]: value } }; + } else { + // Batch level + const dayBatches = [...newConfigs[dayIndex].batches]; + const currentRP = dayBatches[batchIndex].resourcePerson || { type: 'INTERNAL', name: '', phone: '', email: '' }; + dayBatches[batchIndex] = { ...dayBatches[batchIndex], resourcePerson: { ...currentRP, [field]: value } }; + newConfigs[dayIndex] = { ...newConfigs[dayIndex], batches: dayBatches }; + } + return { ...prev, dayConfigs: newConfigs }; + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.domain || !formData.image) { + alert("Please complete all required fields including image."); + return; + } + + setIsSubmitting(true); + try { + let finalImageUrl = formData.image; + + if (formData.image.startsWith('data:')) { + const fileName = `events/${Date.now()}_${formData.title.replace(/\s+/g, '_')}.jpg`; + finalImageUrl = await uploadToSupabase(formData.image, fileName); + } + + const totalLimit = parseInt(formData.maxParticipants); + + // Build the DB row — no longer includes time, batches, event_days, dept_limits + const dbRow: Record = { + title: formData.title, + location: formData.location, + date: new Date(formData.dayConfigs[0].date || formData.date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }), + category: formData.category, + domain: formData.domain, + pricing_type: formData.pricingType, + coordinator: formData.coordinator, + club: formData.club, + image: finalImageUrl, + registration_deadline: formData.registrationDeadline || null, + max_participants: isNaN(totalLimit) ? null : totalLimit, + duration_days: parseInt(formData.durationDays) || 1, + event_summary: formData.event_summary, + is_team_event: formData.isTeamEvent, + team_size_limit: parseInt(formData.teamSizeLimit) || null, + team_composition: formData.teamComposition, + participant_type: formData.participantType, + verification_status: eventToEdit?.verificationStatus || (formData.category === 'CENTRE-ACTIVITY' ? 'PENDING_ADMIN' : 'PENDING_HOD'), + status: eventToEdit?.status || 'Scheduled', + refreshment_expense: parseFloat(formData.refreshment_expense) || 0, + transportation_expense: parseFloat(formData.transportation_expense) || 0, + session_coverage_fee: parseFloat(formData.session_coverage_fee) || 0, + total_expense: parseFloat(formData.total_expense) || 0, + conducting_dept: formData.conducting_dept || null, + created_by: (await supabase.auth.getUser()).data.user?.id || null, + request_by_faculty: eventToEdit?.request_by_faculty || new Date().toISOString(), + request_by_hod: eventToEdit?.request_by_HOD || null + }; + + // If editing, update; if creating, insert + let createdEvent: any; + if (eventToEdit?.id) { + const { data, error } = await supabase.from('events').update(dbRow).eq('id', eventToEdit.id).select().single(); + if (error) throw error; + createdEvent = data; + + // Clean up old schedule + resource persons + dept limits when updating (cascade handles RP/sched/limits cleanup) + await supabase.from('event_schedule').delete().eq('event_id', eventToEdit.id); + await supabase.from('resource_persons').delete().eq('event_id', eventToEdit.id); + await supabase.from('event_dept_limits').delete().eq('event_id', eventToEdit.id); + } else { + const { data, error } = await supabase.from('events').insert([dbRow]).select().single(); + if (error) throw error; + createdEvent = data; + } + + // Save schedule rows to event_schedule table + const scheduleRows: any[] = []; + formData.dayConfigs.forEach((day: any, dIdx: number) => { + const formattedDate = day.date + ? new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }) + : ''; + if (day.batches && day.batches.length > 0) { + day.batches.forEach((batch: any, bIdx: number) => { + scheduleRows.push({ + event_id: createdEvent.id, + day_idx: dIdx + 1, + batch_idx: bIdx + 1, + date: formattedDate, + start_time: batch.startTime || '', + end_time: batch.endTime || '', + }); + }); + } else { + // Single unified slot for the day + scheduleRows.push({ + event_id: createdEvent.id, + day_idx: dIdx + 1, + batch_idx: 1, + date: formattedDate, + start_time: day.startTime || '', + end_time: day.endTime || '', + }); + } + }); + + let insertedSchedule: any[] = []; + if (scheduleRows.length > 0) { + const { data: schedData, error: schedError } = await supabase + .from('event_schedule') + .insert(scheduleRows) + .select(); + if (schedError) { + console.error("Schedule save error:", schedError); + } else { + insertedSchedule = schedData || []; + } + } + + // Save Resource Persons with schedule_id linking + const resourcePersonsToSave: any[] = []; + formData.dayConfigs.forEach((day: any, dIdx: number) => { + if (day.batches.length === 0 || (day.batches.length === 1 && !day.batches[0].resourcePerson?.name)) { + if (day.resourcePerson?.name) { + const schedRow = insertedSchedule.find((s: any) => s.day_idx === dIdx + 1 && s.batch_idx === 1); + resourcePersonsToSave.push({ + event_id: createdEvent.id, + day_idx: dIdx + 1, + batch_idx: 1, + schedule_id: schedRow?.id || null, + ...day.resourcePerson + }); + } + } else { + day.batches.forEach((batch: any, bIdx: number) => { + if (batch.resourcePerson?.name) { + const schedRow = insertedSchedule.find((s: any) => s.day_idx === dIdx + 1 && s.batch_idx === bIdx + 1); + resourcePersonsToSave.push({ + event_id: createdEvent.id, + day_idx: dIdx + 1, + batch_idx: bIdx + 1, + schedule_id: schedRow?.id || null, + ...batch.resourcePerson + }); + } + }); + } + }); + + if (resourcePersonsToSave.length > 0) { + const { error: rpError } = await supabase + .from('resource_persons') + .insert(resourcePersonsToSave); + + if (rpError) { + console.error("Resource Person save error:", rpError); + } + } + + // Save department limits to event_dept_limits table + const deptLimitRows = Object.entries(deptLimits).map(([dept, maxSeats]) => ({ + event_id: createdEvent.id, + department: dept, + max_seats: maxSeats, + section_limits: deptSectionLimits[dept] || {}, + })); + + if (deptLimitRows.length > 0) { + const { error: deptLimitsError } = await supabase + .from('event_dept_limits') + .insert(deptLimitRows); + if (deptLimitsError) { + console.error("Dept limits save error:", deptLimitsError); + } + } + + onSuccess(mapDBRowToEvent(createdEvent)); + } catch (err: any) { + console.error("Submission error:", err); + alert(`Failed to save event: ${err?.message || JSON.stringify(err)}`); + } finally { + setIsSubmitting(false); + } + }; + + const categories: Event['category'][] = ['TECHNICAL', 'NON-TECHNICAL', 'WORKSHOP', 'CENTRE-ACTIVITY']; + + return ( +
+
+
+

+ {eventToEdit ? 'Refine Session' : 'Generate Event'} +

+

Coordinator Hub Portal

+
+ +
+ +
+ {/* Section 1: Event Identity */} +
+

+ + 01. Core Identity +

+
+
+ + setFormData({ ...formData, title: e.target.value })} /> +
+
+
+ + setFormData({ ...formData, coordinator: e.target.value })} /> +
+
+ + +
+
+ + +
+
+
+
+ + {/* Section 1.5: Financial Governance (Admin Only View) */} +
+

+ + 01B. Financial Projections +

+
+
+
+ +
+ + setFormData({ ...formData, refreshment_expense: e.target.value })} /> +
+
+
+ +
+ + setFormData({ ...formData, transportation_expense: e.target.value })} /> +
+
+
+ +
+ + setFormData({ ...formData, session_coverage_fee: e.target.value })} /> +
+
+
+ +
+ + +
+
+
+

+ + Financial data is visible to you and administrative auditors only. +

+
+
+ + {/* Section 2: Logistics */} +
+

+ + 02. Logistics & Hosting +

+
+
+
+ + { + const newDate = e.target.value; + setFormData(prev => { + const newConfigs = [...prev.dayConfigs]; + if (newConfigs.length > 0) newConfigs[0] = { ...newConfigs[0], date: newDate }; + return { ...prev, date: newDate, dayConfigs: newConfigs }; + }); + }} /> +
+
+ + setFormData({ ...formData, durationDays: e.target.value })} /> +
+
+
+ + {/* Custom dropdown for proper color support */} +
+ + {locationDropdownOpen && ( +
+ {Array.from(new Set(allLocations.map(l => l.block))).map(blockName => ( +
+
+ {blockName} +
+ {allLocations.filter(l => l.block === blockName).map(loc => { + const currentSelectedDate = formData.dayConfigs[0].date; + const isBooked = bookedEvents.find(ev => { + // Must match the location name + if (!ev.date || ev.location !== loc.name) return false; + // Skip the current event if we're editing it + if (eventToEdit?.id && ev.id === eventToEdit.id) return false; + + // If no date is selected yet, treat ALL existing bookings for this location as conflicts + if (!currentSelectedDate) return true; + + // Date overlap check: does the new event's date range overlap the existing one? + const existingStart = new Date(ev.date); + const existingEnd = new Date(ev.date); + existingEnd.setDate(existingEnd.getDate() + (ev.duration_days || 1)); + + const newStart = new Date(currentSelectedDate); + const newEnd = new Date(currentSelectedDate); + newEnd.setDate(newEnd.getDate() + (parseInt(formData.durationDays) || 1)); + + return newStart < existingEnd && existingStart < newEnd; + }); + + const isPending = isBooked?.verification_status === 'PENDING'; + const isApproved = isBooked?.verification_status === 'APPROVED'; + const isSelected = formData.location === loc.name; + + return ( + + ); + })} +
+ ))} +
+ )} +
+ {/* Hidden required input for form validation */} + {}} /> +
+
+
+ + {/* Section 3: Itinerary */} +
+

+ + 03. Itinerary Scheduling +

+
+ {formData.dayConfigs.map((day, dIdx) => ( +
+
+ Day {dIdx + 1} Configuration +
+ handleDayDateChange(dIdx, e.target.value)} /> + +
+
+ + {day.batches.length === 0 ? ( + /* Unified Day View */ +
+
+
+ Start Time + handleDayTimeChange(dIdx, 'startTime', e.target.value)} /> +
+
+ End Time + handleDayTimeChange(dIdx, 'endTime', e.target.value)} /> +
+
+ {/* Resource Person for Unified Day */} +
+

Resource Person

+
+
+ +
+ + +
+
+
+ + handleResourcePersonChange(dIdx, null, 'name', e.target.value)} /> +
+ {day.resourcePerson?.type === 'EXTERNAL' ? ( +
+
+ + handleResourcePersonChange(dIdx, null, 'college_name', e.target.value)} /> +
+
+ + handleResourcePersonChange(dIdx, null, 'dept', e.target.value)} /> +
+
+ ) : ( +
+ + handleResourcePersonChange(dIdx, null, 'dept', e.target.value)} /> +
+ )} +
+ + handleResourcePersonChange(dIdx, null, 'phone', e.target.value)} /> +
+
+ + handleResourcePersonChange(dIdx, null, 'email', e.target.value)} /> +
+
+
+
+ ) : ( +
+ {day.batches.map((batch, bIdx) => ( +
+
+ Phase 0{batch.id} Configuration +
+ handleBatchTimeChange(dIdx, bIdx, 'startTime', e.target.value)} /> + / + handleBatchTimeChange(dIdx, bIdx, 'endTime', e.target.value)} /> +
+
+ + {/* Resource Person for Batch */} +
+
+ +
+ + +
+
+
+ + handleResourcePersonChange(dIdx, bIdx, 'name', e.target.value)} /> +
+ {batch.resourcePerson?.type === 'EXTERNAL' ? ( +
+
+ + handleResourcePersonChange(dIdx, bIdx, 'college_name', e.target.value)} /> +
+
+ + handleResourcePersonChange(dIdx, bIdx, 'dept', e.target.value)} /> +
+
+ ) : ( +
+ + handleResourcePersonChange(dIdx, bIdx, 'dept', e.target.value)} /> +
+ )} +
+ + handleResourcePersonChange(dIdx, bIdx, 'phone', e.target.value)} /> +
+
+ + handleResourcePersonChange(dIdx, bIdx, 'email', e.target.value)} /> +
+
+
+ ))} +
+ )} +
+ ))} +
+
+ + {/* Section 4: Governance */} +
+

+ + 04. Access Control +

+
+
+
+ + setFormData({ ...formData, registrationDeadline: e.target.value })} /> +
+
+ + 0 + ? 'bg-slate-100 border-slate-100 text-slate-400 cursor-not-allowed' + : 'bg-white border-slate-200 text-slate-900 focus:border-[#004a99]' + }`} + value={formData.maxParticipants} + disabled={Object.keys(deptLimits).length > 0} + onChange={e => setFormData({ ...formData, maxParticipants: e.target.value })} + /> + {Object.keys(deptLimits).length > 0 && ( +

↓ Controlled by Dept Quotas

+ )} +
+
+ +
+
+

Collective Entry

+

Enable Team Registrations

+
+
+ {formData.isTeamEvent && ( +
+ ['-', '+', 'e', 'E'].includes(e.key) && e.preventDefault()} placeholder="Size" className="w-20 bg-slate-50 border border-slate-100 rounded-xl px-3 py-2 text-[10px] font-black" value={formData.teamSizeLimit} onChange={e => setFormData({ ...formData, teamSizeLimit: e.target.value })} /> + +
+ )} + +
+
+ +
+ +
+ {['INTERNAL', 'EXTERNAL', 'BOTH'].map(t => ( + + ))} +
+
+
+
+ + {/* Section 5: Quotas */} +
+

+ + 05. Department Quotas +

+
+ {formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0 && ( +
+ +

Dept quotas locked — Total Capacity is set. Clear it first to use department-level limits.

+
+ )} +
+ {DEPARTMENTS.slice(0, 10).map(dept => { + const hasSectionLimits = deptSectionLimits[dept] && Object.keys(deptSectionLimits[dept]).length > 0; + return ( +
+ {dept} + 0 + ? 'bg-slate-100 text-slate-300 cursor-not-allowed' + : hasSectionLimits + ? 'bg-blue-50 text-[#004a99] cursor-not-allowed' + : 'bg-slate-50 focus:text-[#004a99]' + }`} + value={deptLimits[dept] || ''} + disabled={(formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0) || hasSectionLimits} + onChange={(e) => handleDeptLimitChange(dept, e.target.value)} + /> + +
+ ); + })} +
+ + {/* Per-Department Section Quota Inputs */} + {activeDeptForSections && ( +
+
+
+

+ + Section limits for {activeDeptForSections} +

+

+ Specify capacity for each section. Leaving a section blank will prevent students in that section from registering. +

+
+ +
+
+ {['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'].map(sec => { + const val = deptSectionLimits[activeDeptForSections]?.[sec] ?? ''; + return ( +
+ Sec {sec} + handleSectionLimitChange(activeDeptForSections, sec, e.target.value)} + /> +
+ ); + })} +
+
+ )} +
+
+ + {/* Section 6: Taxonomy */} +
+

+ + 06. Taxonomy & Domain +

+
+
+
+ + +
+
+ + +
+
+
+

Pricing Model

+
+ + +
+
+
+
+ + {/* Section 7: Assets */} +
+

+ + 07. Digital Assets & Summary +

+
+ + + +
+ +