diff --git a/.gitignore b/.gitignore index 69f2338..0cd120b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ **/.env.test.local **/.env.production.local **/.env.production +**/.env.development # Other potential ignores (depending on your setup) **/*.log @@ -68,4 +69,19 @@ # Credentials **/EventManangementSystem_GoogleCreds.json -**/*-service/generated \ No newline at end of file +**/*-service/generated + +# Python +**/.venv +**/__pycache__ +**/*.pyc +**/*.pyo +**/*.pyd +**/*.pyw +**/*.pyz +**/*.pywz +**/*.pyzz +**/*.pywzz +**/*.pyzzz +**/*.pywzzz +**/*.pyzzzz \ No newline at end of file diff --git a/ENV_SETUP.md b/ENV_SETUP.md new file mode 100644 index 0000000..1ee339b --- /dev/null +++ b/ENV_SETUP.md @@ -0,0 +1,219 @@ +# Environment Variables Setup Guide + +This document explains how environment variables are dynamically loaded based on which Docker Compose file you use. + +## Environment File Strategy + +- **Development**: Uses `.env.development` files when running `docker-compose.dev.yaml` +- **Production**: Uses `.env.production` files when running `docker-compose.yaml` + +## Environment Files + +The `.env.development` files are already created in the project. This section documents their structure and contents: + +### 1. Client: `ems-client/.env.development` + +```bash +# API Configuration +# For browser requests, use localhost to access the gateway +NEXT_PUBLIC_API_BASE_URL=http://localhost/api + +# Environment +NODE_ENV=development +NEXT_TELEMETRY_DISABLED=1 +``` + +### 2. Auth Service: `ems-services/auth-service/.env.development` + +```bash +# Database Configuration +DATABASE_URL=postgresql://admin:password@auth-service-db:5432/auth_db + +# JWT Configuration +JWT_SECRET=your-jwt-secret-key-change-in-production +JWT_EXPIRES_IN=24h + +# RabbitMQ Configuration +RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672 + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Gateway URL for inter-service communication +GATEWAY_URL=http://ems-gateway +``` + +### 3. Event Service: `ems-services/event-service/.env.development` + +```bash +# Database Configuration +DATABASE_URL=postgresql://admin:password@event-service-db:5432/event_db + +# JWT Configuration +JWT_SECRET=your-jwt-secret-key-change-in-production +JWT_EXPIRES_IN=24h + +# RabbitMQ Configuration +RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672 + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Gateway URL for inter-service communication +GATEWAY_URL=http://ems-gateway +``` + +### 4. Booking Service: `ems-services/booking-service/.env.development` + +```bash +# Database Configuration +DATABASE_URL=postgresql://admin:password@booking-service-db:5432/booking_db + +# JWT Configuration +JWT_SECRET=your-jwt-secret-key-change-in-production +JWT_EXPIRES_IN=24h + +# RabbitMQ Configuration +RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672 + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Gateway URL for inter-service communication +GATEWAY_URL=http://ems-gateway +``` + +### 5. Feedback Service: `ems-services/feedback-service/.env.development` + +```bash +# Database Configuration +DATABASE_URL=postgresql://admin:password@feedback-service-db:5432/feedback_db + +# JWT Configuration +JWT_SECRET=your-jwt-secret-key-change-in-production +JWT_EXPIRES_IN=24h + +# RabbitMQ Configuration (optional) +RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672 + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Gateway URL for inter-service communication +GATEWAY_URL=http://ems-gateway +``` + +### 6. Notification Service: `ems-services/notification-service/.env.development` + +```bash +# RabbitMQ Configuration +RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672 + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Gateway URL for inter-service communication +GATEWAY_URL=http://ems-gateway +``` + +### 7. Speaker Service: `ems-services/speaker-service/.env.development` + +```bash +# Database Configuration +DATABASE_URL=postgresql://admin:password@speaker-service-db:5432/speaker_db + +# JWT Configuration +JWT_SECRET=your-jwt-secret-key-change-in-production +JWT_EXPIRES_IN=24h + +# RabbitMQ Configuration +RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672 + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Gateway URL for inter-service communication +GATEWAY_URL=http://ems-gateway +``` + +## Modifying Environment Variables + +To modify environment variables for development: + +1. Edit the corresponding `.env.development` file in each service directory +2. Restart the service: `docker-compose -f docker-compose.dev.yaml restart ` + +For production, edit the `.env.production` files instead. + +## Key Differences: Development vs Production + +### Development (`.env.development`) +- Uses Docker service names for inter-service communication (e.g., `http://ems-gateway`) +- Client uses `http://localhost/api` for browser requests +- Database URLs use Docker container names (e.g., `auth-service-db`) +- RabbitMQ URL uses Docker service name (`rabbitmq`) +- `NODE_ENV=development` + +### Production (`.env.production`) +- May use external URLs or load balancer endpoints +- Client API URL points to production domain +- Database URLs use production database hosts +- RabbitMQ URL uses production RabbitMQ host +- `NODE_ENV=production` + +## Important Notes + +1. **File Priority**: Docker Compose `env_file` loads variables first, then `environment` section overrides them. In our setup: + - `env_file` loads from `.env.development` or `.env.production` + - `environment` section only sets `NODE_ENV` and file watching options + +2. **JWT_SECRET**: **Must be changed** in production! Use a strong, random secret. + +3. **Database URLs**: Development uses Docker container names. Production should use actual database server addresses. + +4. **Client API URL**: + - Development: `http://localhost/api` (browser access via host) + - Production: Your production API URL (e.g., `https://api.yourdomain.com/api`) + +5. **Service-to-Service Communication**: All services use `GATEWAY_URL=http://ems-gateway` to communicate via the gateway in Docker network. + +## Verification + +To verify all `.env.development` files exist: + +```bash +find . -name ".env.development" -type f +``` + +You should see 7 files: +- `ems-client/.env.development` +- `ems-services/auth-service/.env.development` +- `ems-services/event-service/.env.development` +- `ems-services/booking-service/.env.development` +- `ems-services/feedback-service/.env.development` +- `ems-services/notification-service/.env.development` +- `ems-services/speaker-service/.env.development` + +## Troubleshooting + +### Environment variables not loading +1. Check file name: Must be exactly `.env.development` (not `.env.development.local` or similar) +2. Check file location: Must be in the service's root directory +3. Verify Docker Compose is reading the file: Check logs for environment variable values + +### API calls going to wrong URL +1. Verify `NEXT_PUBLIC_API_BASE_URL` in `ems-client/.env.development` +2. Restart the client container after changing `.env.development` +3. Clear Next.js cache if needed: `docker-compose -f docker-compose.dev.yaml restart ems-client` + +### Services can't connect to each other +1. Verify `GATEWAY_URL=http://ems-gateway` in all service `.env.development` files +2. Check that services are on the same Docker network (`event-net`) +3. Verify RabbitMQ URL uses Docker service name: `amqp://guest:guest@rabbitmq:5672` + diff --git a/README-DEV-DOCKER.md b/README-DEV-DOCKER.md new file mode 100644 index 0000000..c871049 --- /dev/null +++ b/README-DEV-DOCKER.md @@ -0,0 +1,171 @@ +# Development Docker Setup with Hot-Reload + +This document explains how to use the development Docker configuration that enables hot-reload for all services. + +## Overview + +The development Docker setup (`docker-compose.dev.yaml`) is configured to: +- Mount source code as volumes for immediate file change detection +- Run services in development mode with automatic reloading +- Use nodemon for backend services (TypeScript with hot-reload) +- Use Next.js dev mode for the client (with built-in hot-reload) + +## Prerequisites + +- Docker and Docker Compose installed +- All `.env.development` files are already created and configured for each service + +The `.env.development` files are already present in the project and contain all necessary environment variables for development mode. They are automatically loaded when using `docker-compose.dev.yaml`. + +If you need to modify any environment variables, edit the corresponding `.env.development` file in each service directory. See [ENV_SETUP.md](./ENV_SETUP.md) for details on what each variable does. + +## Usage + +### Starting All Services in Development Mode + +```bash +docker-compose -f docker-compose.dev.yaml up +``` + +### Starting Services in Background + +```bash +docker-compose -f docker-compose.dev.yaml up -d +``` + +### Stopping Services + +```bash +docker-compose -f docker-compose.dev.yaml down +``` + +### Rebuilding Images (if Dockerfiles change) + +```bash +docker-compose -f docker-compose.dev.yaml build +docker-compose -f docker-compose.dev.yaml up +``` + +### Viewing Logs + +```bash +# All services +docker-compose -f docker-compose.dev.yaml logs -f + +# Specific service +docker-compose -f docker-compose.dev.yaml logs -f auth-service-dev +``` + +## Service Ports + +Services are exposed on the following ports (for direct access, bypassing gateway): + +- **Client**: `http://localhost:3001` +- **Auth Service**: `http://localhost:3002` +- **Event Service**: `http://localhost:3003` +- **Booking Service**: `http://localhost:3004` +- **Feedback Service**: `http://localhost:3005` +- **Notification Service**: `http://localhost:3006` +- **Speaker Service**: `http://localhost:3007` +- **Gateway**: `http://localhost:80` +- **RabbitMQ Management**: `http://localhost:15672` (guest/guest) + +## Hot-Reload Behavior + +### Backend Services (Node.js/TypeScript) +- Uses **nodemon** to watch for file changes +- Automatically restarts when TypeScript files in `src/` are modified +- Prisma client is regenerated on container start +- File watching uses polling mode (compatible with macOS/Windows) + +### Frontend (Next.js) +- Uses **Next.js dev mode** with built-in hot-reload +- Fast Refresh for React components +- Automatic compilation on file changes +- File watching uses polling mode + +## Volume Mounts + +The following directories are mounted as volumes (changes reflect immediately): +- Source code directories (`src/`, `app/`, etc.) +- Configuration files +- Prisma schemas + +The following are excluded from mounts (use container's node_modules): +- `node_modules/` - Uses container's installed dependencies +- `dist/` - Build output (for backend services) +- `.next/` - Next.js build output (for client) + +## Development vs Production + +- **Development** (`docker-compose.dev.yaml`): Hot-reload enabled, all dependencies including dev deps +- **Production** (`docker-compose.yaml`): Optimized builds, production dependencies only + +## Troubleshooting + +### File Changes Not Detected + +If file changes aren't being detected: +1. Ensure you're using `docker-compose.dev.yaml` (not the production file) +2. Check that volumes are properly mounted: `docker-compose -f docker-compose.dev.yaml ps` +3. On macOS/Windows, file watching uses polling - there may be a slight delay (1-2 seconds) + +### Services Not Starting + +1. Check logs: `docker-compose -f docker-compose.dev.yaml logs ` +2. Ensure databases are healthy: `docker-compose -f docker-compose.dev.yaml ps` +3. Verify `.env.production` files exist for all services + +### Port Conflicts + +If ports are already in use: +- Stop other services using those ports +- Or modify port mappings in `docker-compose.dev.yaml` + +### Prisma Issues + +If Prisma client generation fails: +- Ensure DATABASE_URL is set correctly +- Check that database containers are running and healthy +- Manually regenerate: `docker-compose -f docker-compose.dev.yaml exec auth-service-dev npx prisma generate` + +## Environment Variables + +The `docker-compose.dev.yaml` uses `.env.development` files for all environment variables. These files are automatically loaded based on which Docker Compose file you run: + +- **Development**: `docker-compose.dev.yaml` → Uses `.env.development` files +- **Production**: `docker-compose.yaml` → Uses `.env.production` files + +### Required Environment Variables + +All environment variables should be defined in each service's `.env.development` file. See [ENV_SETUP.md](./ENV_SETUP.md) for complete documentation. + +**Key Variables for Development:** + +#### All Backend Services +- `DATABASE_URL` - Database connection URL (uses Docker service names) +- `RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672` - RabbitMQ connection +- `GATEWAY_URL=http://ems-gateway` - Gateway URL for inter-service communication +- `JWT_SECRET` - JWT signing secret (**must be set**) +- `PORT=3000` - Service port +- `NODE_ENV=development` - Development mode + +#### Client +- `NEXT_PUBLIC_API_BASE_URL=http://localhost/api` - API base URL for browser requests +- `NODE_ENV=development` - Development mode + +#### Docker Compose Overrides +The `docker-compose.dev.yaml` only sets: +- `NODE_ENV=development` - Overrides .env file +- `CHOKIDAR_USEPOLLING=true` - File watching (macOS/Windows compatibility) +- `WATCHPACK_POLLING=true` - Next.js file watching + +All other variables come from `.env.development` files. + +## Notes + +- The first startup may take longer as dependencies are installed +- Database migrations are not automatically run in dev mode - run manually if needed +- Development volumes are separate from production volumes (suffixed with `-dev`) +- Service-to-service communication uses Docker service names (e.g., `http://ems-gateway`) +- Client browser requests use `http://localhost/api` (routed through the gateway) diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..21c30d4 --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,329 @@ +# Development Docker Compose Configuration +# This file enables hot-reload for all services +# Usage: docker-compose -f docker-compose.dev.yaml up + +# This section defines all the containers (services) that make up your application. +services: + ######################################### + # APPLICATION SERVICES + ######################################### + ems-client: + build: + context: ./ems-client + dockerfile: Dockerfile.dev + container_name: ems-client-dev + env_file: + - ./ems-client/.env.development + environment: + - NODE_ENV=development + - WATCHPACK_POLLING=true + - CHOKIDAR_USEPOLLING=true + - CHOKIDAR_INTERVAL=1000 + volumes: + - ./ems-client:/app + - /app/node_modules + - /app/.next + networks: + - event-net + ports: + - "3001:3000" + command: npm run dev + + ems-gateway: + build: + context: ./ems-gateway + dockerfile: Dockerfile + container_name: ems-gateway-dev + ports: + - "80:80" + volumes: + - ./ems-gateway/nginx.conf:/etc/nginx/nginx.conf + networks: + - event-net + depends_on: + - ems-client + - auth-service + - event-service + - booking-service + - speaker-service + - feedback-service + - notification-service + + ######################################### + # BACKEND MICROSERVICES + ######################################### + auth-service: &base-service-dev + build: + context: ./ems-services/auth-service + dockerfile: Dockerfile.dev + args: + - DATABASE_URL=postgresql://admin:password@auth-service-db:5432/auth_db + container_name: auth-service-dev + restart: unless-stopped + env_file: + - ./ems-services/auth-service/.env.development + environment: + - NODE_ENV=development + - CHOKIDAR_USEPOLLING=true + - CHOKIDAR_INTERVAL=1000 + volumes: + - ./ems-services/auth-service:/app + - /app/node_modules + - /app/dist + - /app/prisma + networks: + - event-net + ports: + - "3002:3000" + command: sh -c "npx prisma generate && npm run dev" + depends_on: + auth-service-db: + condition: service_healthy + rabbitmq: + condition: service_started + + event-service: + <<: *base-service-dev + build: + context: ./ems-services/event-service + dockerfile: Dockerfile.dev + args: + - DATABASE_URL=postgresql://admin:password@event-service-db:5432/event_db + container_name: event-service-dev + env_file: + - ./ems-services/event-service/.env.development + environment: + - NODE_ENV=development + - CHOKIDAR_USEPOLLING=true + - CHOKIDAR_INTERVAL=1000 + volumes: + - ./ems-services/event-service:/app + - /app/node_modules + - /app/dist + - /app/prisma + ports: + - "3003:3000" + command: sh -c "npx prisma generate && npm run dev" + networks: + - event-net + depends_on: + event-service-db: + condition: service_healthy + rabbitmq: + condition: service_started + auth-service: + condition: service_started + + booking-service: + <<: *base-service-dev + build: + context: ./ems-services/booking-service + dockerfile: Dockerfile.dev + args: + - DATABASE_URL=postgresql://admin:password@booking-service-db:5432/booking_db + container_name: booking-service-dev + env_file: + - ./ems-services/booking-service/.env.development + environment: + - NODE_ENV=development + - CHOKIDAR_USEPOLLING=true + - CHOKIDAR_INTERVAL=1000 + volumes: + - ./ems-services/booking-service:/app + - /app/node_modules + - /app/dist + - /app/prisma + ports: + - "3004:3000" + command: sh -c "npx prisma generate && npm run dev" + networks: + - event-net + depends_on: + booking-service-db: + condition: service_healthy + rabbitmq: + condition: service_started + + feedback-service: + <<: *base-service-dev + build: + context: ./ems-services/feedback-service + dockerfile: Dockerfile.dev + args: + - DATABASE_URL=postgresql://admin:password@feedback-service-db:5432/feedback_db + container_name: feedback-service-dev + env_file: + - ./ems-services/feedback-service/.env.development + environment: + - NODE_ENV=development + - CHOKIDAR_USEPOLLING=true + - CHOKIDAR_INTERVAL=1000 + volumes: + - ./ems-services/feedback-service:/app + - /app/node_modules + - /app/dist + - /app/prisma + ports: + - "3005:3000" + command: sh -c "npx prisma generate && npm run dev" + networks: + - event-net + depends_on: + feedback-service-db: + condition: service_healthy + rabbitmq: + condition: service_started + + notification-service: + build: + context: ./ems-services/notification-service + dockerfile: Dockerfile.dev + container_name: notification-service-dev + env_file: + - ./ems-services/notification-service/.env.development + environment: + - NODE_ENV=development + - CHOKIDAR_USEPOLLING=true + - CHOKIDAR_INTERVAL=1000 + volumes: + - ./ems-services/notification-service:/app + - /app/node_modules + - /app/dist + ports: + - "3006:3000" + command: npm run dev + networks: + - event-net + depends_on: + rabbitmq: + condition: service_started + + speaker-service: + <<: *base-service-dev + build: + context: ./ems-services/speaker-service + dockerfile: Dockerfile.dev + args: + - DATABASE_URL=postgresql://admin:password@speaker-service-db:5432/speaker_db + container_name: speaker-service-dev + env_file: + - ./ems-services/speaker-service/.env.development + environment: + - NODE_ENV=development + - CHOKIDAR_USEPOLLING=true + - CHOKIDAR_INTERVAL=1000 + volumes: + - ./ems-services/speaker-service:/app + - /app/node_modules + - /app/dist + - /app/prisma + ports: + - "3007:3000" + command: sh -c "npx prisma generate && npm run dev" + networks: + - event-net + depends_on: + speaker-service-db: + condition: service_healthy + rabbitmq: + condition: service_started + + ######################################### + # DATABASES FOR MICROSERVICES + ######################################### + + auth-service-db: &base-service-db + image: postgres:14-alpine + container_name: auth-service-db-dev + environment: + - POSTGRES_USER=admin + - POSTGRES_PASSWORD=password + - POSTGRES_DB=auth_db + volumes: + - auth-service-data-dev:/var/lib/postgresql/data + networks: + - event-net + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U admin" ] + interval: 10s + timeout: 5s + retries: 5 + + event-service-db: + <<: *base-service-db + container_name: event-service-db-dev + environment: + - POSTGRES_USER=admin + - POSTGRES_PASSWORD=password + - POSTGRES_DB=event_db + volumes: + - event-service-data-dev:/var/lib/postgresql/data + + booking-service-db: + <<: *base-service-db + container_name: booking-service-db-dev + environment: + - POSTGRES_USER=admin + - POSTGRES_PASSWORD=password + - POSTGRES_DB=booking_db + volumes: + - booking-service-data-dev:/var/lib/postgresql/data + + feedback-service-db: + <<: *base-service-db + container_name: feedback-service-db-dev + environment: + - POSTGRES_USER=admin + - POSTGRES_PASSWORD=password + - POSTGRES_DB=feedback_db + volumes: + - feedback-service-data-dev:/var/lib/postgresql/data + + speaker-service-db: + <<: *base-service-db + container_name: speaker-service-db-dev + environment: + - POSTGRES_USER=admin + - POSTGRES_PASSWORD=password + - POSTGRES_DB=speaker_db + volumes: + - speaker-service-data-dev:/var/lib/postgresql/data + + ######################################### + # INFRASTRUCTURE SERVICES + ######################################### + rabbitmq: + image: rabbitmq:3.9-management-alpine + container_name: rabbitmq-dev + ports: + - "5672:5672" + - "15672:15672" + environment: + - RABBITMQ_DEFAULT_USER=guest + - RABBITMQ_DEFAULT_PASS=guest + volumes: + - rabbitmq_data-dev:/var/lib/rabbitmq/ # Persistent storage for RabbitMQ data + - rabbitmq_log-dev:/var/log/rabbitmq/ # Persistent storage for RabbitMQ + networks: + - event-net + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 5s + timeout: 10s + retries: 5 + start_period: 20s # Give RabbitMQ time to start up before checking + +# This section defines the custom network for our services to communicate. +networks: + event-net: + driver: bridge + +# This section defines a volume for persisting data. +volumes: + auth-service-data-dev: + event-service-data-dev: + booking-service-data-dev: + feedback-service-data-dev: + speaker-service-data-dev: + rabbitmq_data-dev: + rabbitmq_log-dev: diff --git a/docker-compose.yaml b/docker-compose.yaml index 592ac18..81ce444 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -61,8 +61,6 @@ services: args: - DATABASE_URL=postgresql://admin:password@event-service-db:5432/event_db container_name: event-service - environment: - - TZ=America/Chicago env_file: ./ems-services/event-service/.env.production networks: @@ -134,11 +132,6 @@ services: container_name: speaker-service env_file: ./ems-services/speaker-service/.env.production - environment: - - RABBITMQ_URL=amqp://rabbitmq:5672 - - UPLOAD_DIR=/app/uploads - volumes: - - speaker-uploads:/app/uploads # Persistent storage for uploaded materials networks: - event-net depends_on: @@ -244,6 +237,5 @@ volumes: booking-service-data: feedback-service-data: speaker-service-data: - speaker-uploads: # Persistent storage for speaker uploaded materials rabbitmq_data: rabbitmq_log: diff --git a/docs/er-diagram/postgres@ems.png b/docs/er-diagram/postgres@ems.png new file mode 100644 index 0000000..c7091c8 Binary files /dev/null and b/docs/er-diagram/postgres@ems.png differ diff --git a/ems-client/Dockerfile.dev b/ems-client/Dockerfile.dev new file mode 100644 index 0000000..cb90fec --- /dev/null +++ b/ems-client/Dockerfile.dev @@ -0,0 +1,24 @@ +# Development Dockerfile for Next.js Client +FROM alpine:latest +RUN apk update && apk add nodejs npm && rm -rf /var/cache/apk/* + +WORKDIR /app + +# Set development environment +ENV NODE_ENV=development +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + + +# Copy package files first for better caching +COPY package.json package-lock.json ./ + +# Install all dependencies (including dev dependencies) +RUN npm ci + +EXPOSE 3000 + +# The actual source code will be mounted as a volume +# Command will be overridden in docker-compose to use next dev +CMD ["npm", "run", "dev"] diff --git a/ems-client/app/dashboard/admin/events/[id]/page.tsx b/ems-client/app/dashboard/admin/events/[id]/page.tsx index bd032bb..a7a82df 100644 --- a/ems-client/app/dashboard/admin/events/[id]/page.tsx +++ b/ems-client/app/dashboard/admin/events/[id]/page.tsx @@ -1,17 +1,349 @@ 'use client'; -import { EventDetailsPage } from "@/components/events/EventDetailsPage"; +import { useAuth } from "@/lib/auth-context"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { + ArrowLeft, + Calendar, + MapPin, + Clock, + Users, + AlertCircle, + Edit, + CheckCircle, + XCircle, + Ban +} from "lucide-react"; +import { useRouter, useParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { useLogger } from "@/lib/logger/LoggerProvider"; +import { eventAPI } from "@/lib/api/event.api"; +import { EventResponse, EventStatus } from "@/lib/api/types/event.types"; import { withAdminAuth } from "@/components/hoc/withAuth"; +import { RejectionModal } from "@/components/admin/RejectionModal"; + +const LOGGER_COMPONENT_NAME = 'AdminEventDetailsPage'; + +const statusColors = { + [EventStatus.DRAFT]: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200', + [EventStatus.PENDING_APPROVAL]: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200', + [EventStatus.PUBLISHED]: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', + [EventStatus.REJECTED]: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', + [EventStatus.CANCELLED]: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', + [EventStatus.COMPLETED]: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200' +}; + +function AdminEventDetailsPage() { + const { user } = useAuth(); + const router = useRouter(); + const params = useParams(); + const logger = useLogger(); + const eventId = params.id as string; + + const [event, setEvent] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [actionLoading, setActionLoading] = useState(false); + const [isRejectionModalOpen, setIsRejectionModalOpen] = useState(false); + + useEffect(() => { + if (eventId) { + loadEvent(); + } + }, [eventId]); + + const loadEvent = async () => { + try { + setIsLoading(true); + logger.debug(LOGGER_COMPONENT_NAME, 'Loading event details', { eventId }); + + const response = await eventAPI.getEventById(eventId); + + if (response.success) { + setEvent(response.data); + logger.debug(LOGGER_COMPONENT_NAME, 'Event loaded successfully'); + } else { + throw new Error('Failed to load event'); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to load event'; + setError(errorMessage); + logger.error(LOGGER_COMPONENT_NAME, 'Failed to load event', err instanceof Error ? err : new Error(String(err))); + } finally { + setIsLoading(false); + } + }; + + const handleApprove = async () => { + if (!event) return; + + try { + setActionLoading(true); + logger.info(LOGGER_COMPONENT_NAME, 'Approving event', { eventId: event.id }); + + await eventAPI.approveEvent(event.id); + + // Reload event to show updated status + await loadEvent(); + } catch (err) { + logger.error(LOGGER_COMPONENT_NAME, 'Failed to approve event', err as Error); + setError('Failed to approve event'); + } finally { + setActionLoading(false); + } + }; + + const handleRejectConfirm = async (rejectionReason: string) => { + if (!event) return; + + try { + setActionLoading(true); + logger.info(LOGGER_COMPONENT_NAME, 'Rejecting event', { eventId: event.id }); + + await eventAPI.rejectEvent(event.id, { rejectionReason }); + + setIsRejectionModalOpen(false); + await loadEvent(); + } catch (err) { + logger.error(LOGGER_COMPONENT_NAME, 'Failed to reject event', err as Error); + setError('Failed to reject event'); + throw err; + } finally { + setActionLoading(false); + } + }; + + const handleCancel = async () => { + if (!event) return; + + try { + setActionLoading(true); + logger.info(LOGGER_COMPONENT_NAME, 'Cancelling event', { eventId: event.id }); + + await eventAPI.cancelEvent(event.id); + + await loadEvent(); + } catch (err) { + logger.error(LOGGER_COMPONENT_NAME, 'Failed to cancel event', err as Error); + setError('Failed to cancel event'); + } finally { + setActionLoading(false); + } + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error || !event) { + return ( +
+ + + +

Error Loading Event

+

{error}

+ +
+
+
+ ); + } -const AdminEventDetailsPage = () => { return ( - +
+
+
+
+
+ +

+ Event Details +

+
+
+
+
+ +
+ + +
+
+ {event.name} + + {event.status.replace('_', ' ')} + +
+ +
+ + + {event.status === EventStatus.PENDING_APPROVAL && ( + <> + + + + )} + + {event.status === EventStatus.PUBLISHED && ( + + )} +
+
+
+ + + {event.rejectionReason && ( +
+

+ + Rejection Reason: +

+

{event.rejectionReason}

+
+ )} + +
+

Description

+

{event.description}

+
+ +
+
+

Category

+

{event.category}

+
+ +
+

Speaker ID

+

{event.speakerId}

+
+ +
+

+ + Venue +

+

{event.venue.name}

+

{event.venue.address}

+

+ Hours: {event.venue.openingTime} - {event.venue.closingTime} +

+
+ +
+

+ + Capacity +

+

{event.venue.capacity} attendees

+
+ +
+

+ + Event Dates +

+
+
+

Start Date

+

+ {new Date(event.bookingStartDate).toLocaleString()} +

+
+
+

End Date

+

+ {new Date(event.bookingEndDate).toLocaleString()} +

+
+
+
+ +
+

Created At

+

+ {new Date(event.createdAt).toLocaleString()} +

+
+ +
+

Last Updated

+

+ {new Date(event.updatedAt).toLocaleString()} +

+
+
+ + {event.bannerImageUrl && ( +
+

Banner Image

+ {event.name} +
+ )} +
+
+ + {/* Rejection Modal */} + setIsRejectionModalOpen(false)} + onConfirm={handleRejectConfirm} + eventName={event?.name} + isLoading={actionLoading} + /> +
+
); -}; +} + +export default withAdminAuth(AdminEventDetailsPage); -export default withAdminAuth(AdminEventDetailsPage); \ No newline at end of file diff --git a/ems-client/app/dashboard/admin/events/page.tsx b/ems-client/app/dashboard/admin/events/page.tsx index d5a362d..965547a 100644 --- a/ems-client/app/dashboard/admin/events/page.tsx +++ b/ems-client/app/dashboard/admin/events/page.tsx @@ -27,7 +27,6 @@ import { import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { useLogger } from "@/lib/logger/LoggerProvider"; -import { EventJoinInterface } from '@/components/attendance/EventJoinInterface'; import { eventAPI } from "@/lib/api/event.api"; import { EventResponse, EventStatus, EventFilters } from "@/lib/api/types/event.types"; import { withAdminAuth } from "@/components/hoc/withAuth"; @@ -51,7 +50,7 @@ function EventManagementPage() { const [searchTerm, setSearchTerm] = useState(''); const [selectedStatus, setSelectedStatus] = useState('ALL'); const [selectedTimeframe, setSelectedTimeframe] = useState('ALL'); - + // Rejection modal state const [isRejectionModalOpen, setIsRejectionModalOpen] = useState(false); const [eventToReject, setEventToReject] = useState<{ id: string; name: string } | null>(null); @@ -418,14 +417,6 @@ function EventManagementPage() { {event.name} - {event.status.replace('_', ' ')} @@ -465,8 +456,8 @@ function EventManagementPage() { {/* Actions */}
- + {event.status === EventStatus.DRAFT && ( + + )} + {event.status === EventStatus.PENDING_APPROVAL && ( <>
- - {/* Event Join Interface - Only show for published events */} - {event.status === EventStatus.PUBLISHED && ( -
- -
- )} ))} diff --git a/ems-client/app/dashboard/admin/page.tsx b/ems-client/app/dashboard/admin/page.tsx index aedbfc6..1998b66 100644 --- a/ems-client/app/dashboard/admin/page.tsx +++ b/ems-client/app/dashboard/admin/page.tsx @@ -18,54 +18,52 @@ import { Eye, Edit, Trash2, - Ticket + Ticket, + Loader2 } from "lucide-react"; import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useState, useCallback } from "react"; import {useLogger} from "@/lib/logger/LoggerProvider"; import {withAdminAuth} from "@/components/hoc/withAuth"; +import { eventAPI } from "@/lib/api/event.api"; +import { EventResponse, EventStatus } from "@/lib/api/types/event.types"; -// Mock data for development -const mockStats = { - totalUsers: 156, - totalEvents: 8, - activeEvents: 3, - flaggedUsers: 2, - totalRegistrations: 342, - upcomingEvents: 3 -}; - -const mockRecentEvents = [ - { - id: '1', - title: 'Tech Conference 2024', - status: 'published', - registrations: 45, - capacity: 100, - startDate: '2024-02-15', - endDate: '2024-02-17' - }, - { - id: '2', - title: 'Design Workshop', - status: 'draft', - registrations: 12, - capacity: 50, - startDate: '2024-02-20', - endDate: '2024-02-21' - }, - { - id: '3', - title: 'AI Summit', - status: 'published', - registrations: 89, - capacity: 150, - startDate: '2024-03-01', - endDate: '2024-03-03' - } -]; +const LOGGER_COMPONENT_NAME = 'AdminDashboard'; + +// Interface for recent events display +interface RecentEvent { + id: string; + title: string; + status: string; + registrations?: number; + capacity?: number; + startDate: string; + endDate: string; +} -const mockFlaggedUsers = [ +// Interface for flagged users +interface FlaggedUser { + id: string; + name: string; + email: string; + reason: string; + flaggedAt: string; +} + +// Interface for dashboard stats +interface DashboardStats { + totalUsers: number | null; + totalEvents: number; + activeEvents: number; + flaggedUsers: number; + totalRegistrations: number | null; + upcomingEvents: number; +} + +// TODO: Flagged users feature requires backend implementation +// This mock data should be replaced with an API call once the backend endpoint is available +// Expected endpoint: GET /api/admin/users/flagged +const mockFlaggedUsers: FlaggedUser[] = [ { id: '1', name: 'John Doe', @@ -82,16 +80,120 @@ const mockFlaggedUsers = [ } ]; -const LOGGER_COMPONENT_NAME = 'AdminDashboard'; - function AdminDashboard() { const { user, logout } = useAuth(); const router = useRouter(); const logger = useLogger(); + const [recentEvents, setRecentEvents] = useState([]); + const [eventsLoading, setEventsLoading] = useState(true); + const [eventsError, setEventsError] = useState(null); + const [flaggedUsers] = useState(mockFlaggedUsers); // TODO: Replace with API call + const [stats, setStats] = useState({ + totalUsers: null, + totalEvents: 0, + activeEvents: 0, + flaggedUsers: mockFlaggedUsers.length, + totalRegistrations: null, + upcomingEvents: 0 + }); + const [statsLoading, setStatsLoading] = useState(true); + + // Fetch dashboard stats + const fetchDashboardStats = useCallback(async () => { + try { + setStatsLoading(true); + logger.debug(LOGGER_COMPONENT_NAME, 'Fetching dashboard stats'); + + // Fetch all events to get stats + const eventsResponse = await eventAPI.getAllEvents({ + limit: 1000, // Large limit to get all events + page: 1 + }); + + if (eventsResponse.success && eventsResponse.data) { + const allEvents = eventsResponse.data.events; + const totalEvents = eventsResponse.data.total || allEvents.length; + const activeEvents = allEvents.filter(e => e.status === EventStatus.PUBLISHED).length; + + // Count upcoming events (events with bookingStartDate in the future) + const now = new Date(); + const upcomingEvents = allEvents.filter(e => { + const startDate = new Date(e.bookingStartDate); + return startDate > now && e.status === EventStatus.PUBLISHED; + }).length; + + // Total registrations: Currently no admin endpoint to get all bookings across all events + // TODO: Implement admin endpoint: GET /api/admin/bookings/stats or similar + // For now, we'll leave it as null and show "N/A" + let totalRegistrations: number | null = null; + + setStats({ + totalUsers: null, // TODO: No API endpoint available yet + totalEvents, + activeEvents, + flaggedUsers: mockFlaggedUsers.length, + totalRegistrations, + upcomingEvents + }); + + logger.info(LOGGER_COMPONENT_NAME, 'Dashboard stats fetched successfully', { + totalEvents, + activeEvents, + upcomingEvents, + totalRegistrations + }); + } + } catch (error) { + logger.error(LOGGER_COMPONENT_NAME, 'Failed to fetch dashboard stats', error as Error); + } finally { + setStatsLoading(false); + } + }, [logger]); + + // Fetch recent events function + const fetchRecentEvents = useCallback(async () => { + try { + setEventsLoading(true); + setEventsError(null); + logger.debug(LOGGER_COMPONENT_NAME, 'Fetching recent events'); + + // Fetch all events, sorted by creation date (most recent first) + // Limit to 3 most recent events for dashboard + const response = await eventAPI.getAllEvents({ + limit: 3, + page: 1 + }); + + if (response.success && response.data) { + // Sort by createdAt descending (most recent first) and take first 3 + const sortedEvents = [...response.data.events] + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, 3) + .map((event: EventResponse): RecentEvent => ({ + id: event.id, + title: event.name, + status: event.status.toLowerCase(), + capacity: event.venue?.capacity, + startDate: event.bookingStartDate, + endDate: event.bookingEndDate + })); + + setRecentEvents(sortedEvents); + logger.info(LOGGER_COMPONENT_NAME, 'Recent events fetched successfully', { count: sortedEvents.length }); + } + } catch (error) { + logger.error(LOGGER_COMPONENT_NAME, 'Failed to fetch recent events', error as Error); + setEventsError('Failed to load recent events'); + } finally { + setEventsLoading(false); + } + }, [logger]); useEffect(() => { logger.debug(LOGGER_COMPONENT_NAME, 'Admin dashboard loaded', { userRole: user?.role }); - }, [user, logger]); + fetchDashboardStats(); + fetchRecentEvents(); + }, [user, logger, fetchDashboardStats, fetchRecentEvents]); // Loading and auth checks are handled by the HOC @@ -163,10 +265,21 @@ function AdminDashboard() { -
{mockStats.totalUsers}
-

- {mockStats.flaggedUsers} flagged -

+ {statsLoading ? ( +
+ + Loading... +
+ ) : ( + <> +
+ {stats.totalUsers !== null ? stats.totalUsers : 'N/A'} +
+

+ {stats.totalUsers !== null ? `${stats.flaggedUsers} flagged` : 'API endpoint needed'} +

+ + )}
@@ -178,10 +291,19 @@ function AdminDashboard() { -
{mockStats.totalEvents}
-

- {mockStats.activeEvents} active -

+ {statsLoading ? ( +
+ + Loading... +
+ ) : ( + <> +
{stats.totalEvents}
+

+ {stats.activeEvents} active +

+ + )}
@@ -193,25 +315,45 @@ function AdminDashboard() { -
{mockStats.totalRegistrations}
-

- Across all events -

+ {statsLoading ? ( +
+ + Loading... +
+ ) : ( + <> +
+ {stats.totalRegistrations !== null ? stats.totalRegistrations : 'N/A'} +
+

+ Across all events +

+ + )}
- Flagged Users + Upcoming Events - + -
{mockStats.flaggedUsers}
-

- Need review -

+ {statsLoading ? ( +
+ + Loading... +
+ ) : ( + <> +
{stats.upcomingEvents}
+

+ Scheduled soon +

+ + )}
@@ -305,59 +447,124 @@ function AdminDashboard() { -
- {mockRecentEvents.map((event) => ( -
-
-

{event.title}

-
- {event.registrations}/{event.capacity} registrations - +
+ Loading events... +
+ ) : eventsError ? ( +
+

{eventsError}

+ +
+ ) : recentEvents.length === 0 ? ( +
+

No recent events found

+
+ ) : ( +
+ {recentEvents.map((event) => ( +
+
+

{event.title}

+
+ {event.capacity && ( + Capacity: {event.capacity} + )} + + {event.status} + +
+

+ {new Date(event.startDate).toLocaleDateString()} - {new Date(event.endDate).toLocaleDateString()} +

+
+
+ +
-
- - -
-
- ))} -
+ ))} +
+ )}
{/* Flagged Users Alert */} - {mockFlaggedUsers.length > 0 && ( + {/* TODO: Replace with real API call once backend endpoint is available */} + {flaggedUsers.length > 0 && ( Flagged Users Requiring Review + + {/* Note: Currently using mock data. Backend API endpoint needed. */} + {flaggedUsers.length} user{flaggedUsers.length !== 1 ? 's' : ''} flagged +
- {mockFlaggedUsers.map((user) => ( -
+ {flaggedUsers.map((flaggedUser) => ( +
-

{user.name}

-

{user.email}

-

Reason: {user.reason}

+

{flaggedUser.name}

+

{flaggedUser.email}

+

Reason: {flaggedUser.reason}

+

+ Flagged: {new Date(flaggedUser.flaggedAt).toLocaleDateString()} +

- -
diff --git a/ems-client/app/dashboard/attendee/events/page.tsx b/ems-client/app/dashboard/attendee/events/page.tsx index 5ab7b89..431ad4b 100644 --- a/ems-client/app/dashboard/attendee/events/page.tsx +++ b/ems-client/app/dashboard/attendee/events/page.tsx @@ -11,25 +11,25 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useRouter } from 'next/navigation'; import { useLogger } from '@/lib/logger/LoggerProvider'; -import { - Search, - Filter, - Calendar, - MapPin, - Users, - Clock, - CheckCircle, +import { + Search, + Filter, + Calendar, + MapPin, + Users, + Clock, + CheckCircle, AlertCircle, Loader2, Star, Eye, - Ticket + Ticket, + ArrowLeft } from 'lucide-react'; const LOGGER_COMPONENT_NAME = 'AttendeeEventsPage'; import { EventResponse } from '@/lib/api/types/event.types'; -import { EventJoinInterface } from '@/components/attendance/EventJoinInterface'; interface Event extends EventResponse {} @@ -48,7 +48,7 @@ export default function AttendeeEventsPage() { const { user, isAuthenticated } = useAuth(); const router = useRouter(); const logger = useLogger(); - + const [events, setEvents] = useState([]); const [filteredEvents, setFilteredEvents] = useState([]); const [loading, setLoading] = useState(true); @@ -68,7 +68,7 @@ export default function AttendeeEventsPage() { router.push('/login'); return; } - + loadEvents(); loadUserBookings(); }, [isAuthenticated, router]); @@ -80,7 +80,7 @@ export default function AttendeeEventsPage() { // Filter by search term if (filters.searchTerm) { const searchLower = filters.searchTerm.toLowerCase(); - filtered = filtered.filter(event => + filtered = filtered.filter(event => event.name.toLowerCase().includes(searchLower) || event.description.toLowerCase().includes(searchLower) || event.category.toLowerCase().includes(searchLower) || @@ -102,7 +102,7 @@ export default function AttendeeEventsPage() { if (filters.dateRange) { const now = new Date(); const filterDate = new Date(); - + switch (filters.dateRange) { case 'today': filterDate.setHours(0, 0, 0, 0); @@ -126,10 +126,10 @@ export default function AttendeeEventsPage() { try { setLoading(true); logger.info(LOGGER_COMPONENT_NAME, 'Loading published events'); - + const response = await eventAPI.getPublishedEvents(); setEvents(response.data?.events || []); - + logger.info(LOGGER_COMPONENT_NAME, 'Events loaded successfully'); } catch (error) { logger.error(LOGGER_COMPONENT_NAME, 'Failed to load events', error as Error); @@ -141,36 +141,36 @@ export default function AttendeeEventsPage() { const loadUserBookings = async () => { try { logger.info(LOGGER_COMPONENT_NAME, 'Loading user bookings'); - + // Load both bookings and tickets to get complete picture const [bookingsResponse, ticketsResponse] = await Promise.all([ bookingAPI.getUserBookings(), ticketAPI.getUserTickets() ]); - + const bookings = bookingsResponse.data?.bookings || []; const tickets = ticketsResponse.data || []; - + // Create a map of eventId -> true for events the user has booked const bookingMap: { [eventId: string]: boolean } = {}; - + // Add bookings bookings.forEach((booking: any) => { bookingMap[booking.eventId] = true; }); - + // Add tickets (in case bookings API doesn't return all data) tickets.forEach((ticket: any) => { if (ticket.eventId) { bookingMap[ticket.eventId] = true; } }); - + setUserBookings(bookingMap); - logger.info(LOGGER_COMPONENT_NAME, 'User bookings loaded successfully', { - bookingsCount: bookings.length, + logger.info(LOGGER_COMPONENT_NAME, 'User bookings loaded successfully', { + bookingsCount: bookings.length, ticketsCount: tickets.length, - uniqueEvents: Object.keys(bookingMap).length + uniqueEvents: Object.keys(bookingMap).length }); } catch (error) { logger.error(LOGGER_COMPONENT_NAME, 'Failed to load user bookings', error as Error); @@ -210,7 +210,7 @@ export default function AttendeeEventsPage() { } catch (error: any) { setBookingStatus(prev => ({ ...prev, [eventId]: 'error' })); - + // Handle specific error cases if (error?.response?.status === 409) { logger.info(LOGGER_COMPONENT_NAME, 'User already has booking for this event (409)'); @@ -231,17 +231,12 @@ export default function AttendeeEventsPage() { }; - const getBookingButtonText = (eventId: string, event: Event) => { - // Check if event is expired - if (isEventExpired(event)) { - return 'Event Ended'; - } - + const getBookingButtonText = (eventId: string) => { // Check if user already has a booking for this event if (userBookings[eventId]) { return 'Already Booked ✓'; } - + const status = bookingStatus[eventId]; switch (status) { case 'loading': return 'Booking...'; @@ -251,17 +246,12 @@ export default function AttendeeEventsPage() { } }; - const getBookingButtonVariant = (eventId: string, event: Event) => { - // Check if event is expired - if (isEventExpired(event)) { - return 'secondary'; - } - + const getBookingButtonVariant = (eventId: string) => { // Check if user already has a booking for this event if (userBookings[eventId]) { return 'secondary'; } - + const status = bookingStatus[eventId]; switch (status) { case 'success': return 'default'; @@ -275,14 +265,14 @@ export default function AttendeeEventsPage() { if (userBookings[eventId]) { return true; } - + // Secondary check: bookingStatus (temporary UI state) // Only consider 'success' if we don't have userBookings data yet return bookingStatus[eventId] === 'success'; }; - const isButtonDisabled = (eventId: string, event: Event) => { - return isEventExpired(event) || isEventBooked(eventId) || bookingStatus[eventId] === 'loading'; + const isButtonDisabled = (eventId: string) => { + return isEventBooked(eventId) || bookingStatus[eventId] === 'loading'; }; const handleFilterChange = (key: keyof EventFilters, value: string) => { @@ -315,33 +305,22 @@ export default function AttendeeEventsPage() { }; }; - // Utility function to check if event is expired/ended - const isEventExpired = (event: Event) => { - const now = new Date(); - const eventEndDate = new Date(event.bookingEndDate); - return eventEndDate < now || event.status === 'COMPLETED' || event.status === 'CANCELLED'; - }; - - // Utility function to check if event is upcoming - const isEventUpcoming = (event: Event) => { - const now = new Date(); - const eventStartDate = new Date(event.bookingStartDate); - return eventStartDate > now && event.status === 'PUBLISHED'; - }; - - // Utility function to check if event is currently running - const isEventRunning = (event: Event) => { - const now = new Date(); - const eventStartDate = new Date(event.bookingStartDate); - const eventEndDate = new Date(event.bookingEndDate); - return eventStartDate <= now && eventEndDate >= now && event.status === 'PUBLISHED'; - }; - if (loading) { return (
-

Available Events

+
+ +

Available Events

+
{[...Array(3)].map((_, i) => ( @@ -364,14 +343,25 @@ export default function AttendeeEventsPage() {
{/* Header */}
-
-

- Discover Events -

-

Find and book amazing events happening around you

+
+ +
+

+ Discover Events +

+

Find and book amazing events happening around you

+
- -
+ {event.name}
{isBooked && ( @@ -558,25 +530,9 @@ export default function AttendeeEventsPage() { BOOKED )} - {isEventExpired(event) ? ( - - EXPIRED - - ) : isEventRunning(event) ? ( - - - LIVE - - ) : isEventUpcoming(event) ? ( - - - UPCOMING - - ) : ( - - {event.status} - - )} + + {event.status} +
@@ -584,13 +540,13 @@ export default function AttendeeEventsPage() { {eventTime.date} - + {/* Description */}

{event.description}

- + {/* Event Details */}
@@ -598,19 +554,19 @@ export default function AttendeeEventsPage() { Time: {eventTime.time}
- +
Venue: {event.venue.name}
- +
Capacity: {event.venue.capacity} people
- +
Category: @@ -631,18 +587,13 @@ export default function AttendeeEventsPage() { {/* Booking Button */} - - {/* Event Join Interface - Only show for booked events */} - {isBooked && ( -
- -
- )} ); })} -
-
- )} - - {/* Expired Events */} - {filteredEvents.filter(event => isEventExpired(event)).length > 0 && ( -
-

- - Past Events ({filteredEvents.filter(event => isEventExpired(event)).length}) -

-
- {filteredEvents.filter(event => isEventExpired(event)).map((event) => { - const eventTime = formatEventTime(event.bookingStartDate, event.bookingEndDate); - const isBooked = userBookings[event.id]; - - return ( - - -
-
- {event.name} - -
-
- {isBooked && ( - - - ATTENDED - - )} - - EXPIRED - -
-
- - - {eventTime.date} - -
- - - {/* Description */} -

- {event.description} -

- - {/* Event Details */} -
-
- - Time: - {eventTime.time} -
- -
- - Venue: - {event.venue.name} -
- -
- - Capacity: - {event.venue.capacity} people -
- -
- - Category: - {event.category} -
-
- - {/* Booking Status */} - {isBooked && ( -
-
- - You attended this event! -
-
- )} - - {/* Disabled Button for Expired Events */} - -
-
- ); - })} -
-
- )}
)}
diff --git a/ems-client/app/dashboard/attendee/page.tsx b/ems-client/app/dashboard/attendee/page.tsx index f068e23..945664d 100644 --- a/ems-client/app/dashboard/attendee/page.tsx +++ b/ems-client/app/dashboard/attendee/page.tsx @@ -18,76 +18,39 @@ import { TrendingUp, Award, MapPin, - Users + Users, + Loader2 } from "lucide-react"; import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useEffect, useState, useCallback } from "react"; import {useLogger} from "@/lib/logger/LoggerProvider"; import {withUserAuth} from "@/components/hoc/withAuth"; +import { bookingAPI, ticketAPI } from "@/lib/api/booking.api"; +import { eventAPI } from "@/lib/api/event.api"; +import { BookingResponse, TicketResponse } from "@/lib/api/types/booking.types"; +import { EventResponse } from "@/lib/api/types/event.types"; -// Mock data for development -const mockStats = { - registeredEvents: 8, - upcomingEvents: 3, - attendedEvents: 5, - ticketsPurchased: 12, - activeTickets: 4, - usedTickets: 8, - pointsEarned: 1250, - pointsThisMonth: 300, - upcomingThisWeek: 2, - nextWeekEvents: 1 -}; - -const mockUpcomingEvents = [ - { - id: '1', - title: 'TechConf 2024', - date: '2024-01-15', - time: '9:00 AM', - location: 'Convention Center', - attendees: 500, - status: 'registered', - ticketType: 'VIP Pass' - }, - { - id: '2', - title: 'React Workshop', - date: '2024-01-18', - time: '2:00 PM', - location: 'Tech Hub', - attendees: 50, - status: 'registered', - ticketType: 'Standard' - }, - { - id: '3', - title: 'Design Thinking Summit', - date: '2024-01-22', - time: '10:00 AM', - location: 'Innovation Lab', - attendees: 200, - status: 'interested', - ticketType: null - } -]; - -const mockRecentRegistrations = [ - { - id: '1', - event: 'DevSummit 2024', - date: '2024-01-10', - status: 'confirmed', - ticketType: 'Early Bird' - }, - { - id: '2', - event: 'UX Conference', - date: '2024-01-08', - status: 'confirmed', - ticketType: 'Standard' - } -]; +// Interface for display data +interface UpcomingEventDisplay { + id: string; + title: string; + date: string; + time: string; + location: string; + attendees?: number; + status: 'registered' | 'interested'; + ticketType?: string | null; + eventId: string; +} + +interface RecentRegistrationDisplay { + id: string; + event: string; + date: string; + status: string; + ticketType?: string; + bookingId: string; +} const LOGGER_COMPONENT_NAME = 'AttendeeDashboard'; @@ -96,9 +59,187 @@ function AttendeeDashboard() { const router = useRouter(); const logger = useLogger(); + // State for real data + const [loading, setLoading] = useState(true); + const [stats, setStats] = useState({ + registeredEvents: 0, + upcomingEvents: 0, + attendedEvents: 0, + ticketsPurchased: 0, + activeTickets: 0, + usedTickets: 0, + pointsEarned: 0, + pointsThisMonth: 0, + upcomingThisWeek: 0, + nextWeekEvents: 0 + }); + const [upcomingEvents, setUpcomingEvents] = useState([]); + const [recentRegistrations, setRecentRegistrations] = useState([]); + const [error, setError] = useState(null); + + // Helper function to format date + const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); + }; + + // Helper function to format time + const formatTime = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); + }; + + // Helper function to check if event is upcoming + const isUpcoming = (dateString: string): boolean => { + return new Date(dateString) > new Date(); + }; + + // Helper function to check if event is this week + const isThisWeek = (dateString: string): boolean => { + const eventDate = new Date(dateString); + const now = new Date(); + const weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); + return eventDate >= now && eventDate <= weekFromNow; + }; + + // Helper function to check if event is next week + const isNextWeek = (dateString: string): boolean => { + const eventDate = new Date(dateString); + const now = new Date(); + const weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); + const twoWeeksFromNow = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000); + return eventDate >= weekFromNow && eventDate <= twoWeeksFromNow; + }; + + // Fetch all dashboard data + const fetchDashboardData = useCallback(async () => { + if (!user?.id) return; + + try { + setLoading(true); + setError(null); + logger.debug(LOGGER_COMPONENT_NAME, 'Fetching dashboard data', { userId: user.id }); + + // Fetch bookings, tickets, and published events in parallel + const [bookingsResponse, ticketsResponse, publishedEventsResponse] = await Promise.all([ + bookingAPI.getUserBookings(), + ticketAPI.getUserTickets(), + eventAPI.getPublishedEvents() + ]); + + const bookings = bookingsResponse.success ? bookingsResponse.data.bookings : []; + const tickets = ticketsResponse.success ? ticketsResponse.data : []; + const publishedEvents = publishedEventsResponse.success ? publishedEventsResponse.data.events : []; + + logger.debug(LOGGER_COMPONENT_NAME, 'Data fetched', { + bookings: bookings.length, + tickets: tickets.length, + events: publishedEvents.length + }); + + // Calculate stats + const now = new Date(); + const currentMonth = now.getMonth(); + const currentYear = now.getFullYear(); + + const upcomingBookings = bookings.filter(booking => { + const event = publishedEvents.find(e => e.id === booking.eventId); + return event && isUpcoming(event.bookingStartDate); + }); + + const upcomingThisWeek = upcomingBookings.filter(booking => { + const event = publishedEvents.find(e => e.id === booking.eventId); + return event && isThisWeek(event.bookingStartDate); + }); + + const nextWeekBookings = upcomingBookings.filter(booking => { + const event = publishedEvents.find(e => e.id === booking.eventId); + return event && isNextWeek(event.bookingStartDate); + }); + + const activeTickets = tickets.filter(t => t.status === 'ISSUED'); + const usedTickets = tickets.filter(t => t.status === 'SCANNED'); + + // Calculate points (simple calculation: 100 points per booking, 50 per ticket) + const pointsEarned = (bookings.length * 100) + (tickets.length * 50); + const bookingsThisMonth = bookings.filter(b => { + const bookingDate = new Date(b.createdAt); + return bookingDate.getMonth() === currentMonth && bookingDate.getFullYear() === currentYear; + }); + const pointsThisMonth = (bookingsThisMonth.length * 100) + (bookingsThisMonth.length * 50); + + setStats({ + registeredEvents: bookings.length, + upcomingEvents: upcomingBookings.length, + attendedEvents: usedTickets.length, + ticketsPurchased: tickets.length, + activeTickets: activeTickets.length, + usedTickets: usedTickets.length, + pointsEarned, + pointsThisMonth, + upcomingThisWeek: upcomingThisWeek.length, + nextWeekEvents: nextWeekBookings.length + }); + + // Build upcoming events display + const upcomingEventsDisplay: UpcomingEventDisplay[] = upcomingBookings + .slice(0, 3) + .reduce((acc, booking) => { + const event = publishedEvents.find(e => e.id === booking.eventId); + if (!event) return acc; + + acc.push({ + id: booking.id, + eventId: event.id, + title: event.name, + date: formatDate(event.bookingStartDate), + time: formatTime(event.bookingStartDate), + location: event.venue.name, + attendees: event.venue.capacity, + status: 'registered' as const, + ticketType: 'Standard' // Default, could be enhanced with ticket type from booking + }); + return acc; + }, []); + + setUpcomingEvents(upcomingEventsDisplay); + + // Build recent registrations display + const recentBookings = bookings + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, 5); + + const recentRegistrationsDisplay: RecentRegistrationDisplay[] = recentBookings.map(booking => { + const event = publishedEvents.find(e => e.id === booking.eventId); + const ticket = tickets.find(t => t.bookingId === booking.id); + + return { + id: booking.id, + bookingId: booking.id, + event: event?.name || 'Unknown Event', + date: formatDate(booking.createdAt), + status: booking.status.toLowerCase(), + ticketType: ticket ? 'Standard' : undefined + }; + }); + + setRecentRegistrations(recentRegistrationsDisplay); + + logger.info(LOGGER_COMPONENT_NAME, 'Dashboard data loaded successfully'); + } catch (err) { + logger.error(LOGGER_COMPONENT_NAME, 'Failed to load dashboard data', err as Error); + setError('Failed to load dashboard data. Please refresh the page.'); + } finally { + setLoading(false); + } + }, [user, logger]); + useEffect(() => { logger.debug(LOGGER_COMPONENT_NAME, 'Attendee dashboard loaded', { userRole: user?.role }); - }, [user, logger]); + if (user?.id) { + fetchDashboardData(); + } + }, [user, logger, fetchDashboardData]); // Loading and auth checks are handled by the HOC @@ -169,10 +310,19 @@ function AttendeeDashboard() { -
{mockStats.registeredEvents}
-

- {mockStats.upcomingEvents} upcoming -

+ {loading ? ( +
+ + Loading... +
+ ) : ( + <> +
{stats.registeredEvents}
+

+ {stats.upcomingEvents} upcoming +

+ + )}
@@ -184,10 +334,19 @@ function AttendeeDashboard() { -
{mockStats.ticketsPurchased}
-

- {mockStats.activeTickets} active -

+ {loading ? ( +
+ + Loading... +
+ ) : ( + <> +
{stats.ticketsPurchased}
+

+ {stats.activeTickets} active +

+ + )}
@@ -199,10 +358,19 @@ function AttendeeDashboard() { -
{mockStats.pointsEarned}
-

- {mockStats.pointsThisMonth} this month -

+ {loading ? ( +
+ + Loading... +
+ ) : ( + <> +
{stats.pointsEarned}
+

+ {stats.pointsThisMonth} this month +

+ + )}
@@ -214,10 +382,19 @@ function AttendeeDashboard() { -
{mockStats.upcomingThisWeek}
-

- {mockStats.nextWeekEvents} next week -

+ {loading ? ( +
+ + Loading... +
+ ) : ( + <> +
{stats.upcomingThisWeek}
+

+ {stats.nextWeekEvents} next week +

+ + )}
@@ -285,41 +462,56 @@ function AttendeeDashboard() { -
- {mockUpcomingEvents.map((event) => ( -
-
-

{event.title}

-
- - - {event.date} at {event.time} - - - - {event.location} - + {loading ? ( +
+ +
+ ) : upcomingEvents.length === 0 ? ( +
+ +

No upcoming events

+
+ ) : ( +
+ {upcomingEvents.map((event) => ( +
+
+

{event.title}

+
+ + + {event.date} at {event.time} + + + + {event.location} + +
+ {event.ticketType && ( + + {event.ticketType} + + )} +
+
+ + {event.status} + +
- {event.ticketType && ( - - {event.ticketType} - - )} -
-
- - {event.status} - -
-
- ))} -
+ ))} +
+ )}
@@ -335,34 +527,56 @@ function AttendeeDashboard() { -
- {mockRecentRegistrations.map((registration) => ( -
-
-

{registration.event}

-
- - {registration.ticketType} - - - Registered on {registration.date} - + {error && ( +
+

{error}

+
+ )} + {loading ? ( +
+ +
+ ) : recentRegistrations.length === 0 ? ( +
+ +

No recent registrations

+
+ ) : ( +
+ {recentRegistrations.map((registration) => ( +
+
+

{registration.event}

+
+ {registration.ticketType && ( + + {registration.ticketType} + + )} + + Registered on {registration.date} + +
+
+
+ + {registration.status} + +
-
- - {registration.status} - - -
-
- ))} -
+ ))} +
+ )} diff --git a/ems-client/app/dashboard/attendee/tickets/page.tsx b/ems-client/app/dashboard/attendee/tickets/page.tsx index 07675e0..f26890c 100644 --- a/ems-client/app/dashboard/attendee/tickets/page.tsx +++ b/ems-client/app/dashboard/attendee/tickets/page.tsx @@ -10,6 +10,7 @@ import { useRouter } from 'next/navigation'; import { useLogger } from '@/lib/logger/LoggerProvider'; import { TicketResponse } from '@/lib/api/types/booking.types'; import { QRCodeSVG } from 'qrcode.react'; +import { ArrowLeft } from 'lucide-react'; const LOGGER_COMPONENT_NAME = 'AttendeeTicketsPage'; @@ -17,7 +18,7 @@ export default function AttendeeTicketsPage() { const { user, isAuthenticated } = useAuth(); const router = useRouter(); const logger = useLogger(); - + const [tickets, setTickets] = useState([]); const [loading, setLoading] = useState(true); @@ -26,7 +27,7 @@ export default function AttendeeTicketsPage() { router.push('/login'); return; } - + loadTickets(); }, [isAuthenticated, router]); @@ -34,10 +35,10 @@ export default function AttendeeTicketsPage() { try { setLoading(true); logger.info(LOGGER_COMPONENT_NAME, 'Loading user tickets'); - + const response = await ticketAPI.getUserTickets(); setTickets(response.data || []); - + logger.info(LOGGER_COMPONENT_NAME, 'Tickets loaded successfully'); } catch (error) { logger.error(LOGGER_COMPONENT_NAME, 'Failed to load tickets', error as Error); @@ -54,7 +55,7 @@ export default function AttendeeTicketsPage() { 'REVOKED': 'destructive', 'EXPIRED': 'outline' } as const; - + return ( {status} @@ -66,36 +67,22 @@ export default function AttendeeTicketsPage() { return new Date(expiresAt) < new Date(); }; - // Utility function to check if ticket's event is expired/ended - const isTicketEventExpired = (ticket: TicketResponse) => { - if (!ticket.event) return false; - const now = new Date(); - const eventEndDate = new Date(ticket.event.bookingEndDate); - return eventEndDate < now; - }; - - // Utility function to check if ticket's event is upcoming - const isTicketEventUpcoming = (ticket: TicketResponse) => { - if (!ticket.event) return false; - const now = new Date(); - const eventStartDate = new Date(ticket.event.bookingStartDate); - return eventStartDate > now; - }; - - // Utility function to check if ticket's event is currently running - const isTicketEventRunning = (ticket: TicketResponse) => { - if (!ticket.event) return false; - const now = new Date(); - const eventStartDate = new Date(ticket.event.bookingStartDate); - const eventEndDate = new Date(ticket.event.bookingEndDate); - return eventStartDate <= now && eventEndDate >= now; - }; - if (loading) { return (
-

My Tickets

+
+ +

My Tickets

+
{[...Array(3)].map((_, i) => ( @@ -117,16 +104,28 @@ export default function AttendeeTicketsPage() { return (
-
-

My Tickets

-
- +

My Tickets

+
+
+ - + + +
+ ); + } + + const canEdit = event.status === EventStatus.DRAFT || event.status === EventStatus.REJECTED; + const canSubmit = event.status === EventStatus.DRAFT || event.status === EventStatus.REJECTED; + return ( - +
+
+
+
+
+ +

+ Event Details +

+
+
+
+
+ +
+ + +
+
+ {event.name} + + {event.status.replace('_', ' ')} + +
+
+ {canEdit && ( + + )} + {canSubmit && ( + + )} +
+
+
+ + {event.rejectionReason && ( +
+

Rejection Reason:

+

{event.rejectionReason}

+
+ )} + +
+

Description

+

{event.description}

+
+ +
+
+

Category

+

{event.category}

+
+ +
+

+ + Venue +

+

{event.venue.name}

+

{event.venue.address}

+
+ +
+

+ + Event Dates +

+

+ {new Date(event.bookingStartDate).toLocaleString()} -
+ {new Date(event.bookingEndDate).toLocaleString()} +

+
+ +
+

+ + Capacity +

+

{event.venue.capacity} attendees

+
+
+
+
+
+
); -}; +} + +export default withSpeakerAuth(SpeakerEventDetailsPage); -export default withSpeakerAuth(SpeakerEventDetailsPage); \ No newline at end of file diff --git a/ems-client/app/dashboard/speaker/events/page.tsx b/ems-client/app/dashboard/speaker/events/page.tsx index 57fc36c..0f35b23 100644 --- a/ems-client/app/dashboard/speaker/events/page.tsx +++ b/ems-client/app/dashboard/speaker/events/page.tsx @@ -22,20 +22,15 @@ import { Play, Pause, Archive, - AlertCircle, - Mail, - CheckCircle, - XCircle + AlertCircle } from "lucide-react"; import {useRouter} from "next/navigation"; import {useEffect, useState} from "react"; import {useLogger} from "@/lib/logger/LoggerProvider"; -import { EventJoinInterface } from '@/components/attendance/EventJoinInterface'; import {eventAPI} from "@/lib/api/event.api"; import {EventResponse, EventStatus, EventFilters} from "@/lib/api/types/event.types"; import {withSpeakerAuth} from "@/components/hoc/withAuth"; -import {speakerApiClient, SpeakerInvitation} from "@/lib/api/speaker.api"; const statusColors = { [EventStatus.DRAFT]: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200', @@ -53,13 +48,11 @@ function SpeakerEventManagementPage() { const router = useRouter(); const logger = useLogger(); const [searchTerm, setSearchTerm] = useState(''); + const [selectedStatus, setSelectedStatus] = useState('ALL'); const [selectedTimeframe, setSelectedTimeframe] = useState('ALL'); - const [activeTab, setActiveTab] = useState<'my-events' | 'invited-events'>('my-events'); // API state management const [events, setEvents] = useState([]); - const [invitations, setInvitations] = useState([]); - const [invitedEvents, setInvitedEvents] = useState>(new Map()); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [actionLoading, setActionLoading] = useState(null); @@ -70,8 +63,10 @@ function SpeakerEventManagementPage() { totalPages: 0 }); - // Load events from API - Show all published events in "All Events" tab + // Load events from API const loadEvents = async () => { + if (!user?.id) return; + try { setLoading(true); setError(null); @@ -79,13 +74,12 @@ function SpeakerEventManagementPage() { const filters: EventFilters = { page: pagination.page, limit: pagination.limit, - // Note: Published events API only returns PUBLISHED events, so status filter is not needed + status: selectedStatus !== 'ALL' ? selectedStatus : undefined }; - logger.debug(LOGGER_COMPONENT_NAME, 'Loading all published events with filters', filters); + logger.debug(LOGGER_COMPONENT_NAME, 'Loading events with filters', filters); - // Use getPublishedEvents to show ALL published events, not just speaker's own events - const response = await eventAPI.getPublishedEvents(filters); + const response = await eventAPI.getMyEvents(user.id, filters); if (response.success) { setEvents(response.data.events); @@ -94,7 +88,7 @@ function SpeakerEventManagementPage() { total: response.data.total, totalPages: response.data.totalPages })); - logger.debug(LOGGER_COMPONENT_NAME, 'All events loaded successfully', { count: response.data.events.length }); + logger.debug(LOGGER_COMPONENT_NAME, 'Events loaded successfully', { count: response.data.events.length }); } else { throw new Error('Failed to load events'); } @@ -107,44 +101,9 @@ function SpeakerEventManagementPage() { } }; - // Load invitations and their events - const loadInvitations = async () => { - if (!user?.id) return; - - try { - setLoading(true); - const speakerProfile = await speakerApiClient.getSpeakerProfile(user.id); - const allInvitations = await speakerApiClient.getSpeakerInvitations(speakerProfile.id); - setInvitations(allInvitations); - - // Load event details for each invitation - const eventMap = new Map(); - for (const invitation of allInvitations) { - try { - const eventResponse = await eventAPI.getEventById(invitation.eventId); - eventMap.set(invitation.eventId, eventResponse.data); - } catch (err) { - logger.warn(LOGGER_COMPONENT_NAME, 'Failed to load event for invitation', { - invitationId: invitation.id, - eventId: invitation.eventId - }); - } - } - setInvitedEvents(eventMap); - } catch (err) { - logger.error(LOGGER_COMPONENT_NAME, 'Failed to load invitations', err instanceof Error ? err : new Error(String(err))); - } finally { - setLoading(false); - } - }; - useEffect(() => { - if (activeTab === 'my-events') { - loadEvents(); - } else { - loadInvitations(); - } - }, [pagination.page, activeTab]); + loadEvents(); + }, [selectedStatus, pagination.page]); // Filter events based on search and timeframe (status filtering is done server-side) const filteredEvents = events.filter(event => { @@ -202,13 +161,13 @@ function SpeakerEventManagementPage() { return Math.round((registered / capacity) * 100); }; - // Calculate stats from real data (all events shown are published) + // Calculate stats from real data const stats = { total: events.length, - published: events.length, // All events in this tab are published - draft: 0, // Draft events are not shown in "All Events" tab - pending: 0, // Pending events are not shown in "All Events" tab - rejected: 0 // Rejected events are not shown in "All Events" tab + published: events.filter(e => e.status === EventStatus.PUBLISHED).length, + draft: events.filter(e => e.status === EventStatus.DRAFT).length, + pending: events.filter(e => e.status === EventStatus.PENDING_APPROVAL).length, + rejected: events.filter(e => e.status === EventStatus.REJECTED).length }; if (loading) { @@ -302,39 +261,10 @@ function SpeakerEventManagementPage() { Event Management

- Browse all published events, manage your own events, and join invited events. + Create, manage, and monitor all events across the platform.

- {/* Tabs */} -
- - -
- {/* Stats Cards */}
@@ -394,7 +324,7 @@ function SpeakerEventManagementPage() { -
+
+ + + +
- {/* Events Grid - My Events */} - {activeTab === 'my-events' && ( + {/* Events Grid */}
{filteredEvents.map((event) => ( {event.name} - {event.status.replace('_', ' ')} @@ -494,49 +438,43 @@ function SpeakerEventManagementPage() { onClick={() => router.push(`/dashboard/speaker/events/${event.id}`)} > - View Details + View - {/* Only show edit/delete/submit actions if speaker is the event creator */} - {event.speakerId === user?.id && ( - <> - - - {(event.status === EventStatus.DRAFT || event.status === EventStatus.REJECTED) && ( - - )} - - {event.status === EventStatus.DRAFT && ( - - )} - + + + {(event.status === EventStatus.DRAFT || event.status === EventStatus.REJECTED) && ( + )} -
+ {event.status === EventStatus.DRAFT && ( + + )} +
))} @@ -564,127 +502,6 @@ function SpeakerEventManagementPage() {
)}
- )} - - {/* Invited Events Grid */} - {activeTab === 'invited-events' && ( -
- {invitations.map((invitation) => { - const event = invitedEvents.get(invitation.eventId); - if (!event) return null; - - const getStatusIcon = () => { - switch (invitation.status) { - case 'PENDING': - return ; - case 'ACCEPTED': - return ; - case 'DECLINED': - return ; - default: - return ; - } - }; - - return ( - - -
-
-
- {getStatusIcon()} - - {event.name} - -
- - {event.status.replace('_', ' ')} - - - {invitation.status} - -
-
-
- -

- {event.description} -

- -
-
- - {event.venue.name} -
-
- - - {new Date(event.bookingStartDate).toLocaleDateString()} - {new Date(event.bookingEndDate).toLocaleDateString()} - -
-
- -
- - - {invitation.status === 'ACCEPTED' && event.status === EventStatus.PUBLISHED && ( - - )} -
- - {invitation.status === 'ACCEPTED' && event.status === EventStatus.PUBLISHED && ( -
- -
- )} -
-
- ); - })} - - {invitations.length === 0 && !loading && ( -
- - - -

- No Invitations -

-

- You haven't received any event invitations yet. -

-
-
-
- )} -
- )} {/* Pagination */} {pagination.totalPages > 1 && ( diff --git a/ems-client/app/dashboard/speaker/page.tsx b/ems-client/app/dashboard/speaker/page.tsx index 7feac42..a7c2935 100644 --- a/ems-client/app/dashboard/speaker/page.tsx +++ b/ems-client/app/dashboard/speaker/page.tsx @@ -24,7 +24,7 @@ import { Download, Presentation } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useEffect } from "react"; import {useLogger} from "@/lib/logger/LoggerProvider"; import {withSpeakerAuth} from "@/components/hoc/withAuth"; @@ -44,7 +44,6 @@ const LOGGER_COMPONENT_NAME = 'SpeakerDashboard'; function SpeakerDashboard() { const { user, logout } = useAuth(); const router = useRouter(); - const searchParams = useSearchParams(); const logger = useLogger(); const [activeSection, setActiveSection] = useState('overview'); @@ -66,14 +65,6 @@ function SpeakerDashboard() { updateSpeakerProfile, } = useSpeakerData(); - // Set active section from URL query params - useEffect(() => { - const section = searchParams.get('section') as DashboardSection | null; - if (section && ['overview', 'profile', 'materials', 'invitations', 'messages'].includes(section)) { - setActiveSection(section); - } - }, [searchParams]); - useEffect(() => { logger.debug(LOGGER_COMPONENT_NAME, 'Speaker dashboard loaded', { userRole: user?.role }); }, [user, logger]); @@ -342,18 +333,9 @@ function SpeakerDashboard() { -
+
- -