diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0c61882 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,132 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Next.js +.next/ +out/ + +# Build outputs +dist +build + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ +# public - We need this for Next.js + +# Vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Documentation +README.md +*.md + +# Test files +__tests__/ +**/*.test.js +**/*.test.ts +**/*.spec.js +**/*.spec.ts + +# Scripts (migration scripts might contain sensitive data) +scripts/ diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..6e29a69 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,61 @@ +# GovLink Docker Setup + +This is a simple Docker setup for the GovLink Next.js application using MongoDB Atlas. + +## Prerequisites + +- Docker and Docker Compose installed +- MongoDB Atlas account and connection string +- Environment variables configured + +## Quick Start + +1. **Copy environment variables:** + ```bash + cp .env.example .env + ``` + +2. **Edit the `.env` file with your actual values:** + - `MONGODB_URI`: Your MongoDB Atlas connection string + - `JWT_SECRET` and `JWT_REFRESH_SECRET`: Strong secret keys + - `R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY`: Cloudflare R2 credentials + - `OPENAI_API_KEY`: Your OpenAI API key + - `TAVILY_API_KEY`: Your Tavily search API key + - Email configuration for SMTP + +3. **Build and run the application:** + ```bash + docker-compose up --build + ``` + +4. **Access the application:** + - Application: http://localhost:3000 + - Health check: http://localhost:3000/api/health + +## Environment Variables + +The main environment variables you need to configure: + +```env +MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/govlink?retryWrites=true&w=majority +JWT_SECRET=your-super-secret-jwt-key +JWT_REFRESH_SECRET=your-super-secret-refresh-key +R2_ACCESS_KEY_ID=your_r2_access_key +R2_SECRET_ACCESS_KEY=your_r2_secret_access_key +OPENAI_API_KEY=your_openai_api_key +TAVILY_API_KEY=your_tavily_api_key +``` + +## Commands + +- **Build and start:** `docker-compose up --build` +- **Start in background:** `docker-compose up -d` +- **Stop:** `docker-compose down` +- **View logs:** `docker-compose logs -f` +- **Rebuild:** `docker-compose build --no-cache` + +## Notes + +- The application uses MongoDB Atlas, so no local database is included +- The Docker image is optimized for production with Next.js standalone output +- Health checks are included to monitor application status diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bee5b75 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +# Simple Dockerfile for GovLink Next.js Application + +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +RUN apk add --no-cache libc6-compat curl +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm ci + +# Build the application +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED 1 +ENV SKIP_DB_VALIDATION true + +RUN npm run build + +# Production image +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs +RUN apk add --no-cache curl + +# Copy public directory first +COPY public ./public + +# Copy the built application +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 1ad7d2a..055fcd1 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,650 @@ -# GovLink +# 🏛️ GovLink + +**GovLink** is a comprehensive Next.js-based digital government platform designed to revolutionize citizen-government interactions. Built with modern web technologies, it provides a unified interface for citizens, government agents, departments, and administrators to access various government services efficiently. + +## 🌟 Project Overview + +GovLink bridges the digital divide between citizens and government services by providing a centralized, accessible platform that offers: + +- **Digital Service Delivery**: Streamlined online government services +- **Appointment Management**: Intelligent booking system with QR code integration +- **Real-time Communication**: Chat functionality with AI-powered RAG bot +- **Multi-level Administration**: Role-based access control for different user types +- **Document Management**: Secure file handling and verification +- **Multi-language Support**: Internationalization with i18next +- **Analytics & Reporting**: Comprehensive dashboards for insights + +## ✨ Key Features + +### 👥 **Multi-Role System** +- **Citizens**: Access services, book appointments, chat with agents +- **Agents**: Manage appointments, communicate with citizens, handle service requests +- **Departments**: Oversee agents, manage services, configure department settings +- **Administrators**: System-wide management, user oversight, system configuration + +### 🔐 **Authentication & Security** +- JWT-based authentication for all user types +- Role-based access control (RBAC) +- Password encryption with bcrypt +- Rate limiting for API security +- Account verification and email notifications + +### 📱 **Smart Appointment System** +- QR code generation for appointment passes +- Email notifications with QR code attachments +- Department-specific appointment slots +- Agent-specific booking options +- Real-time availability checking + +### 🤖 **AI-Powered Features** +- RAG (Retrieval-Augmented Generation) chatbot +- LangChain integration for intelligent responses +- OpenAI and Tavily search capabilities +- Context-aware government service assistance + +### 🌍 **Internationalization** +- Multi-language support (English, Sinhala, Tamil) +- Language detection and switching +- Localized content for better accessibility + +### 📊 **Analytics & Management** +- Department analytics dashboards +- Agent performance tracking +- User engagement metrics +- Service usage statistics + +### 🔄 **File Management** +- AWS S3 (R2) integration for file storage +- Secure file upload and download +- Document verification workflows +- Profile picture management + +## 🛠️ Technology Stack + +### **Frontend** +- **Framework**: [Next.js 15.4.5](https://nextjs.org) with React 19 +- **Styling**: [Tailwind CSS 4](https://tailwindcss.com) with custom components +- **UI Components**: Radix UI primitives +- **Animations**: Framer Motion +- **Theme**: Next-themes for dark/light mode +- **Icons**: Lucide React + +### **Backend & APIs** +- **Runtime**: Node.js with TypeScript +- **Database**: MongoDB with Mongoose ODM +- **Authentication**: JWT with bcryptjs +- **File Storage**: AWS S3 (R2) with S3 SDK v3 +- **Email Service**: Nodemailer +- **AI/ML**: LangChain, OpenAI, Tavily Search + +### **Development Tools** +- **Language**: TypeScript 5 +- **Linting**: ESLint with Next.js config +- **Build Tool**: Next.js with Turbopack +- **Package Manager**: npm +- **Version Control**: Git + +### **Deployment & Monitoring** +- **Platform**: Netlify (with Azure VM support) +- **Analytics**: Vercel Analytics & Speed Insights +- **Environment**: Multi-environment support (dev, prod) + +## 📁 Project Structure -GovLink is a Next.js-based web application designed to streamline government-related services for users. It provides a user-friendly interface for citizens, agents, and administrators to interact with various functionalities such as booking, chat, profile management, and system configuration. +``` +govlink/ +├── src/ # Source code +│ ├── app/ # Next.js app router +│ │ ├── admin/ # Admin dashboard & management +│ │ ├── agent/ # Agent portal & features +│ │ ├── department/ # Department management +│ │ ├── user/ # Citizen portal +│ │ ├── api/ # API routes & serverless functions +│ │ ├── account-suspended/ # Suspension handling +│ │ ├── feedback/ # Feedback system +│ │ └── ragbot/ # AI chatbot interface +│ ├── components/ # Reusable React components +│ │ ├── adminSystem/ # Admin-specific components +│ │ ├── agent/ # Agent portal components +│ │ ├── department/ # Department components +│ │ ├── user/ # User/citizen components +│ │ └── Icons/ # Custom icon components +│ ├── lib/ # Utility libraries & services +│ │ ├── auth/ # Authentication middleware +│ │ ├── i18n/ # Internationalization +│ │ ├── models/ # Database schemas +│ │ ├── services/ # Business logic services +│ │ ├── tools/ # Utility tools +│ │ └── utils/ # Helper functions +│ └── types/ # TypeScript type definitions +├── public/ # Static assets +├── scripts/ # Database migration & utility scripts +├── .github/ # CI/CD workflows & templates +└── docs/ # Documentation files +``` -## Project Overview +## 🚀 Getting Started -GovLink aims to bridge the gap between citizens and government services by offering a centralized platform for: +### Prerequisites -- Booking appointments and services. -- Real-time chat with agents and bots. -- Profile and document verification. -- Administrative system configuration and user management. +Before you begin, ensure you have the following installed: -## Folder Structure +- **Node.js**: Version 18.0 or higher +- **npm**: Version 8.0 or higher +- **MongoDB**: Local instance or MongoDB Atlas +- **Git**: For version control -``` -├── src/ # Source files -│ ├── app/ # Next.js app routes and pages -│ │ ├── admin/ -│ │ ├── agent/ -│ │ ├── department/ -│ │ ├── user/ -│ │ ├── api/ # Serverless/api route handlers -│ │ ├── favicon.ico -│ │ ├── globals.css -│ │ ├── layout.tsx -│ │ ├── page.tsx -│ │ └── providers.tsx -│ ├── components/ # React components grouped by area -│ │ ├── adminSystem/ -│ │ ├── agent/ -│ │ ├── department/ -│ │ ├── user/ -│ │ ├── Header.tsx -│ │ ├── SelectField.tsx -│ │ ├── ThemeProvider.tsx -│ │ └── ThemeToggle.tsx -│ ├── lib/ # Shared libraries and helpers -│ │ ├── db.ts -│ │ ├── i18n.ts -│ │ ├── r2.ts -│ │ └── utils.ts -│ └── types/ # Type definitions -│ └── department.ts -├── public/ # Static assets (images, SVGs, GIFs) -├── scripts/ # Migration and helper scripts -├── .github/ # CI, templates, workflow files -├── components.json # Design/component metadata -├── middleware.ts -├── package.json -├── package-lock.json -├── tsconfig.json -├── next.config.ts -├── tailwind.config.ts -├── postcss.config.mjs -├── eslint.config.mjs -├── README.md -├── DEPLOYMENT.md -└── ADMIN_RBAC_DOCUMENTATION.md -``` +### Environment Variables + +Create a `.env.local` file in the root directory with the following variables: -## Technologies Used +```bash +# Database +MONGODB_URI=mongodb://localhost:27017/govlink +# or MongoDB Atlas +# MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/govlink -- **Framework**: [Next.js](https://nextjs.org) -- **Styling**: Tailwind CSS -- **State Management**: React Context API -- **Deployment**: Netlify -- **Icons**: Custom SVGs and React components +# JWT Authentication +JWT_SECRET=your-super-secret-jwt-key-min-32-characters -## Getting Started +# AWS S3 (R2) Configuration +R2_ACCESS_KEY_ID=your-r2-access-key +R2_SECRET_ACCESS_KEY=your-r2-secret-key +R2_BUCKET_NAME=your-bucket-name +R2_ENDPOINT=https://your-account-id.r2.cloudflarestorage.com -To get started with the project, follow these steps: +# Email Configuration (Nodemailer) +EMAIL_FROM=noreply@govlink.lk +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER=your-email@gmail.com +EMAIL_PASS=your-app-password -1. Clone the repository: +# OpenAI API (for RAG bot) +OPENAI_API_KEY=your-openai-api-key + +# Tavily Search API (for RAG bot) +TAVILY_API_KEY=your-tavily-api-key + +# Application URLs +NEXT_PUBLIC_APP_URL=http://localhost:3000 +NEXT_PUBLIC_API_URL=http://localhost:3000/api + +# Rate Limiting +RATE_LIMIT_MAX=100 +RATE_LIMIT_WINDOW=900000 +``` +### Installation & Setup + +1. **Clone the repository**: ```bash git clone https://github.com/XFire2025/govlink.git + cd govlink ``` -2. Navigate to the project directory: +2. **Install dependencies**: + ```bash + npm install + ``` +3. **Set up environment variables**: ```bash - cd govlink + cp .env.example .env.local + # Edit .env.local with your configuration ``` -3. Install dependencies: +4. **Set up MongoDB**: + - Install MongoDB locally or use MongoDB Atlas + - Create a database named `govlink` + - Update the `MONGODB_URI` in your `.env.local` +5. **Run database migrations** (optional): ```bash - npm install + npm run migrate:departments ``` -4. Run the development server: - +6. **Start the development server**: ```bash npm run dev ``` -5. Open [http://localhost:3000](http://localhost:3000) in your browser to view the application. +7. **Open your browser**: + Navigate to [http://localhost:3000](http://localhost:3000) + +### Development Scripts + +- `npm run dev` - Start development server with Turbopack +- `npm run build` - Build the application for production +- `npm run start` - Start the production server +- `npm run lint` - Run ESLint for code quality +- `npm run seed:departments` - Seed initial department data +- `npm run migrate:departments` - Run department migrations + +## 🐳 Docker Setup + +### Using Docker Compose (Recommended) + +#### **Quick Start** + +1. **Clone the repository**: + ```bash + git clone https://github.com/XFire2025/govlink.git + cd govlink + ``` + +2. **Set up environment variables**: + ```bash + cp .env.docker.example .env + # Edit .env with your configuration + ``` + +3. **Start all services**: + ```bash + docker-compose up -d + ``` + +4. **Access the application**: + - **GovLink App**: http://localhost:3000 + - **MongoDB**: localhost:27017 + - **MongoDB Express** (optional): http://localhost:8081 + +#### **Available Profiles** + +```bash +# Development with hot reload +docker-compose up + +# Production with optimized build +docker-compose --profile production up -d + +# With MongoDB Express for database management +docker-compose --profile development up -d + +# With Redis caching +docker-compose --profile cache up -d + +# With Nginx reverse proxy +docker-compose --profile production up -d +``` + +#### **Environment Configuration** + +Create a `.env` file based on `.env.docker.example`: + +```bash +# Required variables +JWT_SECRET=your-super-secret-jwt-key-must-be-at-least-32-characters-long +OPENAI_API_KEY=your-openai-api-key +TAVILY_API_KEY=your-tavily-api-key + +# AWS S3/R2 Configuration +R2_ACCESS_KEY_ID=your-access-key +R2_SECRET_ACCESS_KEY=your-secret-key +R2_BUCKET_NAME=your-bucket +R2_ENDPOINT=https://your-account.r2.cloudflarestorage.com + +# Email Configuration +EMAIL_HOST=smtp.gmail.com +EMAIL_USER=your-email@gmail.com +EMAIL_PASS=your-app-password +``` + +#### **Development Workflow** + +```bash +# Start development environment +docker-compose up + +# View logs +docker-compose logs -f govlink-app + +# Execute commands in container +docker-compose exec govlink-app npm run build + +# Stop services +docker-compose down + +# Clean up volumes (⚠️ This will delete data) +docker-compose down -v +``` + +#### **Production Deployment** + +```bash +# Build and start production services +docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d + +# Scale the application +docker-compose up -d --scale govlink-app=3 + +# View production logs +docker-compose logs -f --tail=100 +``` + +### **Docker Services Overview** + +| Service | Port | Description | Profile | +|---------|------|-------------|---------| +| `govlink-app` | 3000 | Next.js application | default | +| `mongodb` | 27017 | MongoDB database | default | +| `mongo-express` | 8081 | Database admin UI | development | +| `redis` | 6379 | Caching layer | cache | +| `nginx` | 80, 443 | Reverse proxy | production | + +### **Health Checks** + +All services include health checks: + +```bash +# Check service status +docker-compose ps + +# View health status +docker inspect govlink-app | grep Health -A 10 +``` + +### **Data Persistence** -## Deployment +Docker volumes ensure data persistence: -GovLink is deployed using Netlify. The deployment status for each branch is as follows: +- `mongodb_data`: Database files +- `govlink_uploads`: User uploaded files +- `redis_data`: Cache data (if using Redis) +- `nginx_logs`: Web server logs -- **Main Branch**: +### **Troubleshooting Docker** + +```bash +# Rebuild containers +docker-compose build --no-cache + +# View container logs +docker-compose logs govlink-app + +# Access container shell +docker-compose exec govlink-app sh + +# Reset everything +docker-compose down -v +docker system prune -a +``` + +## 🌐 Deployment + +### Production Deployment Status + +- **Main Branch** (Production): [![Netlify Status](https://api.netlify.com/api/v1/badges/c64faf7b-b410-4c26-82fa-ac50e5b38da1/deploy-status)](https://app.netlify.com/projects/govlink25/deploys) -- **Dev Branch**: +- **Dev Branch** (Development): [![Netlify Status](https://api.netlify.com/api/v1/badges/399ab7d0-ba49-474d-b44b-a5637bfb2d1b/deploy-status)](https://app.netlify.com/projects/govlinkdev/deploys) -## How to Contribute +### Deployment Options + +#### 1. **Netlify Deployment** (Current) +- Automatic deployments from GitHub +- Environment variable configuration +- Custom domain support +- Serverless functions support + +#### 2. **Azure VM Deployment** +- Complete deployment guide available in `DEPLOYMENT.md` +- CI/CD pipeline with GitHub Actions +- Custom server configuration +- SSL certificate setup + +#### 3. **Docker Deployment** +- Use the provided `docker-compose.yml` +- Containerized application with MongoDB +- Easy scaling and environment management + +### Environment-Specific Configuration + +#### **Development** +```bash +NODE_ENV=development +NEXT_PUBLIC_APP_URL=http://localhost:3000 +``` + +#### **Production** +```bash +NODE_ENV=production +NEXT_PUBLIC_APP_URL=https://your-domain.com +``` + +## 🤝 Contributing -We welcome contributions from the community! To contribute: +We welcome contributions from the community! Here's how you can help: -1. Fork the repository. -2. Create a new branch for your feature or bugfix: +### **How to Contribute** +1. **Fork the repository** ```bash - git checkout -b feature-name + git fork https://github.com/XFire2025/govlink.git ``` -3. Commit your changes: - +2. **Create a feature branch** ```bash - git commit -m "Add your message here" + git checkout -b feature/your-feature-name ``` -4. Push to your branch: +3. **Make your changes** + - Follow the existing code style + - Add tests for new features + - Update documentation as needed + +4. **Commit your changes** + ```bash + git commit -m "feat: add your feature description" + ``` +5. **Push to your branch** ```bash - git push origin feature-name + git push origin feature/your-feature-name ``` -5. Open a pull request on GitHub. +6. **Open a Pull Request** + - Provide a clear description of your changes + - Link any related issues + - Ensure all tests pass + +### **Code Style Guidelines** + +- Use TypeScript for all new code +- Follow the existing naming conventions +- Add JSDoc comments for functions +- Use Tailwind CSS for styling +- Follow the component structure patterns + +### **Pull Request Process** + +![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/XFire2025/XFire_GovLink?utm_source=oss&utm_medium=github&utm_campaign=XFire2025%2FXFire_GovLink&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) + +1. All PRs are reviewed using CodeRabbit AI +2. Maintainers will review your changes +3. Address any feedback or requested changes +4. Once approved, your PR will be merged + +## 📚 Documentation + +- **[Admin RBAC Documentation](ADMIN_RBAC_DOCUMENTATION.md)**: Role-based access control details +- **[Deployment Guide](DEPLOYMENT.md)**: Complete Azure VM deployment instructions +- **[QR Code Implementation](QR_CODE_IMPLEMENTATION.md)**: QR code system documentation +- **API Documentation**: Available in individual route files under `src/app/api/` + +## 🤖 AI Integration + +### **RAG (Retrieval-Augmented Generation) Bot** + +GovLink features an advanced AI chatbot powered by: + +- **LangChain**: For conversation flow and context management +- **OpenAI GPT**: For natural language understanding and generation +- **Tavily Search**: For real-time information retrieval +- **Custom Knowledge Base**: Government-specific information and procedures + +### **Features** +- Context-aware responses about government services +- Multi-language support for better accessibility +- Real-time information retrieval +- Integration with appointment booking system +- Escalation to human agents when needed + +## 🔧 API Documentation + +### **Authentication Endpoints** +- `POST /api/auth/admin` - Admin authentication +- `POST /api/auth/agent/login` - Agent login +- `POST /api/auth/department/login` - Department login +- `GET /api/auth/*/me` - Get current user profile + +### **User Management** +- `GET /api/admin/admins` - List all admins (Super Admin only) +- `POST /api/admin/admins` - Create new admin +- `GET /api/admin/agents` - List all agents +- `POST /api/admin/agents` - Create new agent + +### **Department APIs** +- `GET /api/admin/departments` - List all departments +- `POST /api/admin/departments` - Create new department +- `PUT /api/admin/departments/[id]` - Update department +- `DELETE /api/admin/departments/[id]` - Delete department + +### **Agent Management** +- `GET /api/department/agents` - Get department agents +- `POST /api/department/agents` - Create new agent +- `PUT /api/department/agents/[id]` - Update agent +- `DELETE /api/department/agents/[id]` - Deactivate agent + +### **Public APIs** +- `GET /api/user/departments` - Get all active departments +- `GET /api/user/departments/[id]/agents` - Get department agents +- `POST /api/ragbot` - Chat with AI assistant + +## 🛡️ Security Features + +- **JWT Authentication**: Secure token-based authentication +- **Password Encryption**: bcrypt with salt rounds +- **Rate Limiting**: API request throttling +- **Input Validation**: Comprehensive request validation +- **CORS Protection**: Cross-origin request security +- **Environment Variables**: Secure configuration management +- **Role-Based Access**: Granular permission system + +## 📊 Monitoring & Analytics + +- **Vercel Analytics**: User engagement tracking +- **Speed Insights**: Performance monitoring +- **Custom Dashboards**: Role-specific analytics +- **Error Tracking**: Comprehensive error logging +- **Performance Metrics**: API response time monitoring + +## 🚨 Troubleshooting + +### **Common Issues** + +#### **Database Connection Issues** +```bash +# Check MongoDB service status +sudo systemctl status mongod + +# Restart MongoDB +sudo systemctl restart mongod + +# Check MongoDB logs +sudo tail -f /var/log/mongodb/mongod.log +``` + +#### **Environment Variable Issues** +- Ensure all required environment variables are set +- Check for typos in variable names +- Verify `.env.local` is in the root directory +- Restart the development server after changes + +#### **Build Errors** +```bash +# Clear Next.js cache +rm -rf .next + +# Clear node_modules and reinstall +rm -rf node_modules package-lock.json +npm install + +# Check for TypeScript errors +npm run build +``` + +#### **Port Already in Use** +```bash +# Kill process using port 3000 +npx kill-port 3000 + +# Or use different port +npm run dev -- -p 3001 +``` + +### **Performance Optimization** + +- Enable Turbopack in development: `npm run dev` (already configured) +- Use Next.js Image component for optimized images +- Implement proper caching strategies +- Monitor bundle size with `@next/bundle-analyzer` + +## 📞 Support & Community + +- **Issues**: [GitHub Issues](https://github.com/XFire2025/govlink/issues) +- **Discussions**: [GitHub Discussions](https://github.com/XFire2025/govlink/discussions) +- **Documentation**: [Project Wiki](https://github.com/XFire2025/govlink/wiki) +- **Security**: Report security issues to security@govlink.lk + +## 📝 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 🎯 Roadmap + +### **Phase 1: Core Platform** ✅ +- [x] Multi-role authentication system +- [x] Department and agent management +- [x] Basic appointment booking +- [x] Admin dashboard + +### **Phase 2: Enhanced Features** ✅ +- [x] QR code integration +- [x] Email notification system +- [x] RAG chatbot integration +- [x] Multi-language support -## License +### **Phase 3: Advanced Features** 🔄 +- [ ] Mobile application +- [ ] Advanced analytics +- [ ] Payment gateway integration +- [ ] Document verification system +- [ ] API rate limiting improvements -This project is licensed under the MIT License. See the LICENSE file for details. +### **Phase 4: Enterprise Features** 📋 +- [ ] Multi-tenant architecture +- [ ] Advanced reporting +- [ ] Audit logging +- [ ] SSO integration +- [ ] Advanced security features -## Learn More +## 🙏 Acknowledgments -To learn more about Next.js, take a look at the following resources: +- **Next.js Team**: For the amazing React framework +- **MongoDB**: For the flexible database solution +- **Tailwind CSS**: For the utility-first CSS framework +- **OpenAI**: For AI integration capabilities +- **Netlify**: For deployment and hosting +- **Government of Sri Lanka**: For the project inspiration and requirements -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +--- -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +
-## RAG Application -GovLink is integrated with a Retrieval-Augmented Generation (RAG) application that enhances the user experience by providing intelligent responses and data retrieval capabilities. This integration allows users to interact with the system more effectively, leveraging AI to assist in various tasks. +**Built with ❤️ by the GovLink Team** +[🌐 Website](https://govlink25.netlify.app) • [📚 Documentation](DEPLOYMENT.md) • [🐛 Report Bug](https://github.com/XFire2025/govlink/issues) • [✨ Request Feature](https://github.com/XFire2025/govlink/issues) -## CodeRabit -![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/XFire2025/XFire_GovLink?utm_source=oss&utm_medium=github&utm_campaign=XFire2025%2FXFire_GovLink&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews) \ No newline at end of file +
diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9a7f9ec --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +version: '3.8' + +services: + # GovLink Next.js Application + govlink-app: + build: + context: . + dockerfile: Dockerfile + args: + - SKIP_DB_VALIDATION=true + container_name: govlink-app + restart: unless-stopped + environment: + # Database (MongoDB Atlas) + MONGODB_URI: ${MONGODB_URI} + + # JWT Secrets + JWT_SECRET: ${JWT_SECRET} + JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET} + + # Token Expiration + ACCESS_TOKEN_EXPIRY: ${ACCESS_TOKEN_EXPIRY:-15m} + REFRESH_TOKEN_EXPIRY: ${REFRESH_TOKEN_EXPIRY:-7d} + EMAIL_TOKEN_EXPIRY: ${EMAIL_TOKEN_EXPIRY:-24h} + RESET_TOKEN_EXPIRY: ${RESET_TOKEN_EXPIRY:-1h} + + # Cloudflare R2 Storage + R2_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID} + R2_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY} + R2_BUCKET_NAME: ${R2_BUCKET_NAME:-govlink} + + # OpenAI Configuration + OPENAI_API_KEY: ${OPENAI_API_KEY} + OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4} + + # Tavily Search API + TAVILY_API_KEY: ${TAVILY_API_KEY} + + # Email Configuration + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASS: ${SMTP_PASS:-} + SMTP_FROM: ${SMTP_FROM:-noreply@govlink.lk} + + # Email Service (Alternative naming) + MAIL_ID: ${MAIL_ID:-} + MAIL_PW: ${MAIL_PW:-} + GOV_SERVICE_NAME: ${GOV_SERVICE_NAME:-GovLink Sri Lanka} + + # Application Settings + NODE_ENV: production + NEXT_TELEMETRY_DISABLED: 1 + PORT: 3000 + + # Security + BCRYPT_SALT_ROUNDS: ${BCRYPT_SALT_ROUNDS:-12} + + ports: + - "3000:3000" + healthcheck: + test: curl -f http://localhost:3000/api/health || exit 1 + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s diff --git a/favicon-generator.html b/favicon-generator.html new file mode 100644 index 0000000..ea222f7 --- /dev/null +++ b/favicon-generator.html @@ -0,0 +1,165 @@ + + + + + Lotus Favicon Generator + + + +

Lotus Flower Favicon Generator

+

Click the buttons below to download different sizes of the favicon:

+ +
+ +
+ + + + + + + +
+ + + + diff --git a/next.config.ts b/next.config.ts index cd58cd8..99c9e33 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + output: 'standalone', images: { remotePatterns: [ { diff --git a/public/lotus-favicon.svg b/public/lotus-favicon.svg new file mode 100644 index 0000000..6b05147 --- /dev/null +++ b/public/lotus-favicon.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..376f51d --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "GovLink Sri Lanka", + "short_name": "GovLink", + "description": "Simplifying Government for Every Sri Lankan", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#008060", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/lotus-favicon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + }, + { + "src": "/icon.svg", + "sizes": "192x192", + "type": "image/svg+xml" + }, + { + "src": "/apple-icon.svg", + "sizes": "512x512", + "type": "image/svg+xml" + } + ], + "categories": ["government", "productivity", "utilities"], + "lang": "en-US" +} diff --git a/src/app/api/department/submissions/route.ts b/src/app/api/department/submissions/route.ts index 684307d..e81f5f9 100644 --- a/src/app/api/department/submissions/route.ts +++ b/src/app/api/department/submissions/route.ts @@ -1,20 +1,23 @@ import { NextRequest, NextResponse } from 'next/server'; import connectDB from '@/lib/db'; -import Submission from '@/lib/models/submissionSchema'; +import Appointment from '@/lib/models/appointmentSchema'; import { departmentAuthMiddleware } from '@/lib/auth/department-middleware'; -// GET /api/department/submissions - Get department's submissions +// GET /api/department/submissions - Get department's appointments (showing as submissions) export async function GET(request: NextRequest) { try { // Authenticate department const authResult = await departmentAuthMiddleware(request); if (!authResult.success) { + console.error('Department auth failed:', authResult.message); return NextResponse.json( { success: false, message: authResult.message }, { status: authResult.statusCode } ); } + console.log('Department authenticated:', authResult.department?.departmentId); + await connectDB(); const { searchParams } = new URL(request.url); @@ -27,20 +30,26 @@ export async function GET(request: NextRequest) { const fromDate = searchParams.get('fromDate'); const toDate = searchParams.get('toDate'); - // Build filter object - const filter: Record = { departmentId: authResult.department!.departmentId }; + // Build filter object - match appointments by department + const filter: Record = { department: authResult.department!.departmentId }; + + console.log('Searching for appointments with department:', authResult.department!.departmentId); - if (status) filter.status = status; - if (priority) filter.priority = priority; - if (agentId) filter.agentId = agentId; + if (status) { + // Map submission status to appointment status + const appointmentStatus = status.toLowerCase(); + filter.status = appointmentStatus; + } + if (priority) filter.priority = priority.toLowerCase(); + if (agentId) filter.assignedAgent = agentId; if (search) { filter.$or = [ - { submissionId: { $regex: search, $options: 'i' } }, - { title: { $regex: search, $options: 'i' } }, - { applicantName: { $regex: search, $options: 'i' } }, - { applicantEmail: { $regex: search, $options: 'i' } }, - { applicantNIC: { $regex: search, $options: 'i' } } + { bookingReference: { $regex: search, $options: 'i' } }, + { citizenName: { $regex: search, $options: 'i' } }, + { contactEmail: { $regex: search, $options: 'i' } }, + { citizenNIC: { $regex: search, $options: 'i' } }, + { serviceType: { $regex: search, $options: 'i' } } ]; } @@ -49,31 +58,62 @@ export async function GET(request: NextRequest) { const dateFilter: { $gte?: Date; $lte?: Date } = {}; if (fromDate) dateFilter.$gte = new Date(fromDate); if (toDate) dateFilter.$lte = new Date(toDate); - filter.submittedAt = dateFilter; + filter.date = dateFilter; } // Calculate skip value for pagination const skip = (page - 1) * limit; - // Get submissions with pagination - const submissions = await Submission.find(filter) - .sort({ submittedAt: -1 }) + // Get appointments with pagination + const appointments = await Appointment.find(filter) + .sort({ submittedDate: -1 }) .skip(skip) .limit(limit) - .select('-formData -documents'); // Exclude large fields for list view + .populate('assignedAgent', 'name email status') + .populate('citizenId', 'fullName email') + .select('-documents -agentNotes'); // Exclude large fields for list view + + console.log('Found appointments:', appointments.length); + console.log('Filter used:', JSON.stringify(filter, null, 2)); // Get total count for pagination - const total = await Submission.countDocuments(filter); + const total = await Appointment.countDocuments(filter); + + console.log('Total appointments count:', total); // Get status counts for dashboard - const statusCounts = await Submission.aggregate([ - { $match: { departmentId: authResult.department!.departmentId } }, + const statusCounts = await Appointment.aggregate([ + { $match: { department: authResult.department!.departmentId } }, { $group: { _id: '$status', count: { $sum: 1 } } } ]); + // Transform appointments to match submission interface + const submissions = appointments.map(apt => ({ + id: apt._id.toString(), + submissionId: apt.bookingReference, + title: `${apt.serviceType.charAt(0).toUpperCase() + apt.serviceType.slice(1)} Appointment`, + applicantName: apt.citizenName, + applicantEmail: apt.contactEmail, + status: apt.status.toUpperCase(), + priority: apt.priority.toUpperCase(), + serviceId: apt.serviceType, + agentId: apt.assignedAgent?._id?.toString() || null, + agentName: apt.assignedAgent?.name || null, + agentEmail: apt.assignedAgent?.email || null, + agentStatus: apt.assignedAgent?.status || null, + submittedAt: apt.submittedDate.toISOString(), + updatedAt: apt.updatedAt.toISOString(), + // Additional appointment-specific fields + appointmentDate: apt.date.toISOString(), + appointmentTime: apt.time, + bookingReference: apt.bookingReference, + contactPhone: apt.contactPhone, + citizenNIC: apt.citizenNIC + })); + return NextResponse.json({ success: true, - message: 'Submissions retrieved successfully', + message: 'Appointments retrieved successfully', data: { submissions, pagination: { @@ -82,17 +122,17 @@ export async function GET(request: NextRequest) { total, pages: Math.ceil(total / limit) }, - statusCounts: statusCounts.reduce((acc, item) => { - acc[item._id] = item.count; + statusCounts: statusCounts.reduce((acc: Record, item: { _id: string; count: number }) => { + acc[item._id.toUpperCase()] = item.count; return acc; }, {}) } }); } catch (error) { - console.error('Get submissions error:', error); + console.error('Get appointments error:', error); return NextResponse.json( - { success: false, message: 'Failed to retrieve submissions' }, + { success: false, message: 'Failed to retrieve appointments' }, { status: 500 } ); } diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..0a73e83 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,37 @@ +import { NextRequest, NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; + +export async function GET(request: NextRequest) { + try { + // Check database connection + await connectDB(); + + // Basic health check response + const healthData = { + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + environment: process.env.NODE_ENV || 'development', + version: process.env.npm_package_version || '1.0.0', + database: 'connected', + memory: { + used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), + total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), + } + }; + + return NextResponse.json(healthData, { status: 200 }); + } catch (error) { + console.error('Health check failed:', error); + + return NextResponse.json( + { + status: 'unhealthy', + timestamp: new Date().toISOString(), + error: error instanceof Error ? error.message : 'Unknown error', + database: 'disconnected' + }, + { status: 503 } + ); + } +} diff --git a/src/app/api/ragbot/booking-conversation-status/route.ts b/src/app/api/ragbot/booking-conversation-status/route.ts new file mode 100644 index 0000000..11b9d6f --- /dev/null +++ b/src/app/api/ragbot/booking-conversation-status/route.ts @@ -0,0 +1,73 @@ +// src/app/api/ragbot/booking-conversation-status/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; +import mongoose from 'mongoose'; + +// Use the same schema as the booking conversation +const BookingConversationSchema = new mongoose.Schema({ + sessionId: { type: String, required: true }, + userId: { type: String, default: null }, + currentStep: { type: String, default: 'start' }, + collectedData: { + department: { type: String, default: '' }, + service: { type: String, default: '' }, + agentType: { type: String, default: '' }, + preferredDate: { type: String, default: '' }, + preferredTime: { type: String, default: '' }, + additionalNotes: { type: String, default: '' } + }, + conversationHistory: [{ + message: String, + response: String, + timestamp: { type: Date, default: Date.now } + }], + createdAt: { type: Date, default: Date.now } +}); + +// Add TTL index to auto-delete after 24 hours +BookingConversationSchema.index({ createdAt: 1 }, { expireAfterSeconds: 86400 }); + +const BookingConversation = mongoose.models.BookingConversation || + mongoose.model('BookingConversation', BookingConversationSchema); + +export async function GET(request: NextRequest) { + try { + await connectDB(); + + const { searchParams } = new URL(request.url); + const sessionId = searchParams.get('sessionId'); + + if (!sessionId) { + return NextResponse.json( + { error: 'Session ID is required' }, + { status: 400 } + ); + } + + // Find the booking conversation for this session + const conversation = await BookingConversation.findOne({ sessionId }); + + if (!conversation) { + return NextResponse.json({ + isComplete: false, + collectedData: null + }); + } + + // Check if the conversation is complete + const isComplete = conversation.currentStep === 'complete'; + + return NextResponse.json({ + isComplete, + collectedData: isComplete ? conversation.collectedData : null, + currentStep: conversation.currentStep + }); + + } catch (error) { + console.error('Error checking booking conversation status:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/ragbot/booking-conversation/route.ts b/src/app/api/ragbot/booking-conversation/route.ts new file mode 100644 index 0000000..0b77f0c --- /dev/null +++ b/src/app/api/ragbot/booking-conversation/route.ts @@ -0,0 +1,279 @@ +// src/app/api/ragbot/booking-conversation/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; +import mongoose from 'mongoose'; + +// Booking conversation state schema +const BookingConversationSchema = new mongoose.Schema({ + sessionId: { type: String, required: true }, + userId: { type: String, default: null }, + currentStep: { type: String, default: 'start' }, + collectedData: { + department: { type: String, default: '' }, + service: { type: String, default: '' }, + agentType: { type: String, default: '' }, + preferredDate: { type: String, default: '' }, + preferredTime: { type: String, default: '' }, + additionalNotes: { type: String, default: '' } + }, + conversationHistory: [{ + message: String, + response: String, + timestamp: { type: Date, default: Date.now } + }], + createdAt: { type: Date, default: Date.now } +}); + +// Add TTL index to auto-delete after 24 hours +BookingConversationSchema.index({ createdAt: 1 }, { expireAfterSeconds: 86400 }); + +const BookingConversation = mongoose.models.BookingConversation || + mongoose.model('BookingConversation', BookingConversationSchema); + +interface BookingStep { + step: string; + question: string; + nextStep: string; + dataField: keyof BookingConversationState['collectedData']; +} + +interface BookingConversationState { + sessionId: string; + userId: string | null; + currentStep: string; + collectedData: { + department: string; + service: string; + agentType: string; + preferredDate: string; + preferredTime: string; + additionalNotes: string; + }; + conversationHistory: Array<{ + message: string; + response: string; + timestamp: Date; + }>; +} + +const bookingSteps: Record = { + start: { + step: 'start', + question: "I'd be happy to help you book an appointment! First, which government department do you need assistance with? (e.g., Immigration, Business Registration, Health Services, Education, etc.)", + nextStep: 'department', + dataField: 'department' + }, + department: { + step: 'department', + question: "Great! Now, what specific service do you need from this department? (e.g., passport renewal, license application, certificate request, etc.)", + nextStep: 'service', + dataField: 'service' + }, + service: { + step: 'service', + question: "Perfect! What type of government official would you like to meet with? (e.g., Senior Officer, Customer Service Agent, Technical Specialist, etc.)", + nextStep: 'agentType', + dataField: 'agentType' + }, + agentType: { + step: 'agentType', + question: "Excellent! What's your preferred date for the appointment? (Please provide in YYYY-MM-DD format or describe like 'next Monday', 'this Friday', etc.)", + nextStep: 'preferredDate', + dataField: 'preferredDate' + }, + preferredDate: { + step: 'preferredDate', + question: "Got it! What time would work best for you? (e.g., 9:00 AM, 2:00 PM, morning, afternoon, etc.)", + nextStep: 'preferredTime', + dataField: 'preferredTime' + }, + preferredTime: { + step: 'preferredTime', + question: "Almost done! Is there anything specific you'd like to mention or any additional requirements for your appointment? (Optional - you can say 'none' or provide any special notes)", + nextStep: 'additionalNotes', + dataField: 'additionalNotes' + }, + additionalNotes: { + step: 'additionalNotes', + question: "Perfect! I've collected all your information. You can now proceed to complete your booking by clicking the 'Open Booking Form' button that will appear below. Your information has been saved and will be automatically filled in the form.", + nextStep: 'complete', + dataField: 'additionalNotes' + } +}; + +const translations = { + en: { + steps: bookingSteps, + complete: "Perfect! I've collected all your booking information:\n\n📋 **Booking Summary:**\n• Department: {department}\n• Service: {service}\n• Agent Type: {agentType}\n• Preferred Date: {preferredDate}\n• Preferred Time: {preferredTime}\n• Additional Notes: {additionalNotes}\n\n✅ You can now click the **'Open Booking Form'** button below to complete your appointment booking. All your information will be automatically filled in!", + authRequired: "To save your booking information and proceed, you'll need to log in to your account. Please click the 'Login to Continue' button below.", + error: "I encountered an error processing your booking request. Please try again." + }, + si: { + steps: { + start: { + step: 'start', + question: "ඔබගේ හමුවීම වෙන්කරවීමට මම සතුටුයි! මුලින්ම, ඔබට කුමන රජයේ දෙපාර්තමේන්තුවේ සහාය අවශ්‍යද? (උදා: ගිණුම්කරණ, ව්‍යාපාර ලියාපදිංචිය, සෞඛ්‍ය සේවා, අධ්‍යාපනය)", + nextStep: 'department', + dataField: 'department' + }, + department: { + step: 'department', + question: "ගොඩක් හොඳයි! දැන්, ඔබට මෙම දෙපාර්තමේන්තුවෙන් කුමන නිශ්චිත සේවාව අවශ්‍යද? (උදා: විදේශ ගමන් බලපත්‍ර අලුත් කිරීම, බලපත්‍ර අයදුම්, සහතික ඉල්ලීම)", + nextStep: 'service', + dataField: 'service' + }, + service: { + step: 'service', + question: "පරිපූර්ණයි! ඔබට කුමන වර්ගයේ රජයේ නිලධාරියෙකු හමුවීමට අවශ්‍යද? (උදා: ජ්‍යෙෂ්ඨ නිලධාරී, පාරිභෝගික සේවා නියෝජිතයා, තාක්ෂණික විශේෂඥයා)", + nextStep: 'agentType', + dataField: 'agentType' + } + }, + complete: "පරිපූර්ණයි! මම ඔබගේ සියලුම වෙන්කරවීම් තොරතුරු එකතු කර ගත්තෙමි:\n\n📋 **වෙන්කරවීම් සාරාංශය:**\n• දෙපාර්තමේන්තුව: {department}\n• සේවාව: {service}\n• නියෝජිත වර්ගය: {agentType}\n• කැමති දිනය: {preferredDate}\n• කැමති වේලාව: {preferredTime}\n• අමතර සටහන්: {additionalNotes}\n\n✅ ඔබගේ හමුවීම් වෙන්කරවීම සම්පූර්ණ කිරීමට දැන් **'වෙන්කරවීම් ෆෝරමය විවෘත කරන්න'** බොත්තම ක්ලික් කරන්න. ඔබගේ සියලුම තොරතුරු ස්වයංක්‍රීයව පුරවනු ඇත!", + authRequired: "ඔබගේ වෙන්කරවීම් තොරතුරු සුරැකීමට සහ ඉදිරියට යාමට, ඔබගේ ගිණුමට ලොග් වීම අවශ්‍ය වේ.", + error: "ඔබගේ වෙන්කරවීම් ඉල්ලීම සැකසීමේදී දෝෂයක් ඇති විය." + }, + ta: { + steps: { + start: { + step: 'start', + question: "உங்கள் சந்திப்பை முன்பதிவு செய்ய நான் மகிழ்ச்சியடைகிறேன்! முதலில், எந்த அரசாங்க துறையின் உதவி உங்களுக்கு தேவை? (எ.கா: குடியேற்றம், வணிக பதிவு, சுகாதார சேவைகள், கல்வி)", + nextStep: 'department', + dataField: 'department' + }, + department: { + step: 'department', + question: "அருமை! இப்போது, இந்த துறையிலிருந்து எந்த குறிப்பிட்ட சேவை உங்களுக்கு தேவை? (எ.கா: கடவுச்சீட்டு புதுப்பித்தல், உரிமம் விண்ணப்பம், சான்றிதழ் கோரிக்கை)", + nextStep: 'service', + dataField: 'service' + }, + service: { + step: 'service', + question: "சரியானது! எந்த வகையான அரசாங்க அதிகாரியை சந்திக்க விரும்புகிறீர்கள்? (எ.கா: மூத்த அதிகாரி, வாடிக்கையாளர் சேவை பிரதிநிதி, தொழில்நுட்ப நிபுணர்)", + nextStep: 'agentType', + dataField: 'agentType' + } + }, + complete: "சரியானது! உங்கள் அனைத்து முன்பதிவு தகவலையும் நான் சேகரித்துவிட்டேன்:\n\n📋 **முன்பதிவு சுருக்கம்:**\n• துறை: {department}\n• சேவை: {service}\n• முகவர் வகை: {agentType}\n• விருப்பமான தேதி: {preferredDate}\n• விருப்பமான நேரம்: {preferredTime}\n• கூடுதல் குறிப்புகள்: {additionalNotes}\n\n✅ உங்கள் சந்திப்பு முன்பதிவை முடிக்க கீழே உள்ள **'முன்பதிவு படிவத்தை திற'** பொத்தானை கிளிக் செய்யலாம். உங்கள் அனைத்து தகவல்களும் தானாகவே நிரப்பப்படும்!", + authRequired: "உங்கள் முன்பதிவு தகவலை சேமிக்க மற்றும் தொடர, உங்கள் கணக்கில் உள்நுழைய வேண்டும்.", + error: "உங்கள் முன்பதிவு கோரிக்கையை செயலாக்குவதில் பிழை ஏற்பட்டது." + } +}; + +export async function POST(request: NextRequest) { + try { + await connectDB(); + + const { message, sessionId, language = 'en' } = await request.json(); + + if (!message || !sessionId) { + return NextResponse.json( + { error: 'Message and sessionId are required' }, + { status: 400 } + ); + } + + // Get or create conversation state + let conversation = await BookingConversation.findOne({ sessionId }); + + if (!conversation) { + conversation = new BookingConversation({ + sessionId, + currentStep: 'start', + collectedData: { + department: '', + service: '', + agentType: '', + preferredDate: '', + preferredTime: '', + additionalNotes: '' + }, + conversationHistory: [] + }); + } + + const t = translations[language as keyof typeof translations] || translations.en; + let response = ''; + + // If this is the first message and it's a booking intent, start the flow + if (conversation.currentStep === 'start') { + response = t.steps.start.question; + conversation.currentStep = 'department'; + } else { + // Process the user's response based on current step + const currentStepData = t.steps[conversation.currentStep as keyof typeof t.steps]; + + if (currentStepData && conversation.currentStep !== 'complete') { + // Save the user's response to the appropriate field + const dataField = currentStepData.dataField; + if (dataField && conversation.collectedData) { + conversation.collectedData[dataField] = message.trim(); + } + + // Move to next step + if (currentStepData.nextStep === 'complete') { + // All data collected, show summary + const summary = t.complete + .replace('{department}', conversation.collectedData.department || 'Not specified') + .replace('{service}', conversation.collectedData.service || 'Not specified') + .replace('{agentType}', conversation.collectedData.agentType || 'Not specified') + .replace('{preferredDate}', conversation.collectedData.preferredDate || 'Not specified') + .replace('{preferredTime}', conversation.collectedData.preferredTime || 'Not specified') + .replace('{additionalNotes}', conversation.collectedData.additionalNotes || 'None'); + + response = summary; + conversation.currentStep = 'complete'; + } else { + // Ask next question + const nextStep = t.steps[currentStepData.nextStep as keyof typeof t.steps]; + if (nextStep) { + response = nextStep.question; + conversation.currentStep = currentStepData.nextStep; + } + } + } else if (conversation.currentStep === 'complete') { + response = "Your booking information is ready! Please use the 'Open Booking Form' button to complete your appointment."; + } + } + + // Add to conversation history + conversation.conversationHistory.push({ + message, + response, + timestamp: new Date() + }); + + // Save conversation state + await conversation.save(); + + // Also save to the booking data collection for form pre-fill + if (conversation.currentStep === 'complete') { + try { + await fetch(`${request.nextUrl.origin}/api/user/booking-data`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionId, + bookingData: conversation.collectedData + }) + }); + } catch (error) { + console.error('Error saving booking data:', error); + } + } + + return NextResponse.json({ + response, + currentStep: conversation.currentStep, + collectedData: conversation.collectedData, + isComplete: conversation.currentStep === 'complete' + }); + + } catch (error) { + console.error('Error in booking conversation:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/booking-data/route.ts b/src/app/api/user/booking-data/route.ts new file mode 100644 index 0000000..2d5d916 --- /dev/null +++ b/src/app/api/user/booking-data/route.ts @@ -0,0 +1,212 @@ +// src/app/api/user/booking-data/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; +import mongoose from 'mongoose'; + +interface BookingData { + userId: string; + sessionId: string; + step: string; + department?: string; + departmentName?: string; + service?: string; + serviceName?: string; + agent?: string; + agentName?: string; + date?: string; + time?: string; + notes?: string; + completed: boolean; + createdAt: Date; + updatedAt: Date; +} + +const BookingDataSchema = new mongoose.Schema({ + userId: { type: String, required: true, index: true }, + sessionId: { type: String, required: true, unique: true }, + step: { type: String, required: true }, + department: String, + departmentName: String, + service: String, + serviceName: String, + agent: String, + agentName: String, + date: String, + time: String, + notes: String, + completed: { type: Boolean, default: false }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now } +}, { + collection: 'booking_data' +}); + +// TTL index - automatically delete documents after 24 hours +BookingDataSchema.index({ createdAt: 1 }, { expireAfterSeconds: 86400 }); + +const BookingDataModel = mongoose.models.BookingData || mongoose.model('BookingData', BookingDataSchema); + +// GET - Retrieve booking data by session ID +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const sessionId = searchParams.get('sessionId'); + const userId = searchParams.get('userId'); + + if (!sessionId || !userId) { + return NextResponse.json( + { success: false, message: 'Session ID and User ID are required' }, + { status: 400 } + ); + } + + await connectDB(); + + const bookingData = await BookingDataModel.findOne({ + sessionId, + userId + }).lean(); + + if (!bookingData) { + return NextResponse.json( + { success: false, message: 'Booking data not found' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + data: bookingData + }); + + } catch (error) { + console.error('Error retrieving booking data:', error); + return NextResponse.json( + { + success: false, + message: 'Failed to retrieve booking data', + error: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} + +// POST - Save or update booking data +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { + userId, + sessionId, + step, + department, + departmentName, + service, + serviceName, + agent, + agentName, + date, + time, + notes, + completed = false + } = body; + + if (!userId || !sessionId || !step) { + return NextResponse.json( + { success: false, message: 'User ID, Session ID, and step are required' }, + { status: 400 } + ); + } + + await connectDB(); + + const bookingData = await BookingDataModel.findOneAndUpdate( + { sessionId, userId }, + { + $set: { + step, + department, + departmentName, + service, + serviceName, + agent, + agentName, + date, + time, + notes, + completed, + updatedAt: new Date() + } + }, + { + upsert: true, + new: true, + setDefaultsOnInsert: true + } + ); + + return NextResponse.json({ + success: true, + message: 'Booking data saved successfully', + data: bookingData + }); + + } catch (error) { + console.error('Error saving booking data:', error); + return NextResponse.json( + { + success: false, + message: 'Failed to save booking data', + error: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} + +// DELETE - Clear booking data +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const sessionId = searchParams.get('sessionId'); + const userId = searchParams.get('userId'); + + if (!sessionId || !userId) { + return NextResponse.json( + { success: false, message: 'Session ID and User ID are required' }, + { status: 400 } + ); + } + + await connectDB(); + + const result = await BookingDataModel.deleteOne({ + sessionId, + userId + }); + + if (result.deletedCount === 0) { + return NextResponse.json( + { success: false, message: 'Booking data not found' }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + message: 'Booking data cleared successfully' + }); + + } catch (error) { + console.error('Error clearing booking data:', error); + return NextResponse.json( + { + success: false, + message: 'Failed to clear booking data', + error: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/dashboard/route.ts b/src/app/api/user/dashboard/route.ts new file mode 100644 index 0000000..6086339 --- /dev/null +++ b/src/app/api/user/dashboard/route.ts @@ -0,0 +1,204 @@ +// src/app/api/user/dashboard/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import connectDB from '@/lib/db'; +import { verifyAccessToken, JWTPayload } from '@/lib/auth/user-jwt'; +import User from '@/lib/models/userSchema'; +import Appointment from '@/lib/models/appointmentSchema'; +import Submission from '@/lib/models/submissionSchema'; +import Feedback from '@/lib/models/feedbackSchema'; + +// GET /api/user/dashboard - Get user dashboard data +export async function GET(request: NextRequest) { + try { + await connectDB(); + + // Get access token from cookies + const accessToken = request.cookies.get('access_token')?.value; + if (!accessToken) { + return NextResponse.json( + { success: false, message: 'Access token not found' }, + { status: 401 } + ); + } + + // Verify the access token + let decoded: JWTPayload; + try { + decoded = verifyAccessToken(accessToken); + } catch (error) { + console.error('Token verification error:', error); + return NextResponse.json( + { success: false, message: 'Invalid or expired access token' }, + { status: 401 } + ); + } + + // Get complete user data + const userData = await User.findById(decoded.userId).select('-password'); + if (!userData) { + return NextResponse.json( + { success: false, message: 'User not found' }, + { status: 404 } + ); + } + + // Debug logging + console.log('Dashboard API - User data:', { + id: userData._id, + fullName: userData.fullName, + firstName: userData.firstName, + lastName: userData.lastName, + email: userData.email, + nicNumber: userData.nicNumber, + nic: userData.nic + }); + + // Get current date for filtering + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + // Get appointments count + const [totalAppointments, activeAppointments] = await Promise.all([ + Appointment.countDocuments({ citizenId: decoded.userId }), + Appointment.countDocuments({ + citizenId: decoded.userId, + status: { $in: ['scheduled', 'confirmed'] } + }) + ]); + + // Get submissions/applications count + const [totalApplications, pendingApplications] = await Promise.all([ + Submission.countDocuments({ citizenId: decoded.userId }), + Submission.countDocuments({ + citizenId: decoded.userId, + status: { $in: ['submitted', 'under_review', 'pending_payment'] } + }) + ]); + + // Get completed count (approved submissions + completed appointments) + const [completedSubmissions, completedAppointments] = await Promise.all([ + Submission.countDocuments({ + citizenId: decoded.userId, + status: 'approved' + }), + Appointment.countDocuments({ + citizenId: decoded.userId, + status: 'completed' + }) + ]); + + // Get messages count (feedback given by user) + const messagesCount = await Feedback.countDocuments({ + citizenId: decoded.userId + }); + + // Calculate recent activity (this month) + const [recentAppointments, recentApplications] = await Promise.all([ + Appointment.countDocuments({ + citizenId: decoded.userId, + createdAt: { $gte: startOfMonth } + }), + Submission.countDocuments({ + citizenId: decoded.userId, + createdAt: { $gte: startOfMonth } + }) + ]); + + // Prepare dashboard stats + const dashboardStats = { + activeBookings: activeAppointments, + applications: pendingApplications, + messages: messagesCount, + completed: completedSubmissions + completedAppointments + }; + + // Prepare user profile data + const userProfile = { + id: userData._id, + fullName: userData.fullName, + firstName: userData.firstName || userData.fullName?.split(' ')[0] || userData.email?.split('@')[0], + lastName: userData.lastName || userData.fullName?.split(' ').slice(1).join(' ') || '', + email: userData.email, + nicNumber: userData.nicNumber || userData.nic, // Support both field names + mobileNumber: userData.mobileNumber, + accountStatus: userData.accountStatus, + verificationStatus: userData.verificationStatus, + profileStatus: userData.profileStatus, + isEmailVerified: userData.verificationStatus !== 'unverified', + isProfileComplete: userData.profileStatus === 'verified', + profileCompletionPercentage: calculateProfileCompletion(userData) + }; + + // Prepare activity summary + const activitySummary = { + totalAppointments, + totalApplications, + recentActivity: { + thisMonth: { + appointments: recentAppointments, + applications: recentApplications + } + } + }; + + return NextResponse.json({ + success: true, + message: 'Dashboard data retrieved successfully', + data: { + user: userProfile, + stats: dashboardStats, + activity: activitySummary + } + }); + + } catch (error) { + console.error('Get user dashboard data error:', error); + return NextResponse.json( + { success: false, message: 'Failed to retrieve dashboard data' }, + { status: 500 } + ); + } +} + +// Helper function to calculate profile completion percentage +function calculateProfileCompletion(user: Record): number { + const requiredFields = [ + 'fullName', + 'email', + 'nicNumber', + 'dateOfBirth', + 'mobileNumber' + ]; + + const optionalFields = [ + 'nameInSinhala', + 'nameInTamil', + 'gender', + 'permanentAddress', + 'emergencyContact', + 'profilePicture' + ]; + + let completedRequired = 0; + let completedOptional = 0; + + // Check required fields (worth 70% of completion) + requiredFields.forEach(field => { + if (user[field] && user[field] !== '') { + completedRequired++; + } + }); + + // Check optional fields (worth 30% of completion) + optionalFields.forEach(field => { + if (user[field] && user[field] !== '') { + completedOptional++; + } + }); + + // Calculate percentage + const requiredPercentage = (completedRequired / requiredFields.length) * 70; + const optionalPercentage = (completedOptional / optionalFields.length) * 30; + + return Math.round(requiredPercentage + optionalPercentage); +} diff --git a/src/app/api/user/settings/route.ts b/src/app/api/user/settings/route.ts new file mode 100644 index 0000000..b4f9812 --- /dev/null +++ b/src/app/api/user/settings/route.ts @@ -0,0 +1,113 @@ +import { NextRequest, NextResponse } from "next/server"; +import { verifyAccessToken } from "@/lib/auth/user-jwt"; +import connectToDatabase from "@/lib/db"; +import User from "@/lib/models/userSchema"; + +export async function PUT(request: NextRequest) { + try { + // Get access token from cookies + const accessToken = request.cookies.get("access_token")?.value; + + if (!accessToken) { + return NextResponse.json( + { error: "Access token not found" }, + { status: 401 } + ); + } + + // Verify the access token + let decoded; + try { + decoded = verifyAccessToken(accessToken); + } catch { + return NextResponse.json( + { error: "Invalid or expired access token" }, + { status: 401 } + ); + } + + const { emailNotifications, language } = await request.json(); + + await connectToDatabase(); + + // Find and update user settings + const user = await User.findById(decoded.userId); + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + // Update user preferences + const updatedUser = await User.findByIdAndUpdate( + decoded.userId, + { + $set: { + "preferences.emailNotifications": emailNotifications, + "preferences.language": language, + updatedAt: new Date(), + }, + }, + { new: true, runValidators: true } + ); + + return NextResponse.json({ + success: true, + message: "Settings updated successfully", + settings: { + emailNotifications: updatedUser.preferences?.emailNotifications ?? true, + language: updatedUser.preferences?.language ?? "en", + }, + }); + } catch (error) { + console.error("Error updating user settings:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +export async function GET(request: NextRequest) { + try { + // Get access token from cookies + const accessToken = request.cookies.get("access_token")?.value; + + if (!accessToken) { + return NextResponse.json( + { error: "Access token not found" }, + { status: 401 } + ); + } + + // Verify the access token + let decoded; + try { + decoded = verifyAccessToken(accessToken); + } catch { + return NextResponse.json( + { error: "Invalid or expired access token" }, + { status: 401 } + ); + } + + await connectToDatabase(); + + const user = await User.findById(decoded.userId).select("preferences"); + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + return NextResponse.json({ + success: true, + settings: { + emailNotifications: user.preferences?.emailNotifications ?? true, + language: user.preferences?.language ?? "en", + }, + }); + } catch (error) { + console.error("Error fetching user settings:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/src/app/apple-icon.svg b/src/app/apple-icon.svg new file mode 100644 index 0000000..82d0787 --- /dev/null +++ b/src/app/apple-icon.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/department/dashboard/page.tsx b/src/app/department/dashboard/page.tsx index 76c5c2b..93c984f 100644 --- a/src/app/department/dashboard/page.tsx +++ b/src/app/department/dashboard/page.tsx @@ -3,7 +3,6 @@ import React from "react"; import { motion } from "framer-motion"; import { Users, FileText, Clock, CheckCircle, ArrowUpRight, RefreshCw, AlertCircle } from "lucide-react"; -import { useTranslation } from "@/lib/i18n/hooks/useTranslation"; import { useDashboardData } from "@/lib/hooks/useDepartmentApi"; // Define a specific interface for a Stat object @@ -89,7 +88,6 @@ const ErrorCard = ({ message, onRetry }: { message: string; onRetry: () => void ); export default function DepartmentDashboardPage() { - const { t } = useTranslation('department'); const { data: dashboardData, loading, error, refetch } = useDashboardData(); // Create stats array from API data or show loading/error state @@ -99,7 +97,7 @@ export default function DepartmentDashboardPage() { return [ { index: 0, - title: t('dashboard.stats.total_submissions'), + title: 'Total Submissions', value: dashboardData.stats.totalSubmissions.toLocaleString(), change: dashboardData.changes.submissionChange, icon: FileText, @@ -108,7 +106,7 @@ export default function DepartmentDashboardPage() { }, { index: 1, - title: t('dashboard.stats.assigned_agents'), + title: 'Assigned Agents', value: dashboardData.stats.activeAgents.toString(), change: `${dashboardData.stats.totalAgents} total`, icon: Users, @@ -117,7 +115,7 @@ export default function DepartmentDashboardPage() { }, { index: 2, - title: t('dashboard.stats.pending_reviews'), + title: 'Pending Reviews', value: dashboardData.stats.pendingSubmissions.toString(), change: "pending", icon: Clock, @@ -126,7 +124,7 @@ export default function DepartmentDashboardPage() { }, { index: 3, - title: t('dashboard.stats.service_uptime'), + title: 'Service Uptime', value: `${dashboardData.stats.serviceUptime}%`, change: `${dashboardData.stats.activeServices}/${dashboardData.stats.totalServices}`, icon: CheckCircle, @@ -145,10 +143,10 @@ export default function DepartmentDashboardPage() {

- {t('dashboard.title')} + Department Dashboard

-

{t('dashboard.welcome')}

+

Welcome to your department dashboard

@@ -182,7 +180,7 @@ export default function DepartmentDashboardPage() { className="bg-card/90 dark:bg-card/95 backdrop-blur-md p-6 rounded-2xl border border-border/50 shadow-glow modern-card" >
-

{t('activity.title')}

+

Recent Activity

{dashboardData?.recentSubmissions && dashboardData.recentSubmissions.length > 0 && ( @@ -194,7 +192,7 @@ export default function DepartmentDashboardPage() { className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-muted/30 rounded-lg transition-all" > - {t('activity.refresh')} + Refresh
diff --git a/src/app/favicon.ico.backup b/src/app/favicon.ico.backup new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/src/app/favicon.ico.backup differ diff --git a/src/app/icon.svg b/src/app/icon.svg new file mode 100644 index 0000000..6b05147 --- /dev/null +++ b/src/app/icon.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a7348ea..53ca405 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -31,6 +31,13 @@ const notoSansTamil = Noto_Sans_Tamil({ export const metadata: Metadata = { title: 'GovLink Sri Lanka', description: 'Simplifying Government for Every Sri Lankan', + icons: { + icon: [ + { url: '/icon.svg', type: 'image/svg+xml' }, + { url: '/favicon.ico', sizes: '32x32', type: 'image/x-icon' }, + ], + apple: '/apple-icon.svg', + }, }; export default function RootLayout({ diff --git a/src/app/ragbot/page.tsx b/src/app/ragbot/page.tsx index 26cda3b..842f606 100644 --- a/src/app/ragbot/page.tsx +++ b/src/app/ragbot/page.tsx @@ -2,6 +2,8 @@ "use client"; import React, { Suspense, useState, useEffect, useRef } from 'react'; import UserDashboardLayout from '@/components/user/dashboard/UserDashboardLayout'; +import BookingChatManager from '@/components/user/chat/BookingChatManager'; +import { useRouter } from 'next/navigation'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeHighlight from 'rehype-highlight'; @@ -191,8 +193,17 @@ const UserMessage = ({ text, timestamp = new Date() }: { text: string; timestamp ); -const TypingIndicator = ({ language = 'en' }: { language?: Language }) => { +const TypingIndicator = ({ language = 'en', isBookingMode = false }: { language?: Language, isBookingMode?: boolean }) => { const t = chatTranslations[language]; + + const bookingMessages = { + en: { analyzing: 'Processing your booking details', preparing: 'Preparing your booking questions' }, + si: { analyzing: 'ඔබගේ වෙන්කරවීම් විස්තර සකසමින්', preparing: 'වෙන්කරවීම් ප්‍රශ්න සූදානම් කරමින්' }, + ta: { analyzing: 'உங்கள் முன்பதிவு விவரங்களை செயலாக்குகிறது', preparing: 'முன்பதிவு கேள்விகளை தயாரிക்கிறது' } + }; + + const messages = isBookingMode ? bookingMessages[language] : t; + return (
@@ -205,11 +216,11 @@ const TypingIndicator = ({ language = 'en' }: { language?: Language }) => { - {t.analyzing} + {messages.analyzing}
- {t.preparing} + {messages.preparing}
@@ -449,44 +460,179 @@ const ChatInput = ({ onSendMessage, language = 'en', disabled = false }: { onSen // --- MAIN CHAT PAGE COMPONENT --- export default function RAGBotPage() { + const router = useRouter(); const [messages, setMessages] = useState([]); const [isTyping, setIsTyping] = useState(false); const [currentLanguage, setCurrentLanguage] = useState('en'); const [sessionId] = useState(`rag_session_${Date.now()}_${Math.random().toString(36).substring(7)}`); + const [lastUserMessage, setLastUserMessage] = useState(''); + const [isInBookingConversation, setIsInBookingConversation] = useState(false); const t = chatTranslations[currentLanguage]; + // Store session ID in sessionStorage for BookingChatManager to access + useEffect(() => { + if (typeof window !== 'undefined') { + sessionStorage.setItem('ragSessionId', sessionId); + } + }, [sessionId]); + const handleLanguageChange = (newLanguage: Language) => { setCurrentLanguage(newLanguage); }; - const handleSendMessage = async (messageText: string) => { - if (!messageText.trim()) return; + const handleLoginRequired = () => { + router.push('/user/auth/login?redirect=/ragbot'); + }; - const userMessage: Message = { type: 'user', text: messageText, timestamp: new Date() }; - setMessages(prev => [...prev, userMessage]); - setIsTyping(true); + const handleBookingQuestionGenerated = (question: string) => { + const botMessage: Message = { + type: 'bot', + text: question, + timestamp: new Date(), + }; + setMessages(prev => [...prev, botMessage]); + }; + + // Check if user message contains booking intent vs information search intent + const checkBookingIntent = (message: string): boolean => { + // If we're already in a booking conversation, continue with booking flow + // unless user explicitly asks for information search + if (isInBookingConversation) { + const searchKeywords = [ + 'search', 'find', 'tell me about', 'information about', 'details about', + 'what is', 'how does', 'explain', 'describe', 'show me', 'lookup', + 'සොයන්න', 'විස්තර', 'කියන්න', 'පෙන්වන්න', + 'தேடு', 'விவரங்கள்', 'சொல்லு', 'காட்டு' + ]; + const lowerMessage = message.toLowerCase(); + const isExplicitSearch = searchKeywords.some(keyword => lowerMessage.includes(keyword)); + return !isExplicitSearch; // Continue booking unless explicit search request + } + + const lowerMessage = message.toLowerCase(); + + // Information search keywords - if these are present, it's NOT a booking intent + const searchKeywords = [ + 'search', 'find', 'tell me about', 'information about', 'details about', + 'what is', 'how does', 'explain', 'describe', 'show me', 'lookup', + 'සොයන්න', 'විස්තර', 'කියන්න', 'පෙන්වන්න', + 'தேடு', 'விவரங்கள்', 'சொல்லு', 'காட்டு' + ]; + + // If user is explicitly asking for information, return false (not booking) + if (searchKeywords.some(keyword => lowerMessage.includes(keyword))) { + return false; + } + + // Booking action keywords - these indicate intent to book/schedule + const bookingActionKeywords = [ + 'book', 'schedule', 'appointment', 'meeting', 'visit', 'apply for', + 'register for', 'submit application', 'request appointment', 'need appointment', + 'want to book', 'would like to schedule', 'can i book', 'වෙන්කරවීම', + 'හමුවීම ගන්න', 'ලියාපදිංචි වීම', 'முன்பதิवு', 'சந்திப்பு பதிவু' + ]; + + // Response patterns that indicate they're providing booking details + const responsePatterns = [ + // These patterns suggest user is responding to booking questions + 'immigration', 'business registration', 'health services', 'education', + 'passport', 'license', 'certificate', 'permit', 'renewal', + 'senior officer', 'customer service', 'agent', 'monday', 'tuesday', + 'morning', 'afternoon', 'am', 'pm', 'tomorrow', 'next week' + ]; + + // Check if it's a booking action or response to booking questions + const hasBookingAction = bookingActionKeywords.some(keyword => + lowerMessage.includes(keyword) + ); + + const hasResponsePattern = responsePatterns.some(pattern => + lowerMessage.includes(pattern) + ); + + return hasBookingAction || hasResponsePattern; + }; + // Handle booking conversation flow + const handleBookingConversation = async (message: string): Promise => { try { - const response = await fetch('/api/ragbot/chat', { + // Send message to booking conversation handler API + const response = await fetch('/api/ragbot/booking-conversation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: messageText, sessionId }), + body: JSON.stringify({ + message, + sessionId, + language: currentLanguage + }), }); if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.details || 'Failed to get response from server'); + throw new Error('Failed to process booking conversation'); } const data = await response.json(); - const botMessage: Message = { - type: 'bot', - text: data.response, - timestamp: new Date(), - departmentContacts: data.departmentContacts, - sources: data.sources, - }; - setMessages(prev => [...prev, botMessage]); + return data.response; + } catch (error) { + console.error('Error in booking conversation:', error); + return "I'd like to help you with booking, but I encountered an error. Please try again or contact support."; + } + }; + + const handleSendMessage = async (messageText: string) => { + if (!messageText.trim()) return; + + const userMessage: Message = { type: 'user', text: messageText, timestamp: new Date() }; + setMessages(prev => [...prev, userMessage]); + setLastUserMessage(messageText); + setIsTyping(true); + + try { + // First check if this is a booking-related message + const isBookingIntent = checkBookingIntent(messageText); + + if (isBookingIntent) { + // Set booking conversation state + setIsInBookingConversation(true); + + // Handle booking conversation flow instead of RAG search + const bookingResponse = await handleBookingConversation(messageText); + const botMessage: Message = { + type: 'bot', + text: bookingResponse, + timestamp: new Date(), + }; + setMessages(prev => [...prev, botMessage]); + + // Check if booking is complete + if (bookingResponse.includes('Open Booking Form') || bookingResponse.includes('booking information is ready')) { + setIsInBookingConversation(false); + } + } else { + // Reset booking conversation state when switching to information mode + setIsInBookingConversation(false); + // Original RAG chat flow for general government information + const response = await fetch('/api/ragbot/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: messageText, sessionId }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.details || 'Failed to get response from server'); + } + + const data = await response.json(); + const botMessage: Message = { + type: 'bot', + text: data.response, + timestamp: new Date(), + departmentContacts: data.departmentContacts, + sources: data.sources, + }; + setMessages(prev => [...prev, botMessage]); + } } catch (error) { console.error('Error sending message:', error); @@ -532,18 +678,38 @@ export default function RAGBotPage() { }> - +
+
); }; -function ChatContent({ messages, isTyping, language = 'en', onSendMessage }: { messages: Message[]; isTyping: boolean; language: Language, onSendMessage: (message: string) => void; }) { +function ChatContent({ messages, isTyping, language = 'en', onSendMessage, isInBookingMode }: { + messages: Message[]; + isTyping: boolean; + language: Language; + onSendMessage: (message: string) => void; + isInBookingMode?: boolean; +}) { const bottomRef = useRef(null); const t = chatTranslations[language]; @@ -596,7 +762,7 @@ function ChatContent({ messages, isTyping, language = 'en', onSendMessage }: { m ))} - {isTyping && } + {isTyping && }
); diff --git a/src/app/user/booking/new/page.tsx b/src/app/user/booking/new/page.tsx index 5d0a789..71d587b 100644 --- a/src/app/user/booking/new/page.tsx +++ b/src/app/user/booking/new/page.tsx @@ -521,6 +521,7 @@ function mockAvailableSlots(agentId: string, date: string, baseSlots: string[]): export default function NewBookingPage() { const router = useRouter(); const { user, isAuthenticated, isLoading } = useAuth(); + const [searchParams, setSearchParams] = useState(null); const [form, setForm] = useState({ department: "", @@ -537,6 +538,7 @@ export default function NewBookingPage() { const [reference, setReference] = useState(''); const [success, setSuccess] = useState(null); const [error, setError] = useState(null); + const [isFromChat, setIsFromChat] = useState(false); const [appointmentData, setAppointmentData] = useState<{ id: string; bookingReference: string; @@ -672,6 +674,38 @@ export default function NewBookingPage() { // Load departments on mount useEffect(() => { loadDepartments(); + + // Handle pre-filled data from chat + if (typeof window !== 'undefined') { + const urlParams = new URLSearchParams(window.location.search); + const fromChat = urlParams.get('fromChat') === 'true'; + + if (fromChat) { + setIsFromChat(true); + + // Pre-fill form with chat data + const chatData = { + department: urlParams.get('department') || '', + service: urlParams.get('service') || '', + position: urlParams.get('agent') || '', // Map agent to position + day: urlParams.get('date') || '', + slot: urlParams.get('time') || '', + notes: urlParams.get('notes') || '' + }; + + setForm(prev => ({ + ...prev, + ...chatData + })); + + // Clear booking data from localStorage since we're now in the form + if (typeof window !== 'undefined') { + localStorage.removeItem('govlink_booking_data'); + } + + console.log('Pre-filled form with chat data:', chatData); + } + } }, [loadDepartments]); // Load services when department changes diff --git a/src/app/user/dashboard/page.tsx b/src/app/user/dashboard/page.tsx index 7704dfe..8c954c2 100644 --- a/src/app/user/dashboard/page.tsx +++ b/src/app/user/dashboard/page.tsx @@ -1,9 +1,10 @@ // src/app/user/dashboard/page.tsx "use client"; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import UserDashboardLayout from '@/components/user/dashboard/UserDashboardLayout'; import { useAuth } from '@/lib/auth/AuthContext'; import { CitizenProtectedRoute } from '@/lib/auth/ProtectedRoute'; +import { useDashboard } from '@/lib/hooks/useDashboard'; import Link from 'next/link'; // Types @@ -41,7 +42,7 @@ const dashboardTranslations: Record = { en: { - welcome: 'Citizen Dashboard', + welcome: 'Hello', subtitle: 'Access government services and manage your applications', overview: 'Services Overview', quickActions: 'Quick Actions', @@ -143,13 +144,34 @@ interface StatCard { trend?: string; } -const StatsOverview = ({ language = 'en' }: { language: Language }) => { +interface StatsOverviewProps { + language: Language; + stats?: { + activeBookings: number; + applications: number; + messages: number; + completed: number; + }; + loading?: boolean; +} + +const StatsOverview = ({ language = 'en', stats, loading = false }: StatsOverviewProps) => { const t = dashboardTranslations[language]; - const stats: StatCard[] = [ + // Default stats if none provided + const defaultStats = { + activeBookings: 0, + applications: 0, + messages: 0, + completed: 0 + }; + + const currentStats = stats || defaultStats; + + const statsCards: StatCard[] = [ { id: 'bookings', - value: '3', + value: loading ? '...' : currentStats.activeBookings.toString(), label: t.stats.activeBookings, icon: ( @@ -161,11 +183,11 @@ const StatsOverview = ({ language = 'en' }: { language: Language }) => { ), color: '#FF5722', bgColor: 'from-[#FF5722]/10 to-[#FF5722]/5', - trend: '+2' + trend: loading ? undefined : currentStats.activeBookings > 0 ? `+${currentStats.activeBookings}` : undefined }, { id: 'applications', - value: '7', + value: loading ? '...' : currentStats.applications.toString(), label: t.stats.applications, icon: ( @@ -177,11 +199,11 @@ const StatsOverview = ({ language = 'en' }: { language: Language }) => { ), color: '#FFC72C', bgColor: 'from-[#FFC72C]/10 to-[#FFC72C]/5', - trend: '+1' + trend: loading ? undefined : currentStats.applications > 0 ? `+${currentStats.applications}` : undefined }, { id: 'messages', - value: '12', + value: loading ? '...' : currentStats.messages.toString(), label: t.stats.messages, icon: ( @@ -190,11 +212,11 @@ const StatsOverview = ({ language = 'en' }: { language: Language }) => { ), color: '#008060', bgColor: 'from-[#008060]/10 to-[#008060]/5', - trend: '+5' + trend: loading ? undefined : currentStats.messages > 0 ? `+${currentStats.messages}` : undefined }, { id: 'completed', - value: '24', + value: loading ? '...' : currentStats.completed.toString(), label: t.stats.completed, icon: ( @@ -203,13 +225,13 @@ const StatsOverview = ({ language = 'en' }: { language: Language }) => { ), color: '#8D153A', bgColor: 'from-[#8D153A]/10 to-[#8D153A]/5', - trend: '+3' + trend: loading ? undefined : currentStats.completed > 0 ? `+${currentStats.completed}` : undefined } ]; return (
- {stats.map((stat, index) => ( + {statsCards.map((stat, index) => (
('en'); const { user } = useAuth(); + const { data: dashboardData, loading: dashboardLoading } = useDashboard(); const t = dashboardTranslations[currentLanguage]; @@ -358,24 +381,37 @@ export default function UserDashboardPage() { setCurrentLanguage(newLanguage); }; + // Debug logging to track data changes + useEffect(() => { + console.log('Dashboard data changed:', dashboardData); + }, [dashboardData]); + + useEffect(() => { + console.log('Auth user changed:', user); + }, [user]); + + useEffect(() => { + console.log('Dashboard loading state:', dashboardLoading); + }, [dashboardLoading]); + return ( - {t.welcome.split(' ')[0]}{' '} + {t.welcome}{' '} - {user?.firstName || user?.email?.split('@')[0] || 'User'} + {dashboardLoading ? '...' : (dashboardData?.user?.firstName || user?.firstName || user?.email?.split('@')[0] || 'User')}! } - subtitle={`Welcome to GovLink, ${user?.firstName || 'Citizen'}! ${t.subtitle}`} + subtitle={`Welcome to GovLink, ${dashboardLoading ? '...' : (dashboardData?.user?.firstName || user?.firstName || 'Citizen')}! ${t.subtitle}`} language={currentLanguage} onLanguageChange={handleLanguageChange} >
{/* Profile Completion Banner */} - {user && !user.isProfileComplete && ( + {dashboardData?.user && !dashboardData.user.isProfileComplete && (
@@ -389,12 +425,12 @@ export default function UserDashboardPage() { Complete Your Profile

- Complete your profile to access all government services. Your profile is {user.profileCompletionPercentage}% complete. + Complete your profile to access all government services. Your profile is {dashboardData.user.profileCompletionPercentage}% complete.

@@ -430,18 +466,18 @@ export default function UserDashboardPage() {

- Account {user.accountStatus === 'pending' ? 'Pending Verification' : 'Issue'} + Account {dashboardData.user.accountStatus === 'pending_verification' ? 'Pending Verification' : 'Issue'}

- {user.accountStatus === 'pending' + {dashboardData.user.accountStatus === 'pending_verification' ? 'Your account is pending verification. Some services may be limited until verification is complete.' : 'There is an issue with your account. Please contact support for assistance.' } @@ -460,7 +496,11 @@ export default function UserDashboardPage() { {t.overview}

- +
{/* Main Services Grid */} @@ -544,25 +584,39 @@ export default function UserDashboardPage() {

- Welcome, Sanduni Perera + Welcome, {dashboardLoading ? '...' : (dashboardData?.user?.fullName || user?.firstName || 'User')}

- Citizen ID: 199512345678 + Citizen ID: {dashboardLoading ? 'Loading...' : (dashboardData?.user?.nicNumber || 'Not available')}

{/* Status Indicators */}
-
-
- Account Verified -
-
- - - - - Profile Complete -
+ {dashboardData?.user?.isEmailVerified && ( +
+
+ Account Verified +
+ )} + {dashboardData?.user?.isProfileComplete && ( +
+ + + + + Profile Complete +
+ )} + {!dashboardData?.user?.isProfileComplete && dashboardData?.user && ( +
+ + + + + + Profile {dashboardData.user.profileCompletionPercentage}% Complete +
+ )}
diff --git a/src/app/user/help/page.tsx b/src/app/user/help/page.tsx new file mode 100644 index 0000000..3477216 --- /dev/null +++ b/src/app/user/help/page.tsx @@ -0,0 +1,566 @@ +"use client"; +import React, { useState } from "react"; +import UserDashboardLayout from "@/components/user/dashboard/UserDashboardLayout"; + +const UserHelpPage = () => { + const [language, setLanguage] = useState<"en" | "si" | "ta">("en"); + const [searchQuery, setSearchQuery] = useState(""); + const [activeCategory, setActiveCategory] = useState("getting-started"); + const [selectedArticle, setSelectedArticle] = useState(null); + + const translations = { + en: { + title: "Help & Support", + subtitle: "Find answers to your questions and get support", + searchPlaceholder: "Search for help...", + categories: "Categories", + gettingStarted: "Getting Started", + accountManagement: "Account Management", + services: "Government Services", + appointments: "Appointments", + documents: "Documents & Forms", + technical: "Technical Support", + contact: "Contact Support", + phone: "Phone", + email: "Email", + chat: "Live Chat", + faqs: "Frequently Asked Questions", + tutorials: "Video Tutorials", + }, + si: { + title: "උදව් සහ සහාය", + subtitle: "ඔබේ ප්‍රශ්නවලට පිළිතුරු සොයා ගන්න සහ සහාය ලබා ගන්න", + searchPlaceholder: "උදව් සඳහා සොයන්න...", + categories: "කාණ්ඩ", + gettingStarted: "ආරම්භ කිරීම", + accountManagement: "ගිණුම් කළමනාකරණය", + services: "රජයේ සේවාවන්", + appointments: "හමුවීම්", + documents: "ලේඛන සහ ෆෝම්", + technical: "තාක්ෂණික සහාය", + contact: "සහාය අමතන්න", + phone: "දුරකථනය", + email: "ඊමේල්", + chat: "සජීව කතාබස්", + faqs: "නිතර අසන ප්‍රශ්න", + tutorials: "වීඩියෝ නිබන්ධන", + }, + ta: { + title: "உதவி மற்றும் ஆதரவு", + subtitle: "உங்கள் கேள்விகளுக்கான பதில்களைக் கண்டறிந்து ஆதரவைப் பெறுங்கள்", + searchPlaceholder: "உதவிக்காக தேடுங்கள்...", + categories: "வகைகள்", + gettingStarted: "தொடங்குதல்", + accountManagement: "கணக்கு நிர்வாகம்", + services: "அரசாங்க சேவைகள்", + appointments: "சந்திப்புகள்", + documents: "ஆவணங்கள் மற்றும் படிவங்கள்", + technical: "தொழில்நுட்ப ஆதரவு", + contact: "ஆதரவைத் தொடர்பு கொள்ளவும்", + phone: "தொலைபேசி", + email: "மின்னஞ்சல்", + chat: "நேரடி அரட்டை", + faqs: "அடிக்கடி கேட்கப்படும் கேள்விகள்", + tutorials: "வீடியோ பயிற்சிகள்", + }, + }; + + const t = translations[language]; + + const helpCategories = [ + { + id: "getting-started", + icon: ( + + + + + + + ), + title: t.gettingStarted, + articles: [ + { + title: "How to register as a citizen", + content: + "To register as a citizen on GovLink:\n\n1. Click on 'Register' from the main page\n2. Fill in your personal details including your NIC number\n3. Provide a valid email address and mobile number\n4. Create a secure password\n5. Verify your email address by clicking the link sent to your inbox\n6. Complete your profile with additional information\n\nOnce registered, you'll have access to all government services available on the platform.", + }, + { + title: "First time login guide", + content: + "For your first login:\n\n1. Go to the citizen login page\n2. Enter your registered email and password\n3. If you forgot your password, use the 'Forgot Password' link\n4. After successful login, you'll be taken to your dashboard\n5. Complete your profile if prompted\n6. Explore the available services and features\n\nTip: Keep your login credentials secure and don't share them with anyone.", + }, + { + title: "Setting up your profile", + content: + "To complete your profile setup:\n\n1. Navigate to your profile page from the user menu\n2. Upload a clear profile picture\n3. Fill in all required personal information\n4. Add your address details\n5. Upload necessary verification documents\n6. Set your communication preferences\n7. Review and save your information\n\nA complete profile helps government agents serve you better and speeds up service delivery.", + }, + { + title: "Understanding the dashboard", + content: + "Your dashboard provides:\n\n1. **Quick Actions**: Apply for services, book appointments\n2. **Recent Activity**: View your recent applications and bookings\n3. **Notifications**: Important updates from government departments\n4. **Service Status**: Track your ongoing applications\n5. **Shortcuts**: Quick access to frequently used services\n6. **Help**: Access to support and guidance\n\nThe dashboard is your central hub for all government service interactions.", + }, + ], + }, + { + id: "account-management", + icon: ( + + + + + ), + title: t.accountManagement, + articles: [ + { + title: "Updating your personal information", + content: + "To update your personal information:\n\n1. Go to your profile page\n2. Click on 'Edit Profile'\n3. Update the necessary fields\n4. Upload new documents if required\n5. Save your changes\n\nNote: Some changes may require verification by government officials.", + }, + { + title: "Changing your password", + content: + "To change your password:\n\n1. Go to Settings from your profile menu\n2. Click on 'Change Password'\n3. Enter your current password\n4. Enter your new password (must be strong)\n5. Confirm your new password\n6. Save changes\n\nFor security, you'll be logged out of all devices after changing your password.", + }, + { + title: "Managing notification preferences", + content: + "To manage your notifications:\n\n1. Go to Settings\n2. Navigate to 'Notification Preferences'\n3. Choose your preferred notification methods (email, SMS)\n4. Select which types of notifications you want to receive\n5. Set your notification frequency\n6. Save your preferences\n\nYou can update these settings anytime.", + }, + { + title: "Account verification process", + content: + "Account verification steps:\n\n1. Upload required documents (NIC, proof of address)\n2. Wait for government verification (usually 2-3 business days)\n3. Check your email for verification status updates\n4. If additional documents are needed, you'll be notified\n5. Once verified, you'll have access to all services\n\nVerified accounts have higher service limits and faster processing.", + }, + ], + }, + { + id: "services", + icon: ( + + + + + ), + title: t.services, + articles: [ + { + title: "Available government services", + content: + "GovLink provides access to various government services:\n\n• **Identity Services**: NIC applications, passport renewals\n• **Business Services**: Business registration, licenses\n• **Health Services**: Medical certificates, health records\n• **Education Services**: Certificate verification, transcripts\n• **Social Services**: Welfare applications, pensions\n• **Property Services**: Land records, property transfers\n\nNew services are regularly added to the platform.", + }, + { + title: "How to apply for services", + content: + "To apply for government services:\n\n1. Browse available services from your dashboard\n2. Select the service you need\n3. Read the requirements and prepare documents\n4. Fill out the application form completely\n5. Upload required documents\n6. Review and submit your application\n7. Pay any applicable fees online\n8. Track your application status\n\nYou'll receive notifications about your application progress.", + }, + { + title: "Service status tracking", + content: + "Track your service applications:\n\n1. Go to your dashboard\n2. Click on 'My Applications'\n3. View the status of each application\n4. Click on any application for detailed information\n5. Check for updates and required actions\n6. Download completed documents when ready\n\nStatus updates include: Submitted, Under Review, Approved, Rejected, or Completed.", + }, + { + title: "Required documents checklist", + content: + "Common documents needed for services:\n\n**Identity Verification**:\n• Valid NIC (front and back)\n• Recent passport-size photograph\n\n**Address Verification**:\n• Utility bill (not older than 3 months)\n• Bank statement or rent agreement\n\n**Service-Specific Documents**:\n• Birth certificate (for passport applications)\n• Marriage certificate (for spouse-related services)\n• Educational certificates (for verification services)\n\nAlways check specific requirements for each service.", + }, + ], + }, + { + id: "appointments", + icon: ( + + + + + + + ), + title: t.appointments, + articles: [ + { + title: "Booking an appointment", + content: + "To book an appointment:\n\n1. Go to 'Book Appointment' from your dashboard\n2. Select the government department\n3. Choose the service you need\n4. Select an available agent\n5. Pick a convenient date and time\n6. Provide the purpose of your visit\n7. Confirm your appointment details\n8. Receive confirmation via email and SMS\n\nArrive 10 minutes early with all required documents.", + }, + { + title: "Rescheduling appointments", + content: + "To reschedule an appointment:\n\n1. Go to 'My Appointments' in your dashboard\n2. Find the appointment you want to reschedule\n3. Click 'Reschedule'\n4. Select a new available time slot\n5. Confirm the changes\n6. You'll receive updated confirmation\n\nNote: You can reschedule up to 24 hours before your appointment time.", + }, + { + title: "Canceling appointments", + content: + "To cancel an appointment:\n\n1. Navigate to 'My Appointments'\n2. Select the appointment to cancel\n3. Click 'Cancel Appointment'\n4. Provide a reason for cancellation (optional)\n5. Confirm the cancellation\n6. You'll receive cancellation confirmation\n\nCanceling early helps others book available slots.", + }, + { + title: "Appointment reminders", + content: + "Appointment reminder system:\n\n• **24 hours before**: Email and SMS reminder\n• **2 hours before**: Final SMS reminder\n• **Digital calendar**: Add appointment to your calendar\n• **Dashboard alerts**: Visual reminders when you log in\n\nYou can customize reminder preferences in your settings to receive notifications via your preferred method.", + }, + ], + }, + ]; + + const handleArticleClick = (articleTitle: string) => { + setSelectedArticle(selectedArticle === articleTitle ? null : articleTitle); + }; + + const getSelectedArticleContent = () => { + const currentCategory = helpCategories.find( + (cat) => cat.id === activeCategory + ); + if (!currentCategory || !selectedArticle) return null; + + const article = currentCategory.articles.find( + (art) => typeof art === "object" && art.title === selectedArticle + ); + return typeof article === "object" ? article.content : null; + }; + + const contactMethods = [ + { + icon: ( + + + + ), + title: t.phone, + value: "+94 11 234 5678", + description: "Available 24/7", + }, + { + icon: ( + + + + + ), + title: t.email, + value: "support@govlink.lk", + description: "Response within 24 hours", + }, + { + icon: ( + + + + ), + title: t.chat, + value: "Start Live Chat", + description: "Mon-Fri 8AM-6PM", + }, + ]; + + const faqs = [ + { + question: "How do I reset my password?", + answer: + 'You can reset your password by clicking the "Forgot Password" link on the login page and following the instructions sent to your email.', + }, + { + question: "How long does service processing take?", + answer: + "Processing times vary by service type. Most applications are processed within 5-10 business days. You can track the status in your dashboard.", + }, + { + question: "What documents do I need for verification?", + answer: + "You typically need a valid NIC, proof of address, and any service-specific documents. Check the requirements for each service.", + }, + { + question: "Can I cancel my application?", + answer: + "Yes, you can cancel applications that haven't been processed yet. Go to your dashboard and select the application to cancel.", + }, + ]; + + return ( + +
+ {/* Search Bar */} +
+
+ setSearchQuery(e.target.value)} + className="w-full px-4 py-3 pl-12 border border-border rounded-xl bg-card/50 backdrop-blur-md focus:outline-none focus:ring-2 focus:ring-[#FFC72C]/50 shadow-lg" + /> + + + + +
+
+ +
+ {/* Categories Sidebar */} +
+
+

{t.categories}

+ +
+
+ + {/* Main Content */} +
+ {/* Help Articles */} +
+

+ {helpCategories.find((cat) => cat.id === activeCategory)?.title} +

+
+ {helpCategories + .find((cat) => cat.id === activeCategory) + ?.articles.map((article, index) => ( +
+ + + {/* Article Content */} + {selectedArticle === + (typeof article === "object" + ? article.title + : article) && ( +
+
+ {typeof article === "object" ? ( +
+                                {article.content}
+                              
+ ) : ( +

+ Content for this article will be available soon. +

+ )} +
+
+ )} +
+ ))} +
+
+ + {/* FAQs */} +
+

+ + + + + + {t.faqs} +

+
+ {faqs.map((faq, index) => ( +
+ +
+ {faq.question} + + + +
+
+
+

{faq.answer}

+
+
+ ))} +
+
+ + {/* Contact Support */} +
+

+ + + + {t.contact} +

+
+ {contactMethods.map((method, index) => ( +
+
+ {method.icon} + {method.title} +
+
+ {method.value} +
+
+ {method.description} +
+
+ ))} +
+
+
+
+
+
+ ); +}; + +export default UserHelpPage; diff --git a/src/app/user/settings/page.tsx b/src/app/user/settings/page.tsx new file mode 100644 index 0000000..c80a40f --- /dev/null +++ b/src/app/user/settings/page.tsx @@ -0,0 +1,270 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { useAuth } from "@/lib/auth/AuthContext"; +import UserDashboardLayout from "@/components/user/dashboard/UserDashboardLayout"; + +const UserSettingsPage = () => { + const { user } = useAuth(); + const [language, setLanguage] = useState<"en" | "si" | "ta">("en"); + const [emailNotifications, setEmailNotifications] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [saveMessage, setSaveMessage] = useState(""); + + const translations = { + en: { + title: "Account Settings", + subtitle: "Manage your account preferences and notification settings", + profileSection: "Profile Information", + email: "Email Address", + phone: "Phone Number", + notificationSection: "Notification Preferences", + emailNotifications: "Email Notifications", + save: "Save Changes", + saving: "Saving...", + cancel: "Cancel", + saveSuccess: "Settings saved successfully!", + saveError: "Failed to save settings. Please try again.", + }, + si: { + title: "ගිණුම් සැකසුම්", + subtitle: "ඔබේ ගිණුම් මනාපයන් සහ දැනුම්දීම් සැකසුම් කළමනාකරණය කරන්න", + profileSection: "පැතිකඩ තොරතුරු", + email: "ඊමේල් ලිපිනය", + phone: "දුරකථන අංකය", + notificationSection: "දැනුම්දීම් මනාපයන්", + emailNotifications: "ඊමේල් දැනුම්දීම්", + save: "වෙනස්කම් සුරකින්න", + saving: "සුරකිමින්...", + cancel: "අවලංගු කරන්න", + saveSuccess: "සැකසුම් සාර්ථකව සුරකින ලදී!", + saveError: "සැකසුම් සුරැකීමට අසමත් විය. කරුණාකර නැවත උත්සාහ කරන්න.", + }, + ta: { + title: "கணக்கு அமைப்புகள்", + subtitle: + "உங்கள் கணக்கு விருப்பத்தேர்வுகள் மற்றும் அறிவிப்பு அமைப்புகளை நிர்வகிக்கவும்", + profileSection: "சுயவிவர தகவல்", + email: "மின்னஞ்சல் முகவரி", + phone: "தொலைபேசி எண்", + notificationSection: "அறிவிப்பு விருப்பத்தேர்வுகள்", + emailNotifications: "மின்னஞ்சல் அறிவிப்புகள்", + save: "மாற்றங்களை சேமி", + saving: "சேமிக்கிறது...", + cancel: "ரத்து செய்", + saveSuccess: "அமைப்புகள் வெற்றிகரமாக சேமிக்கப்பட்டன!", + saveError: + "அமைப்புகளை சேமிக்க முடியவில்லை. தயவுசெய்து மீண்டும் முயற்சிக்கவும்.", + }, + }; + + const t = translations[language]; + + // Load current settings when component mounts + useEffect(() => { + const loadSettings = async () => { + try { + const response = await fetch("/api/user/settings", { + credentials: "include", + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + setEmailNotifications(data.settings.emailNotifications); + setLanguage(data.settings.language); + } + } + } catch (error) { + console.error("Error loading settings:", error); + } + }; + + loadSettings(); + }, []); + + const handleSave = async () => { + setIsSaving(true); + setSaveMessage(""); + + try { + // Create settings payload + const settingsData = { + emailNotifications, + language, + }; + + // Make API call to save settings + const response = await fetch("/api/user/settings", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(settingsData), + }); + + if (response.ok) { + setSaveMessage(t.saveSuccess); + // Clear success message after 3 seconds + setTimeout(() => setSaveMessage(""), 3000); + } else { + throw new Error("Failed to save settings"); + } + } catch (error) { + console.error("Error saving settings:", error); + setSaveMessage(t.saveError); + // Clear error message after 5 seconds + setTimeout(() => setSaveMessage(""), 5000); + } finally { + setIsSaving(false); + } + }; + + return ( + +
+ {/* Profile Information */} +
+

+ + + + + {t.profileSection} +

+
+
+ + +
+
+ + +
+
+
+ + {/* Notification Preferences */} +
+

+ + + + + {t.notificationSection} +

+
+
+ + +
+
+
+ + {/* Action Buttons */} +
+ {/* Success/Error Message */} + {saveMessage && ( +
+ {saveMessage} +
+ )} + +
+ + +
+
+
+
+ ); +}; + +export default UserSettingsPage; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index b7083d2..3da2f44 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -8,7 +8,7 @@ import { Menu, X, User, ChevronDown } from "lucide-react"; import { ThemeToggle } from "./ThemeToggle"; import { CompactLanguageSwitcher } from "./LanguageSwitcher"; import { useTranslation } from "@/lib/i18n/hooks/useTranslation"; -import { LotusIcon as NavbarLotusIcon } from '@/components/Icons/LotusIcon'; +import { LotusIcon as NavbarLotusIcon } from "@/components/Icons/LotusIcon"; // Navbar lotus icon now imported from shared Icons @@ -16,9 +16,9 @@ import { LotusIcon as NavbarLotusIcon } from '@/components/Icons/LotusIcon'; const smoothScrollTo = (id: string) => { const element = document.getElementById(id); if (element) { - element.scrollIntoView({ - behavior: 'smooth', - block: 'start' + element.scrollIntoView({ + behavior: "smooth", + block: "start", }); } }; @@ -26,7 +26,7 @@ const smoothScrollTo = (id: string) => { export const Header: React.FC = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [loginDropdownOpen, setLoginDropdownOpen] = useState(false); - const { t } = useTranslation('home'); + const { t } = useTranslation("home"); const loginDropdownRef = useRef(null); const toggleMobileMenu = () => setMobileMenuOpen(!mobileMenuOpen); @@ -34,19 +34,22 @@ export const Header: React.FC = () => { // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (loginDropdownRef.current && !loginDropdownRef.current.contains(event.target as Node)) { + if ( + loginDropdownRef.current && + !loginDropdownRef.current.contains(event.target as Node) + ) { setLoginDropdownOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener("mousedown", handleClickOutside); }; }, []); return ( - { {/* Desktop Navigation */}
- - - -
{/* CTA Button - Desktop */}
- - {t('navigation.contact')} + + {t("navigation.contact")}
@@ -100,6 +114,15 @@ export const Header: React.FC = () => {
+ {/* Sign Up Button */} + + + {t("navigation.signUp")} + + {/* Right-most Login Dropdown (after ThemeToggle) */}
@@ -127,7 +154,7 @@ export const Header: React.FC = () => { className="flex items-center gap-3 px-4 py-3 text-sm text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-all duration-200" > - {t('navigation.userLogin')} + {t("navigation.userLogin")} { className="flex items-center gap-3 px-4 py-3 text-sm text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-all duration-200" > - {t('navigation.agentLogin')} + {t("navigation.agentLogin")} { className="flex items-center gap-3 px-4 py-3 text-sm text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-all duration-200" > - {t('navigation.departmentLogin')} + {t("navigation.departmentLogin")}
)} -
@@ -158,7 +184,10 @@ export const Header: React.FC = () => { {/* Mobile Language Selector */} - @@ -177,45 +206,95 @@ export const Header: React.FC = () => { >
- - - - + {/* Mobile Login Options */}
- {t('navigation.loginOptions')} + {t("navigation.loginOptions")}
- setMobileMenuOpen(false)} className="flex items-center gap-3 px-4 py-3 text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded-lg transition-all duration-200"> + + {/* Sign Up Button for Mobile */} + setMobileMenuOpen(false)} + className="flex items-center gap-3 px-4 py-3 text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded-lg transition-all duration-200" + > + + {t("navigation.signUp")} + + + setMobileMenuOpen(false)} + className="flex items-center gap-3 px-4 py-3 text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded-lg transition-all duration-200" + > - {t('navigation.userLogin')} + {t("navigation.userLogin")} - setMobileMenuOpen(false)} className="flex items-center gap-3 px-4 py-3 text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded-lg transition-all duration-200"> + setMobileMenuOpen(false)} + className="flex items-center gap-3 px-4 py-3 text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded-lg transition-all duration-200" + > - {t('navigation.agentLogin')} + {t("navigation.agentLogin")} - setMobileMenuOpen(false)} className="flex items-center gap-3 px-4 py-3 text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded-lg transition-all duration-200"> + setMobileMenuOpen(false)} + className="flex items-center gap-3 px-4 py-3 text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded-lg transition-all duration-200" + > - {t('navigation.departmentLogin')} + {t("navigation.departmentLogin")}
- - setMobileMenuOpen(false)} className="block w-full"> + + setMobileMenuOpen(false)} + className="block w-full" + >
- {t('navigation.contact')} + + {t("navigation.contact")} +
- @@ -226,4 +305,4 @@ export const Header: React.FC = () => {
); -}; \ No newline at end of file +}; diff --git a/src/components/department/DepartmentSidebar.tsx b/src/components/department/DepartmentSidebar.tsx index bda7c47..df642b7 100644 --- a/src/components/department/DepartmentSidebar.tsx +++ b/src/components/department/DepartmentSidebar.tsx @@ -44,7 +44,7 @@ export default function DepartmentSidebar() { -
+
diff --git a/src/components/department/SubmissionManagement.tsx b/src/components/department/SubmissionManagement.tsx index 5870797..f64eebd 100644 --- a/src/components/department/SubmissionManagement.tsx +++ b/src/components/department/SubmissionManagement.tsx @@ -64,10 +64,12 @@ export default function SubmissionManagement() { const StatusBadge = ({ status }: { status: string }) => { const statusMap: Record; color: string }> = { PENDING: { text: t('submissions.pending'), icon: Clock, color: 'text-[#FFC72C] bg-[#FFC72C]/10' }, + CONFIRMED: { text: 'Confirmed', icon: CheckCircle, color: 'text-[#008060] bg-[#008060]/10' }, + CANCELLED: { text: 'Cancelled', icon: XCircle, color: 'text-[#FF5722] bg-[#FF5722]/10' }, + COMPLETED: { text: t('submissions.completed'), icon: CheckCircle, color: 'text-[#008060] bg-[#008060]/10' }, APPROVED: { text: t('submissions.approved'), icon: CheckCircle, color: 'text-[#008060] bg-[#008060]/10' }, REJECTED: { text: t('submissions.rejected'), icon: XCircle, color: 'text-[#FF5722] bg-[#FF5722]/10' }, IN_REVIEW: { text: t('submissions.inReview'), icon: HelpCircle, color: 'text-[#8D153A] bg-[#8D153A]/10' }, - COMPLETED: { text: t('submissions.completed'), icon: CheckCircle, color: 'text-[#008060] bg-[#008060]/10' }, }; const statusInfo = statusMap[status] || { @@ -86,16 +88,16 @@ export default function SubmissionManagement() { }; return ( -
-
+
+

- {t('submissions.title')} + Appointments Management

-

{t('submissions.description')}

+

Review, process, and manage all citizen appointments for your department.

@@ -169,7 +173,7 @@ export default function SubmissionManagement() { ) : submissions.length === 0 ? (
-

No submissions found

+

No appointments found

{searchTerm && (

Try adjusting your search or filters @@ -181,13 +185,13 @@ export default function SubmissionManagement() { - - + + - - + + - + @@ -196,10 +200,21 @@ export default function SubmissionManagement() {
{t('submissions.table.citizen')}{t('submissions.table.service')}CitizenAppointment Details Priority{t('submissions.table.status')}{t('submissions.table.submittedOn')}StatusSubmitted On Agent{t('submissions.table.actions')}Actions
{sub.applicantName}
{sub.applicantEmail}
+ {sub.citizenNIC && ( +
NIC: {sub.citizenNIC}
+ )} + {sub.contactPhone && ( +
📱 {sub.contactPhone}
+ )}
{sub.title}
-
ID: {sub.submissionId}
+
Ref: {sub.bookingReference || sub.submissionId}
+ {sub.appointmentDate && ( +
+ 📅 {new Date(sub.appointmentDate).toLocaleDateString()} at {sub.appointmentTime} +
+ )}
- {sub.agentId || 'Unassigned'} + {sub.agentName ? ( +
+
{sub.agentName}
+
{sub.agentEmail}
+ {sub.agentStatus === 'INACTIVE' && ( + + Inactive + + )} +
+ ) : sub.status === 'PENDING' ? ( + Unaccepted + ) : ( + Unassigned + )}
))} @@ -98,16 +64,16 @@ export default function DepartmentAnalyticsDashboard() {
- {loading ? ( - - ) : ( - - )} + +
+
+ {stat.change}
-
{stat.change}
-
{loading ? '--' : stat.value}
+
{stat.value}
{stat.label}
@@ -116,15 +82,18 @@ export default function DepartmentAnalyticsDashboard() { {/* Tab Navigation */}
-
- + - -
@@ -132,9 +101,10 @@ export default function DepartmentAnalyticsDashboard() { {/* Tab Content */}
{activeTab === 'metrics' && } - {activeTab === 'reports' &&

Service Reports will be displayed here.

} - {activeTab === 'leaderboard' &&

Agent Leaderboard will be displayed here.

} + {activeTab === 'reports' && } + {activeTab === 'leaderboard' && } + {activeTab === 'appointments' && }
); -} \ No newline at end of file +} diff --git a/src/components/department/analytics/DepartmentPerformanceMetrics.tsx b/src/components/department/analytics/DepartmentPerformanceMetrics.tsx index 9862aa5..cd71a64 100644 --- a/src/components/department/analytics/DepartmentPerformanceMetrics.tsx +++ b/src/components/department/analytics/DepartmentPerformanceMetrics.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { useTranslation } from '@/lib/i18n/hooks/useTranslation'; -// Simple chart component, as seen in agent analytics +// Simple chart component const SimpleChart = ({ data, colorFrom, colorTo }: { data: number[], colorFrom: string, colorTo: string }) => { if (data.length === 0) return
; const maxValue = Math.max(...data); @@ -31,12 +31,14 @@ const SimpleChart = ({ data, colorFrom, colorTo }: { data: number[], colorFrom: export default function DepartmentPerformanceMetrics() { const { t } = useTranslation('department'); - // Mock data for visualizations + // Dummy data for visualizations const submissionsTrendData = [120, 150, 130, 180, 200, 190, 220, 250, 230, 280, 300, 290]; const topAgents = [ - { name: "Nimali Gunaratne", score: 98.2, isCurrentUser: false, rank: 1 }, - { name: "Bhanuka Rajapaksa", score: 95.5, isCurrentUser: false, rank: 2 }, - { name: "Priya De Silva", score: 92.1, isCurrentUser: false, rank: 3 }, + { name: "Nimali Gunaratne", score: "98.2", isCurrentUser: false, rank: 1, completed: 45 }, + { name: "Bhanuka Rajapaksa", score: "95.5", isCurrentUser: false, rank: 2, completed: 42 }, + { name: "Priya De Silva", score: "92.1", isCurrentUser: false, rank: 3, completed: 38 }, + { name: "Kasun Perera", score: "89.8", isCurrentUser: false, rank: 4, completed: 35 }, + { name: "Sanduni Fernando", score: "87.3", isCurrentUser: false, rank: 5, completed: 32 }, ]; return ( @@ -52,11 +54,14 @@ export default function DepartmentPerformanceMetrics() {

{t('analytics.metrics.topPerformingAgents')}

- {topAgents.map((agent) => ( -
+ {topAgents.slice(0, 5).map((agent) => ( +
#{agent.rank}
-
{agent.name}
+
+
{agent.name}
+
{agent.completed} completed
+
{agent.score}%
@@ -66,4 +71,4 @@ export default function DepartmentPerformanceMetrics() {
); -} \ No newline at end of file +} diff --git a/src/components/department/analytics/ServiceReports.tsx b/src/components/department/analytics/ServiceReports.tsx new file mode 100644 index 0000000..c2a8e1c --- /dev/null +++ b/src/components/department/analytics/ServiceReports.tsx @@ -0,0 +1,181 @@ +// src/components/department/analytics/ServiceReports.tsx +"use client"; +import React from 'react'; +import { FileText, TrendingUp, Clock } from 'lucide-react'; + +interface ServiceStats { + id: string; + name: string; + category: string; + isActive: boolean; + processingTime: string; + fee: number; + submissionsCount: number; + avgProcessingDays: number; + completionRate: number; +} + +export default function ServiceReports() { + + // Dummy service data for display + const serviceStats = [ + { + id: '1', + name: 'Project Accept', + category: 'No dis', + isActive: true, + processingTime: '35 days', + fee: 120, + submissionsCount: 1, + avgProcessingDays: 35, + completionRate: 100, + }, + { + id: '2', + name: 'Certificate Services', + category: 'Official Documents', + isActive: true, + processingTime: '5-7 days', + fee: 250, + submissionsCount: 47, + avgProcessingDays: 6, + completionRate: 96, + }, + { + id: '3', + name: 'Robot Processing', + category: 'Automated Services', + isActive: true, + processingTime: '7-10 days', + fee: 2500, + submissionsCount: 156, + avgProcessingDays: 8, + completionRate: 87, + }, + { + id: '4', + name: 'Death Certificate Request', + category: 'Vital Records', + isActive: true, + processingTime: '1-2 days', + fee: 300, + submissionsCount: 45, + avgProcessingDays: 1, + completionRate: 100, + }, + { + id: '5', + name: 'Property Tax Assessment', + category: 'Property Services', + isActive: false, + processingTime: '5-7 days', + fee: 1000, + submissionsCount: 23, + avgProcessingDays: 6, + completionRate: 82, + } + ]; + + return ( +
+
+
+
+ + Active Services +
+
4
+
+ +
+
+ + Total Services +
+
5
+
+ +
+
+ + Avg Processing +
+
2.3 days
+
+
+ +
+
+

Service Performance Report

+
+ +
+ + + + + + + + + + + + + {serviceStats.map((service: ServiceStats) => ( + + + + + + + + + ))} + +
+ Service Name + + Category + + Submissions + + Avg Processing + + Completion Rate + + Status +
+
{service.name}
+
+ + {service.category} + + + {service.submissionsCount} + + {service.avgProcessingDays} days + +
+
+
+
+ {service.completionRate}% +
+
+ + {service.isActive ? 'Active' : 'Inactive'} + +
+
+
+
+ ); +} diff --git a/src/components/user/chat/AuthPrompt.tsx b/src/components/user/chat/AuthPrompt.tsx new file mode 100644 index 0000000..f4e03c5 --- /dev/null +++ b/src/components/user/chat/AuthPrompt.tsx @@ -0,0 +1,101 @@ +// src/components/user/chat/AuthPrompt.tsx +"use client"; +import React from 'react'; +import { User, ArrowRight, Shield } from 'lucide-react'; +import { useRouter } from 'next/navigation'; + +interface AuthPromptProps { + message: string; + language?: 'en' | 'si' | 'ta'; + redirectPath?: string; +} + +const translations = { + en: { + loginRequired: 'Login Required', + loginPrompt: 'Please login to continue with booking appointments and accessing personalized services.', + loginButton: 'Login to Continue', + registerPrompt: "Don't have an account?", + registerButton: 'Register Now', + secureMessage: 'Your data is secure and encrypted' + }, + si: { + loginRequired: 'ප්‍රවේශය අවශ්‍යයි', + loginPrompt: 'හමුවීම් වෙන්කරවීම සහ පුද්ගලිකත සේවා වලට ප්‍රවේශ වීමට කරුණාකර ප්‍රවේශ වන්න.', + loginButton: 'ඉදිරියට යාමට ප්‍රවේශ වන්න', + registerPrompt: 'ගිණුමක් නැත?', + registerButton: 'දැන් ලියාපදිංචි වන්න', + secureMessage: 'ඔබගේ දත්ත ආරක්ෂිත සහ සංකේතනය කර ඇත' + }, + ta: { + loginRequired: 'உள்நுழைவு தேவை', + loginPrompt: 'சந்திப்பு முன்பதிவு மற்றும் தனிப்பயன் சேவைகளைப் பெற உள்நுழையவும்.', + loginButton: 'தொடர உள்நுழையவும்', + registerPrompt: 'கணக்கு இல்லையா?', + registerButton: 'இப்போது பதிவு செய்யவும்', + secureMessage: 'உங்கள் தரவு பாதுகாப்பானது மற்றும் மறைகுறியாக்கப்பட்டுள்ளது' + } +}; + +export const AuthPrompt: React.FC = ({ + message, + language = 'en', + redirectPath = '/Ragbot' +}) => { + const router = useRouter(); + const t = translations[language]; + + const handleLogin = () => { + router.push(`/user/auth/login?redirect=${encodeURIComponent(redirectPath)}`); + }; + + const handleRegister = () => { + router.push(`/user/auth/register?redirect=${encodeURIComponent(redirectPath)}`); + }; + + return ( +
+
+
+
+ +
+
+ +
+
+

+ {t.loginRequired} +

+

+ {message || t.loginPrompt} +

+
+ +
+ + + +
+ +

+ + {t.secureMessage} +

+
+
+
+ ); +}; diff --git a/src/components/user/chat/BookingChatButton.tsx b/src/components/user/chat/BookingChatButton.tsx new file mode 100644 index 0000000..0d6bb47 --- /dev/null +++ b/src/components/user/chat/BookingChatButton.tsx @@ -0,0 +1,143 @@ +// src/components/user/chat/BookingChatButton.tsx +"use client"; +import React from 'react'; +import { Calendar, User, CheckCircle, AlertCircle } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useBookingChat } from '@/lib/hooks/useBookingChat'; +import { useAuth } from '@/lib/auth/AuthContext'; + +interface BookingChatButtonProps { + onLoginRequest?: () => void; + language?: 'en' | 'si' | 'ta'; +} + +const translations = { + en: { + openBookingForm: 'Open Booking Form', + loginRequired: 'Login Required', + loginToBook: 'Login to Book Appointment', + bookingReady: 'Booking Form Ready', + collectingInfo: 'Collecting Information...', + complete: 'Information Complete', + loginFirst: 'Please login to proceed with booking' + }, + si: { + openBookingForm: 'වෙන්කරවීමේ ආකෘතිය විවෘත කරන්න', + loginRequired: 'ප්‍රවේශය අවශ්‍යයි', + loginToBook: 'හමුවීමක් වෙන්කරවීමට ප්‍රවේශ වන්න', + bookingReady: 'වෙන්කරවීමේ ආකෘතිය සූදානම්', + collectingInfo: 'තොරතුරු එකතු කරමින්...', + complete: 'තොරතුරු සම්පූර්ණයි', + loginFirst: 'වෙන්කරවීම සමඟ ඉදිරියට යාමට කරුණාකර ප්‍රවේශ වන්න' + }, + ta: { + openBookingForm: 'முன்பதிவு படிவத்தைத் திறக்கவும்', + loginRequired: 'உள்நுழைவு தேவை', + loginToBook: 'சந்திப்பு முன்பதிவு செய்ய உள்நுழையவும்', + bookingReady: 'முன்பதிவு படிவம் தயார்', + collectingInfo: 'தகவல் சேகரிக்கப்படுகிறது...', + complete: 'தகவல் முடிந்தது', + loginFirst: 'முன்பதிவுடன் தொடர உள்நுழையவும்' + } +}; + +export const BookingChatButton: React.FC = ({ + onLoginRequest, + language = 'en' +}) => { + const router = useRouter(); + const { isAuthenticated, user } = useAuth(); + const { bookingData, canProceedToForm, isReadyForBooking } = useBookingChat(); + const t = translations[language]; + + const handleBookingAction = () => { + if (!isAuthenticated || !user) { + if (onLoginRequest) { + onLoginRequest(); + } else { + router.push('/user/auth/login?redirect=/user/booking/new'); + } + return; + } + + if (canProceedToForm()) { + // Navigate to booking form with pre-filled data + const queryParams = new URLSearchParams(); + if (bookingData?.department) queryParams.set('department', bookingData.department); + if (bookingData?.service) queryParams.set('service', bookingData.service); + if (bookingData?.agent) queryParams.set('agent', bookingData.agent); + if (bookingData?.date) queryParams.set('date', bookingData.date); + if (bookingData?.time) queryParams.set('time', bookingData.time); + if (bookingData?.notes) queryParams.set('notes', bookingData.notes); + + router.push(`/user/booking/new?fromChat=true&${queryParams.toString()}`); + } + }; + + const getButtonContent = () => { + if (!isAuthenticated) { + return { + icon: , + text: t.loginToBook, + subtext: t.loginFirst, + className: 'from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700', + disabled: false + }; + } + + if (canProceedToForm()) { + return { + icon: , + text: t.openBookingForm, + subtext: t.bookingReady, + className: 'from-green-500 to-green-600 hover:from-green-600 hover:to-green-700', + disabled: false + }; + } + + // When the conversation is still gathering details, don't display a "Collecting Information" label + // Show a neutral disabled state with no subtext to avoid the collecting message appearing under the chat + if (isReadyForBooking()) { + return { + icon: , + text: t.bookingReady, + subtext: '', + className: 'from-yellow-500 to-yellow-600 hover:from-yellow-600 hover:to-yellow-700', + disabled: true + }; + } + + return { + icon: , + text: t.bookingReady, + subtext: '', + className: 'from-gray-500 to-gray-600', + disabled: true + }; + }; + + const { icon, text, subtext, className, disabled } = getButtonContent(); + + return ( +
+ +
+ ); +}; diff --git a/src/components/user/chat/BookingChatManager.tsx b/src/components/user/chat/BookingChatManager.tsx new file mode 100644 index 0000000..1652ed2 --- /dev/null +++ b/src/components/user/chat/BookingChatManager.tsx @@ -0,0 +1,308 @@ +// src/components/user/chat/BookingChatManager.tsx +"use client"; +import React, { useCallback, useEffect, useState } from 'react'; +import { useBookingChat, BookingChatContext } from '@/lib/hooks/useBookingChat'; +import { AuthPrompt } from './AuthPrompt'; +import { useAuth } from '@/lib/auth/AuthContext'; +import { useRouter } from 'next/navigation'; + +interface BookingConversationData { + department?: string; + service?: string; + agentType?: string; + preferredDate?: string; + preferredTime?: string; + additionalNotes?: string; +} + +interface BookingChatManagerProps { + onBookingQuestionGenerated?: (question: string) => void; + onLoginRequired?: () => void; + language?: 'en' | 'si' | 'ta'; + lastUserMessage?: string; + sessionId?: string; + messages?: Array<{ type: 'user' | 'bot'; text: string; timestamp: Date }>; +} + +interface BookingIntent { + isBookingIntent: boolean; + extractedData?: BookingChatContext; + confidence: number; +} + +const bookingKeywords = [ + 'book', 'appointment', 'schedule', 'meeting', 'visit', 'apply for', + 'register', 'submit', 'request', 'service', 'office', 'agent', + 'වෙන්කරවීම', 'හමුවීම', 'ලියාපදිංචිය', 'සේවාව', 'කාර්යාලය', + 'முன்பதிவு', 'சந்திப்பு', 'பதிவு', 'சேவை', 'அலுவலகம்' +]; + +const departmentKeywords = { + immigration: ['passport', 'visa', 'immigration', 'emigration', 'travel document'], + business: ['business', 'company', 'registration', 'trade license', 'commercial'], + health: ['health', 'medical', 'certificate', 'clinic', 'hospital'], + education: ['education', 'school', 'university', 'certificate', 'scholarship'], + tax: ['tax', 'revenue', 'VAT', 'income tax', 'registration'] +}; + +export const BookingChatManager: React.FC = ({ + onBookingQuestionGenerated, + onLoginRequired, + language = 'en', + lastUserMessage, + sessionId, + messages = [] +}) => { + const router = useRouter(); + const { isAuthenticated } = useAuth(); + const [showAuthPrompt, setShowAuthPrompt] = useState(false); + const [completedBookingData, setCompletedBookingData] = useState(null); + const [hasNavigated, setHasNavigated] = useState(false); + const { + bookingData, + getNextQuestion, + processUserResponse, + saveBookingData + } = useBookingChat(); + + // Check for completed booking conversation from the API + useEffect(() => { + const checkCompletedBooking = async () => { + if (!isAuthenticated) return; + + try { + // Use prop sessionId or fallback to sessionStorage + const currentSessionId = sessionId || sessionStorage.getItem('ragSessionId') || `rag_session_${Date.now()}`; + + const response = await fetch(`/api/ragbot/booking-conversation-status?sessionId=${currentSessionId}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + + if (response.ok) { + const data = await response.json(); + if (data.isComplete && data.collectedData) { + setCompletedBookingData(data.collectedData); + // Also sync with the booking chat hook + await saveBookingData({ + department: data.collectedData.department, + service: data.collectedData.service, + agent: data.collectedData.agentType, + date: data.collectedData.preferredDate, + time: data.collectedData.preferredTime, + notes: data.collectedData.additionalNotes, + completed: true + }); + } + } + } catch (error) { + console.error('Error checking booking status:', error); + } + }; + + checkCompletedBooking(); + + // Check periodically for updates + const interval = setInterval(checkCompletedBooking, 2000); + return () => clearInterval(interval); + }, [isAuthenticated, saveBookingData, sessionId]); + + const detectBookingIntent = useCallback((message: string): BookingIntent => { + const lowerMessage = message.toLowerCase(); + const hasBookingKeyword = bookingKeywords.some(keyword => + lowerMessage.includes(keyword.toLowerCase()) + ); + + if (!hasBookingKeyword) { + return { isBookingIntent: false, confidence: 0 }; + } + + // Extract department information + const extractedData: BookingChatContext = {}; + let confidence = 0.5; + + for (const [dept, keywords] of Object.entries(departmentKeywords)) { + if (keywords.some(keyword => lowerMessage.includes(keyword))) { + extractedData.departmentName = dept; + confidence = 0.8; + break; + } + } + + return { + isBookingIntent: true, + extractedData: Object.keys(extractedData).length > 0 ? extractedData : undefined, + confidence + }; + }, []); + + const handleBookingFlow = useCallback(async (message: string) => { + if (!isAuthenticated) { + setShowAuthPrompt(true); + if (onLoginRequired) { + onLoginRequired(); + } + return; + } + + try { + const intent = detectBookingIntent(message); + + if (intent.isBookingIntent && !bookingData) { + // Start new booking process + await processUserResponse(message, intent.extractedData); + const nextQuestion = getNextQuestion(); + if (nextQuestion && onBookingQuestionGenerated) { + onBookingQuestionGenerated(nextQuestion); + } + } else if (bookingData && !bookingData.completed) { + // Continue existing booking process + await processUserResponse(message); + const nextQuestion = getNextQuestion(); + if (nextQuestion && onBookingQuestionGenerated) { + onBookingQuestionGenerated(nextQuestion); + } + } + } catch (error) { + console.error('Error in booking flow:', error); + if (error instanceof Error && error.message.includes('Authentication required')) { + setShowAuthPrompt(true); + if (onLoginRequired) { + onLoginRequired(); + } + } + } + }, [ + isAuthenticated, + bookingData, + detectBookingIntent, + processUserResponse, + getNextQuestion, + onBookingQuestionGenerated, + onLoginRequired, + setShowAuthPrompt + ]); + + // Process last user message for booking intent + useEffect(() => { + if (lastUserMessage) { + const intent = detectBookingIntent(lastUserMessage); + if (intent.isBookingIntent) { + handleBookingFlow(lastUserMessage); + } + } + }, [lastUserMessage, detectBookingIntent, handleBookingFlow]); + + // Check if the last few bot messages contain completion indicators + const recentBotMessages = messages + .filter(msg => msg.type === 'bot') + .slice(-3) // Check last 3 bot messages + .map(msg => msg.text); + + const hasCompletionMessage = recentBotMessages.some(text => + text.includes("Perfect! I've collected all your information") || + text.includes("Open Booking Form") || + text.includes("booking information is ready") || + text.includes("You can now proceed to complete your booking") + ); + + // Check if booking is completed (either through API or completion message) + const isBookingCompleted = completedBookingData || hasCompletionMessage; + + // Auto-navigate to booking form when conversation completes + useEffect(() => { + if (isBookingCompleted && isAuthenticated && !hasNavigated) { + console.log('Auto-navigating to booking form...'); + + // Build query params from either completed data or bookingData + const queryParams = new URLSearchParams(); + queryParams.set('fromChat', 'true'); + + const dataSource = completedBookingData || bookingData; + if (dataSource) { + // Handle both BookingChatContext and BookingData types + if ('departmentName' in dataSource && dataSource.departmentName) { + queryParams.set('department', dataSource.departmentName); + } else if ('department' in dataSource && dataSource.department) { + queryParams.set('department', dataSource.department); + } + + if ('serviceName' in dataSource && dataSource.serviceName) { + queryParams.set('service', dataSource.serviceName); + } else if ('service' in dataSource && dataSource.service) { + queryParams.set('service', dataSource.service); + } + + // Handle agentType from API data + if ('agentType' in dataSource && (dataSource as BookingConversationData).agentType) { + queryParams.set('agent', (dataSource as BookingConversationData).agentType!); + } else if ('agentName' in dataSource && dataSource.agentName) { + queryParams.set('agent', dataSource.agentName); + } else if ('agent' in dataSource && dataSource.agent) { + queryParams.set('agent', dataSource.agent); + } + + // Handle preferredDate from API data + if ('preferredDate' in dataSource && (dataSource as BookingConversationData).preferredDate) { + queryParams.set('date', (dataSource as BookingConversationData).preferredDate!); + } else if ('date' in dataSource && dataSource.date) { + queryParams.set('date', dataSource.date); + } + + // Handle preferredTime from API data + if ('preferredTime' in dataSource && (dataSource as BookingConversationData).preferredTime) { + queryParams.set('time', (dataSource as BookingConversationData).preferredTime!); + } else if ('time' in dataSource && dataSource.time) { + queryParams.set('time', dataSource.time); + } + + // Handle additionalNotes from API data + if ('additionalNotes' in dataSource && (dataSource as BookingConversationData).additionalNotes) { + queryParams.set('notes', (dataSource as BookingConversationData).additionalNotes!); + } else if ('notes' in dataSource && dataSource.notes) { + queryParams.set('notes', dataSource.notes); + } + } + + setHasNavigated(true); + router.push(`/user/booking/new?${queryParams.toString()}`); + } + }, [isBookingCompleted, isAuthenticated, hasNavigated, completedBookingData, bookingData, router]); + + // Enhanced detection - check if there's any booking-related conversation happening + const hasAnyBookingActivity = + bookingData || + completedBookingData || + hasCompletionMessage || + messages.some(msg => + msg.text.includes('department') || + msg.text.includes('appointment') || + msg.text.includes('book') || + msg.text.includes('schedule') || + msg.text.includes('service') || + msg.text.includes('agent') || + msg.text.includes('Perfect! I') || + msg.text.includes('booking') + ); + + // Don't render anything if no booking activity exists + if (!hasAnyBookingActivity && !lastUserMessage) { + return null; + } + + return ( +
+ {showAuthPrompt && ( + + )} +
+ {isBookingCompleted ? 'Redirecting to booking form...' : 'Processing your booking request...'} +
+
+ ); +}; + +export default BookingChatManager; diff --git a/src/components/user/dashboard/UserDashboardLayout.tsx b/src/components/user/dashboard/UserDashboardLayout.tsx index 2c14915..a30d92e 100644 --- a/src/components/user/dashboard/UserDashboardLayout.tsx +++ b/src/components/user/dashboard/UserDashboardLayout.tsx @@ -1,15 +1,15 @@ // src/components/user/dashboard/UserDashboardLayout.tsx "use client"; -import React, { useState } from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { ThemeToggle } from '@/components/ThemeToggle'; -import { useAuth } from '@/lib/auth/AuthContext'; -import { useLogout } from '@/lib/auth/useAuthUtils'; -import { LotusIcon } from '@/components/Icons/LotusIcon'; +import React, { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { ThemeToggle } from "@/components/ThemeToggle"; +import { useAuth } from "@/lib/auth/AuthContext"; +import { useLogout } from "@/lib/auth/useAuthUtils"; +import { LotusIcon } from "@/components/Icons/LotusIcon"; // Types for translations -type Language = 'en' | 'si' | 'ta'; +type Language = "en" | "si" | "ta"; interface Translation { dashboard: string; @@ -24,39 +24,39 @@ interface Translation { // Translation data const translations: Record = { en: { - dashboard: 'Dashboard', - citizenPortal: 'Citizen Portal', - logout: 'Logout', - profile: 'Profile', - settings: 'Settings', - help: 'Help', - notifications: 'Notifications' + dashboard: "Dashboard", + citizenPortal: "Citizen Portal", + logout: "Logout", + profile: "Profile", + settings: "Settings", + help: "Help", + notifications: "Notifications", }, si: { - dashboard: 'පාලනය', - citizenPortal: 'පුරවැසි පෝට්ලය', - logout: 'ඉවත්වන්න', - profile: 'පැතිකඩ', - settings: 'සැකසුම්', - help: 'උදව්', - notifications: 'දැනුම්දීම්' + dashboard: "පාලනය", + citizenPortal: "පුරවැසි පෝට්ලය", + logout: "ඉවත්වන්න", + profile: "පැතිකඩ", + settings: "සැකසුම්", + help: "උදව්", + notifications: "දැනුම්දීම්", }, ta: { - dashboard: 'டாஷ்போர்டு', - citizenPortal: 'குடிமக்கள் போர்டல்', - logout: 'வெளியேறு', - profile: 'சுயவிவரம்', - settings: 'அமைப்புகள்', - help: 'உதவி', - notifications: 'அறிவிப்புகள்' - } + dashboard: "டாஷ்போர்டு", + citizenPortal: "குடிமக்கள் போர்டல்", + logout: "வெளியேறு", + profile: "சுயவிவரம்", + settings: "அமைப்புகள்", + help: "உதவி", + notifications: "அறிவிப்புகள்", + }, }; // Language options const languageOptions = [ - { code: 'en', label: 'English', nativeLabel: 'English' }, - { code: 'si', label: 'Sinhala', nativeLabel: 'සිංහල' }, - { code: 'ta', label: 'Tamil', nativeLabel: 'தமிழ்' } + { code: "en", label: "English", nativeLabel: "English" }, + { code: "si", label: "Sinhala", nativeLabel: "සිංහල" }, + { code: "ta", label: "Tamil", nativeLabel: "தமிழ்" }, ]; // Lotus icon now imported from shared Icons @@ -67,26 +67,41 @@ const SriLankanBackground = () => {
{/* Main background image */}
-
{/* Overlay gradients for better text readability */}
- + {/* Enhanced lotus-inspired accent patterns */}
-
-
-
+
+
+
{/* Additional subtle accents */} -
-
+
+
); @@ -98,20 +113,20 @@ interface UserDashboardLayoutProps { subtitle?: string; language?: Language; onLanguageChange?: (language: Language) => void; - size?: 'default' | 'compact' | 'dense'; - contentMode?: 'normal' | 'fill'; + size?: "default" | "compact" | "dense"; + contentMode?: "normal" | "fill"; headerContent?: React.ReactNode; } -const UserDashboardLayout: React.FC = ({ - children, - title, +const UserDashboardLayout: React.FC = ({ + children, + title, subtitle, - language = 'en', + language = "en", onLanguageChange, - size = 'default', - contentMode = 'normal', - headerContent + size = "default", + contentMode = "normal", + headerContent, }) => { const router = useRouter(); const { user, isAuthenticated, isLoading } = useAuth(); @@ -121,18 +136,21 @@ const UserDashboardLayout: React.FC = ({ const t = translations[language]; // Get user display data - const userDisplayName = user?.firstName && user?.lastName - ? `${user.firstName} ${user.lastName}` - : user?.email?.split('@')[0] || 'User'; - - const userInitials = user?.firstName && user?.lastName - ? `${user.firstName[0]}${user.lastName[0]}`.toUpperCase() - : user?.email ? user.email[0].toUpperCase() - : 'U'; - - const userShortName = user?.firstName - ? `${user.firstName} ${user.lastName ? user.lastName[0] + '.' : ''}` - : user?.email?.split('@')[0] || 'User'; + const userDisplayName = + user?.firstName && user?.lastName + ? `${user.firstName} ${user.lastName}` + : user?.email?.split("@")[0] || "User"; + + const userInitials = + user?.firstName && user?.lastName + ? `${user.firstName[0]}${user.lastName[0]}`.toUpperCase() + : user?.email + ? user.email[0].toUpperCase() + : "U"; + + const userShortName = user?.firstName + ? `${user.firstName} ${user.lastName ? user.lastName[0] + "." : ""}` + : user?.email?.split("@")[0] || "User"; const handleLanguageChange = (newLanguage: Language) => { if (onLanguageChange) { @@ -142,7 +160,7 @@ const UserDashboardLayout: React.FC = ({ }; const handleLogout = () => { - logoutAndRedirect('/user/auth/login'); + logoutAndRedirect("/user/auth/login"); }; // Show loading state if user data is still loading @@ -156,7 +174,7 @@ const UserDashboardLayout: React.FC = ({ // Redirect to login if not authenticated (this shouldn't happen due to middleware) if (!isAuthenticated) { - router.push('/user/auth/login'); + router.push("/user/auth/login"); return null; } @@ -164,7 +182,7 @@ const UserDashboardLayout: React.FC = ({
{/* EXACT SAME Sri Lankan Background */} - + {/* Header - EXACT SAME styling as Agent Layout */}
- GovLink - {t.citizenPortal} + + GovLink + + + {t.citizenPortal} +
{/* Notification Bell */}
- {isDropdownOpen && ( <> -
setIsDropdownOpen(false)} />
{languageOptions.map((lang) => ( ))} @@ -236,7 +289,7 @@ const UserDashboardLayout: React.FC = ({ )}
- + {/* Profile Dropdown */}
{isProfileDropdownOpen && ( <> -
setIsProfileDropdownOpen(false)} />
-
{userDisplayName}
+
+ {userDisplayName} +
{user?.email}
- {user?.role === 'citizen' ? 'Citizen Account' : user?.role} + {user?.role === "citizen" + ? "Citizen Account" + : user?.role} {user?.accountStatus && ( - + {user.accountStatus} )}
- - + + {t.profile} - - +
-
- +
@@ -326,40 +442,64 @@ const UserDashboardLayout: React.FC = ({ {/* Main Content */} -
+
{headerContent ? ( -
+
{headerContent}
) : ( -
-

+
+

{title}

- + {subtitle && ( -

+

{subtitle}

)}
)} - {contentMode === 'fill' ? ( -
- {children} -
+ {contentMode === "fill" ? ( +
{children}
) : ( children )} @@ -368,4 +508,4 @@ const UserDashboardLayout: React.FC = ({ ); }; -export default UserDashboardLayout; \ No newline at end of file +export default UserDashboardLayout; diff --git a/src/lib/hooks/useBookingChat.ts b/src/lib/hooks/useBookingChat.ts new file mode 100644 index 0000000..3a07459 --- /dev/null +++ b/src/lib/hooks/useBookingChat.ts @@ -0,0 +1,227 @@ +// src/lib/hooks/useBookingChat.ts +import { useState, useCallback } from 'react'; +import { useAuth } from '@/lib/auth/AuthContext'; + +export interface BookingData { + step: 'department' | 'service' | 'agent' | 'schedule' | 'details' | 'complete'; + department?: string; + departmentName?: string; + service?: string; + serviceName?: string; + agent?: string; + agentName?: string; + date?: string; + time?: string; + notes?: string; + sessionId: string; + userId?: string; + completed: boolean; +} + +export interface BookingChatContext { + departmentId?: string; + departmentName?: string; + serviceId?: string; + serviceName?: string; + agentId?: string; + agentName?: string; + date?: string; + time?: string; +} + +export interface BookingChatHook { + bookingData: BookingData | null; + saveBookingData: (data: Partial) => void; + clearBookingData: () => void; + isReadyForBooking: () => boolean; + getNextQuestion: () => string | null; + processUserResponse: (response: string, context?: BookingChatContext) => Promise; + canProceedToForm: () => boolean; +} + +const BOOKING_STORAGE_KEY = 'govlink_booking_data'; + +const bookingQuestions = { + department: "I'd be happy to help you book an appointment! First, which department do you need assistance with? For example: Immigration, Business Registration, Education, Health, etc.", + service: "Great! Now, what specific service do you need from {department}? Please describe the service you're looking for.", + agent: "Perfect! What type of official would you like to meet with? For example: Senior Officer, Specialist, Manager, etc.", + schedule: "Excellent! When would you prefer to schedule your appointment? Please provide your preferred date and time.", + details: "Almost done! Do you have any specific notes or requirements for your appointment? If not, just say 'no' or 'none'.", + complete: "Thank you! I have collected all the necessary information. You can now open the booking form to complete your appointment request." +}; + +export const useBookingChat = (): BookingChatHook => { + const { user, isAuthenticated } = useAuth(); + const [bookingData, setBookingData] = useState(() => { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(BOOKING_STORAGE_KEY); + if (stored) { + try { + return JSON.parse(stored); + } catch { + return null; + } + } + } + return null; + }); + + const saveBookingData = useCallback(async (data: Partial) => { + if (!isAuthenticated || !user) { + console.warn('User must be authenticated to save booking data'); + return; + } + + const newData: BookingData = { + step: 'department', + sessionId: `booking_${Date.now()}_${Math.random().toString(36).substring(7)}`, + completed: false, + userId: user.id, + ...bookingData, + ...data, + }; + + setBookingData(newData); + + // Save to localStorage + if (typeof window !== 'undefined') { + localStorage.setItem(BOOKING_STORAGE_KEY, JSON.stringify(newData)); + } + + // Also save to MongoDB for persistence across devices + try { + const response = await fetch('/api/user/booking-data', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(newData), + }); + + if (!response.ok) { + console.warn('Failed to save booking data to server:', await response.text()); + } + } catch (error) { + console.warn('Error saving booking data to server:', error); + // Continue with localStorage only + } + }, [bookingData, isAuthenticated, user]); + + const clearBookingData = useCallback(async () => { + setBookingData(null); + + // Clear from localStorage + if (typeof window !== 'undefined') { + localStorage.removeItem(BOOKING_STORAGE_KEY); + } + + // Clear from MongoDB if user is authenticated + if (isAuthenticated && user && bookingData?.sessionId) { + try { + await fetch(`/api/user/booking-data?sessionId=${bookingData.sessionId}&userId=${user.id}`, { + method: 'DELETE', + }); + } catch (error) { + console.warn('Error clearing booking data from server:', error); + } + } + }, [isAuthenticated, user, bookingData]); + + const isReadyForBooking = useCallback(() => { + return !!(bookingData?.department && bookingData?.service); + }, [bookingData]); + + const canProceedToForm = useCallback(() => { + return !!( + bookingData?.department && + bookingData?.service && + bookingData?.agent && + bookingData?.completed && + isAuthenticated && + user + ); + }, [bookingData, isAuthenticated, user]); + + const getNextQuestion = useCallback(() => { + if (!bookingData) return bookingQuestions.department; + + switch (bookingData.step) { + case 'department': + return bookingQuestions.service.replace('{department}', bookingData.departmentName || 'the selected department'); + case 'service': + return bookingQuestions.agent; + case 'agent': + return bookingQuestions.schedule; + case 'schedule': + return bookingQuestions.details; + case 'details': + return bookingQuestions.complete; + default: + return null; + } + }, [bookingData]); + + const processUserResponse = useCallback(async (response: string, context?: BookingChatContext) => { + if (!isAuthenticated || !user) { + throw new Error('Authentication required to process booking'); + } + + if (!bookingData) { + // Start new booking process + saveBookingData({ + step: 'department', + department: context?.departmentId, + departmentName: context?.departmentName || response, + }); + return; + } + + switch (bookingData.step) { + case 'department': + saveBookingData({ + step: 'service', + service: context?.serviceId, + serviceName: context?.serviceName || response, + }); + break; + case 'service': + saveBookingData({ + step: 'agent', + agent: context?.agentId, + agentName: context?.agentName || response, + }); + break; + case 'agent': + saveBookingData({ + step: 'schedule', + date: context?.date, + time: context?.time, + }); + break; + case 'schedule': + saveBookingData({ + step: 'details', + notes: response, + }); + break; + case 'details': + saveBookingData({ + step: 'complete', + completed: true, + }); + break; + default: + break; + } + }, [bookingData, saveBookingData, isAuthenticated, user]); + + return { + bookingData, + saveBookingData, + clearBookingData, + isReadyForBooking, + getNextQuestion, + processUserResponse, + canProceedToForm, + }; +}; diff --git a/src/lib/hooks/useDashboard.ts b/src/lib/hooks/useDashboard.ts new file mode 100644 index 0000000..0670550 --- /dev/null +++ b/src/lib/hooks/useDashboard.ts @@ -0,0 +1,117 @@ +// src/lib/hooks/useDashboard.ts +import { useState, useEffect, useCallback } from 'react'; +import { useAuth } from '@/lib/auth/AuthContext'; + +export interface DashboardStats { + activeBookings: number; + applications: number; + messages: number; + completed: number; +} + +export interface DashboardData { + user: { + id: string; + fullName: string; + firstName: string; + lastName: string; + email: string; + nicNumber: string; + mobileNumber?: string; + accountStatus: string; + verificationStatus: string; + profileStatus: string; + isEmailVerified: boolean; + isProfileComplete: boolean; + profileCompletionPercentage: number; + }; + stats: DashboardStats; + activity: { + totalAppointments: number; + totalApplications: number; + recentActivity: { + thisMonth: { + appointments: number; + applications: number; + }; + }; + }; +} + +export interface DashboardState { + data: DashboardData | null; + loading: boolean; + error: string | null; +} + +export const useDashboard = () => { + const [state, setState] = useState({ + data: null, + loading: true, + error: null, + }); + + const { isAuthenticated, user } = useAuth(); + + const fetchDashboardData = useCallback(async () => { + if (!isAuthenticated) { + setState(prev => ({ + ...prev, + loading: false, + error: 'Not authenticated' + })); + return; + } + + try { + setState(prev => ({ ...prev, loading: true, error: null })); + + const response = await fetch('/api/user/dashboard', { + method: 'GET', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch dashboard data: ${response.statusText}`); + } + + const result = await response.json(); + + if (result.success) { + console.log('Dashboard API response:', result.data); + setState({ + data: result.data, + loading: false, + error: null, + }); + } else { + throw new Error(result.message || 'Failed to fetch dashboard data'); + } + } catch (error) { + console.error('Dashboard fetch error:', error); + setState({ + data: null, + loading: false, + error: error instanceof Error ? error.message : 'An error occurred', + }); + } + }, [isAuthenticated]); + + const refetchDashboard = useCallback(() => { + fetchDashboardData(); + }, [fetchDashboardData]); + + useEffect(() => { + if (isAuthenticated && user) { + fetchDashboardData(); + } + }, [isAuthenticated, user, fetchDashboardData]); + + return { + ...state, + refetch: refetchDashboard, + }; +}; diff --git a/src/lib/hooks/useDepartmentApi.ts b/src/lib/hooks/useDepartmentApi.ts index 983a11c..dff516e 100644 --- a/src/lib/hooks/useDepartmentApi.ts +++ b/src/lib/hooks/useDepartmentApi.ts @@ -188,13 +188,18 @@ export function useSubmissions(filters?: { const fetchData = useCallback(async () => { try { setState(prev => ({ ...prev, loading: true, error: null })); + console.log('Fetching submissions with filters:', filters); const response = await DepartmentApiService.getSubmissions(filters); + console.log('Submissions API response:', response); if (response.success && response.data) { + console.log('Submissions data:', response.data); setState({ data: response.data, loading: false, error: null }); } else { + console.error('Failed to fetch submissions:', response.message); setState({ data: null, loading: false, error: response.message }); } } catch (error) { + console.error('Fetch submissions error:', error); setState({ data: null, loading: false, error: (error as Error).message }); } }, [filters]); diff --git a/src/lib/i18n/resources/en/department.json b/src/lib/i18n/resources/en/department.json index 28215c5..1bde4f3 100644 --- a/src/lib/i18n/resources/en/department.json +++ b/src/lib/i18n/resources/en/department.json @@ -86,11 +86,13 @@ "month": "This Month", "quarter": "This Quarter" }, - "quickStats": "Department-wide Statistics", - "totalSubmissions": "Total Submissions", - "avgResolutionTime": "Avg. Resolution Time", - "approvalRate": "Approval Rate", - "citizenSatisfaction": "Citizen Satisfaction", + "quickStats": { + "title": "Department-wide Statistics", + "totalSubmissions": "Total Submissions", + "avgResolutionTime": "Avg. Resolution Time", + "approvalRate": "Approval Rate", + "citizenSatisfaction": "Citizen Satisfaction" + }, "refreshData": "Refresh Data", "lastUpdated": "Last Updated", "metrics": { diff --git a/src/lib/i18n/resources/en/home.json b/src/lib/i18n/resources/en/home.json index 8810952..c166b09 100644 --- a/src/lib/i18n/resources/en/home.json +++ b/src/lib/i18n/resources/en/home.json @@ -12,6 +12,7 @@ "notifications": "Notifications", "messages": "Messages", "login": "Login", + "signUp": "Sign Up", "loginOptions": "Login Options", "userLogin": "User Login", "agentLogin": "Agent Login", diff --git a/src/lib/i18n/resources/si/home.json b/src/lib/i18n/resources/si/home.json index a173128..d5f5780 100644 --- a/src/lib/i18n/resources/si/home.json +++ b/src/lib/i18n/resources/si/home.json @@ -12,6 +12,7 @@ "notifications": "දැනුම් දීම්", "messages": "පණිවිඩ", "login": "ප්‍රවේශය", + "signUp": "ලියාපදිංචි වන්න", "loginOptions": "ප්‍රවේශ විකල්ප", "userLogin": "පරිශීලක ප්‍රවේශය", "agentLogin": "නියෝජිත ප්‍රවේශය", diff --git a/src/lib/i18n/resources/ta/home.json b/src/lib/i18n/resources/ta/home.json index e00e3a0..4835f27 100644 --- a/src/lib/i18n/resources/ta/home.json +++ b/src/lib/i18n/resources/ta/home.json @@ -12,6 +12,7 @@ "notifications": "அறிவிப்புகள்", "messages": "செய்திகள்", "login": "உள்நுழைவு", + "signUp": "பதிவு செய்யவும்", "loginOptions": "உள்நுழைவு விருப்பங்கள்", "userLogin": "பயனர் உள்நுழைவு", "agentLogin": "முகவர் உள்நுழைவு", diff --git a/src/lib/services/departmentApiService.ts b/src/lib/services/departmentApiService.ts index cf89f36..33b51c3 100644 --- a/src/lib/services/departmentApiService.ts +++ b/src/lib/services/departmentApiService.ts @@ -81,8 +81,17 @@ export interface Submission { priority: string; serviceId: string; agentId?: string; + agentName?: string; + agentEmail?: string; + agentStatus?: string; submittedAt: string; updatedAt: string; + // Additional appointment-specific fields + appointmentDate?: string; + appointmentTime?: string; + bookingReference?: string; + contactPhone?: string; + citizenNIC?: string; } export interface DepartmentProfile { @@ -392,7 +401,14 @@ class DepartmentApiService { if (filters?.priority) searchParams.set('priority', filters.priority); if (filters?.search) searchParams.set('search', filters.search); if (filters?.limit) searchParams.set('limit', filters.limit.toString()); - if (filters?.offset) searchParams.set('offset', filters.offset.toString()); + + // Convert offset to page number (API expects page, not offset) + if (filters?.offset !== undefined && filters?.limit) { + const page = Math.floor(filters.offset / filters.limit) + 1; + searchParams.set('page', page.toString()); + } else if (filters?.offset === 0 || !filters?.offset) { + searchParams.set('page', '1'); + } const response = await fetch(`/api/department/submissions?${searchParams.toString()}`, { method: 'GET', diff --git a/src/lib/services/govlinkEmailService.ts b/src/lib/services/govlinkEmailService.ts index 5116ad6..e9beb8f 100644 --- a/src/lib/services/govlinkEmailService.ts +++ b/src/lib/services/govlinkEmailService.ts @@ -1,22 +1,29 @@ import nodemailer from 'nodemailer'; -// Create transporter using Gmail -const transporter = nodemailer.createTransport({ +// Check if email credentials are provided +const hasEmailCredentials = process.env.MAIL_ID && process.env.MAIL_PW; + +// Create transporter using Gmail only if credentials are available +const transporter = hasEmailCredentials ? nodemailer.createTransport({ service: 'gmail', auth: { user: process.env.MAIL_ID, pass: process.env.MAIL_PW, }, -}); - -// Verify transporter configuration -transporter.verify((error: Error | null) => { - if (error) { - console.error('Email service configuration error:', error); - } else { - console.log('GovLink email service is ready to send messages'); - } -}); +}) : null; + +// Verify transporter configuration only if it exists +if (transporter) { + transporter.verify((error: Error | null) => { + if (error) { + console.error('Email service configuration error:', error); + } else { + console.log('GovLink email service is ready to send messages'); + } + }); +} else { + console.warn('Email service disabled: MAIL_ID and MAIL_PW environment variables not provided'); +} export interface EmailOptions { to: string | string[]; @@ -35,6 +42,16 @@ export interface EmailOptions { export const sendEmail = async (options: EmailOptions): Promise<{ success: boolean; message: string; messageId?: string; error?: string }> => { try { + // Check if email service is configured + if (!transporter) { + console.warn('Email service not configured - SMTP credentials missing'); + return { + success: false, + message: 'Email service not configured', + error: 'SMTP credentials not provided' + }; + } + const mailOptions = { from: `"${process.env.GOV_SERVICE_NAME}" <${process.env.MAIL_ID}>`, to: Array.isArray(options.to) ? options.to.join(', ') : options.to,