Skip to content

Latest commit

 

History

History
3365 lines (2851 loc) · 102 KB

File metadata and controls

3365 lines (2851 loc) · 102 KB

PHASE 3: Backend API Development

3.1 Server Entry Point

server/server.js

// ====================================
// CivicStreak Backend Server
// ====================================
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
require('dotenv').config();

const app = express();

// ============ MIDDLEWARE ============

// Security headers
app.use(helmet());

// CORS configuration
app.use(cors({
    origin: process.env.CLIENT_URL || 'http://localhost:3000',
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    allowedHeaders: ['Content-Type', 'Authorization']
}));

// Rate limiting
const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);

// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Logging
if (process.env.NODE_ENV === 'development') {
    app.use(morgan('dev'));
}

// ============ ROUTES ============

// Health check
app.get('/api/health', (req, res) => {
    res.json({
        status: 'OK',
        message: 'CivicStreak API is running 🏙️',
        timestamp: new Date().toISOString(),
        version: '1.0.0'
    });
});

// Import route modules
const authRoutes = require('./src/routes/auth');
const taskRoutes = require('./src/routes/tasks');
const userRoutes = require('./src/routes/users');
const circleRoutes = require('./src/routes/circles');
const portfolioRoutes = require('./src/routes/portfolio');
const wardRoutes = require('./src/routes/ward');
const whatsappRoutes = require('./src/routes/whatsapp');
const issueRoutes = require('./src/routes/issues');

// Mount routes
app.use('/api/auth', authRoutes);
app.use('/api/tasks', taskRoutes);
app.use('/api/users', userRoutes);
app.use('/api/circles', circleRoutes);
app.use('/api/portfolio', portfolioRoutes);
app.use('/api/wards', wardRoutes);
app.use('/api/whatsapp', whatsappRoutes);
app.use('/api/issues', issueRoutes);

// ============ ERROR HANDLING ============

// 404 handler
app.use((req, res) => {
    res.status(404).json({
        success: false,
        message: `Route ${req.originalUrl} not found`
    });
});

// Global error handler
app.use((err, req, res, next) => {
    console.error('Error:', err.stack);
    res.status(err.statusCode || 500).json({
        success: false,
        message: err.message || 'Internal Server Error',
        ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    });
});

// ============ START SERVER ============

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
    console.log(`
    🏙️  CivicStreak Server Started
    📍 Port: ${PORT}
    🌍 Environment: ${process.env.NODE_ENV || 'development'}
    🕐 Time: ${new Date().toLocaleString()}
    `);
});

module.exports = app;

3.2 Configuration Files

server/src/config/database.js

// ====================================
// Supabase Database Configuration
// ====================================
const { createClient } = require('@supabase/supabase-js');

const supabaseUrl = process.env.SUPABASE_URL;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;

// Admin client (for server-side operations - bypasses RLS)
const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey, {
    auth: {
        autoRefreshToken: false,
        persistSession: false
    }
});

// Public client (respects RLS)
const supabase = createClient(supabaseUrl, supabaseAnonKey);

module.exports = { supabase, supabaseAdmin };

server/src/config/cloudinary.js

// ====================================
// Cloudinary Configuration
// ====================================
const cloudinary = require('cloudinary').v2;
const { CloudinaryStorage } = require('multer-storage-cloudinary');
const multer = require('multer');

cloudinary.config({
    cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
    api_key: process.env.CLOUDINARY_API_KEY,
    api_secret: process.env.CLOUDINARY_API_SECRET
});

const storage = new CloudinaryStorage({
    cloudinary: cloudinary,
    params: {
        folder: 'CivicStreak',
        allowed_formats: ['jpg', 'jpeg', 'png', 'gif', 'webp'],
        transformation: [{ width: 1200, height: 1200, crop: 'limit' }],
    },
});

const upload = multer({
    storage: storage,
    limits: { fileSize: 5 * 1024 * 1024 } // 5MB limit
});

module.exports = { cloudinary, upload };

server/src/config/twilio.js

// ====================================
// Twilio WhatsApp Configuration
// ====================================
const twilio = require('twilio');

const client = twilio(
    process.env.TWILIO_ACCOUNT_SID,
    process.env.TWILIO_AUTH_TOKEN
);

const WHATSAPP_FROM = process.env.TWILIO_WHATSAPP_NUMBER;

/**
 * Send a WhatsApp message
 * @param {string} to - Recipient phone number (e.g., 'whatsapp:+919876543210')
 * @param {string} body - Message body
 */
const sendWhatsAppMessage = async (to, body) => {
    try {
        const message = await client.messages.create({
            from: WHATSAPP_FROM,
            to: `whatsapp:${to}`,
            body: body
        });
        console.log(`WhatsApp message sent: ${message.sid}`);
        return message;
    } catch (error) {
        console.error('WhatsApp send error:', error);
        throw error;
    }
};

module.exports = { client, sendWhatsAppMessage };

3.3 Middleware

server/src/middleware/auth.js

// ====================================
// Authentication Middleware
// ====================================
const { supabaseAdmin } = require('../config/database');

/**
 * Verify Supabase JWT token
 * Extracts user from the Authorization header
 */
const authenticate = async (req, res, next) => {
    try {
        const authHeader = req.headers.authorization;

        if (!authHeader || !authHeader.startsWith('Bearer ')) {
            return res.status(401).json({
                success: false,
                message: 'Access denied. No token provided.'
            });
        }

        const token = authHeader.split(' ')[1];

        // Verify with Supabase
        const { data: { user }, error } = await supabaseAdmin.auth.getUser(token);

        if (error || !user) {
            return res.status(401).json({
                success: false,
                message: 'Invalid or expired token.'
            });
        }

        // Attach user to request
        req.user = user;
        req.userId = user.id;
        next();
    } catch (error) {
        console.error('Auth middleware error:', error);
        res.status(500).json({
            success: false,
            message: 'Authentication error.'
        });
    }
};

/**
 * Optional auth - doesn't fail if no token
 */
const optionalAuth = async (req, res, next) => {
    try {
        const authHeader = req.headers.authorization;
        if (authHeader && authHeader.startsWith('Bearer ')) {
            const token = authHeader.split(' ')[1];
            const { data: { user } } = await supabaseAdmin.auth.getUser(token);
            if (user) {
                req.user = user;
                req.userId = user.id;
            }
        }
        next();
    } catch (error) {
        next(); // Continue without auth
    }
};

module.exports = { authenticate, optionalAuth };

server/src/middleware/errorHandler.js

// ====================================
// Error Handler Middleware
// ====================================
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = true;
        Error.captureStackTrace(this, this.constructor);
    }
}

const errorHandler = (err, req, res, next) => {
    let { statusCode = 500, message } = err;

    if (process.env.NODE_ENV === 'production' && !err.isOperational) {
        message = 'Something went wrong!';
    }

    res.status(statusCode).json({
        success: false,
        message,
        ...(process.env.NODE_ENV === 'development' && {
            error: err,
            stack: err.stack
        })
    });
};

module.exports = { AppError, errorHandler };

3.4 Routes & Controllers

server/src/routes/auth.js

// ====================================
// Auth Routes
// ====================================
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { authenticate } = require('../middleware/auth');

// POST /api/auth/register
router.post('/register', authController.register);

// POST /api/auth/login
router.post('/login', authController.login);

// POST /api/auth/login-phone
router.post('/login-phone', authController.loginWithPhone);

// POST /api/auth/verify-otp
router.post('/verify-otp', authController.verifyOTP);

// GET /api/auth/me
router.get('/me', authenticate, authController.getMe);

// POST /api/auth/logout
router.post('/logout', authenticate, authController.logout);

module.exports = router;

server/src/controllers/authController.js

// ====================================
// Auth Controller
// ====================================
const { supabase, supabaseAdmin } = require('../config/database');

const authController = {
    /**
     * Register a new user with email/password
     */
    register: async (req, res) => {
        try {
            const { email, password, full_name, phone, ward_id } = req.body;

            // Validate input
            if (!email || !password || !full_name) {
                return res.status(400).json({
                    success: false,
                    message: 'Email, password, and full name are required.'
                });
            }

            // Register with Supabase Auth
            const { data, error } = await supabase.auth.signUp({
                email,
                password,
                options: {
                    data: {
                        full_name,
                        phone
                    }
                }
            });

            if (error) {
                return res.status(400).json({
                    success: false,
                    message: error.message
                });
            }

            // Update profile with additional info
            if (data.user) {
                await supabaseAdmin
                    .from('profiles')
                    .update({
                        full_name,
                        phone,
                        ward_id,
                        interests: req.body.interests || [],
                        college: req.body.college || null,
                        age: req.body.age || null
                    })
                    .eq('id', data.user.id);
            }

            res.status(201).json({
                success: true,
                message: 'Registration successful! Welcome to CivicStreak 🏙️',
                data: {
                    user: data.user,
                    session: data.session
                }
            });
        } catch (error) {
            console.error('Register error:', error);
            res.status(500).json({
                success: false,
                message: 'Registration failed. Please try again.'
            });
        }
    },

    /**
     * Login with email/password
     */
    login: async (req, res) => {
        try {
            const { email, password } = req.body;

            const { data, error } = await supabase.auth.signInWithPassword({
                email,
                password
            });

            if (error) {
                return res.status(401).json({
                    success: false,
                    message: 'Invalid credentials.'
                });
            }

            // Fetch full profile
            const { data: profile } = await supabaseAdmin
                .from('profiles')
                .select('*, wards(ward_name, ward_number)')
                .eq('id', data.user.id)
                .single();

            res.json({
                success: true,
                message: 'Login successful!',
                data: {
                    user: profile,
                    session: data.session
                }
            });
        } catch (error) {
            console.error('Login error:', error);
            res.status(500).json({
                success: false,
                message: 'Login failed.'
            });
        }
    },

    /**
     * Login with phone (send OTP)
     */
    loginWithPhone: async (req, res) => {
        try {
            const { phone } = req.body;

            const { data, error } = await supabase.auth.signInWithOtp({
                phone
            });

            if (error) {
                return res.status(400).json({
                    success: false,
                    message: error.message
                });
            }

            res.json({
                success: true,
                message: 'OTP sent to your phone!'
            });
        } catch (error) {
            console.error('Phone login error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to send OTP.'
            });
        }
    },

    /**
     * Verify OTP
     */
    verifyOTP: async (req, res) => {
        try {
            const { phone, token } = req.body;

            const { data, error } = await supabase.auth.verifyOtp({
                phone,
                token,
                type: 'sms'
            });

            if (error) {
                return res.status(400).json({
                    success: false,
                    message: 'Invalid OTP.'
                });
            }

            res.json({
                success: true,
                message: 'Phone verified!',
                data: {
                    user: data.user,
                    session: data.session
                }
            });
        } catch (error) {
            console.error('OTP verification error:', error);
            res.status(500).json({
                success: false,
                message: 'Verification failed.'
            });
        }
    },

    /**
     * Get current user profile
     */
    getMe: async (req, res) => {
        try {
            const { data: profile, error } = await supabaseAdmin
                .from('profiles')
                .select(`
                    *,
                    wards(ward_name, ward_number, city),
                    circle_members(
                        circle_id,
                        role,
                        circles(name, adopted_issue_id)
                    )
                `)
                .eq('id', req.userId)
                .single();

            if (error || !profile) {
                return res.status(404).json({
                    success: false,
                    message: 'Profile not found.'
                });
            }

            // Get achievements
            const { data: achievements } = await supabaseAdmin
                .from('user_achievements')
                .select('*, achievements(*)')
                .eq('user_id', req.userId);

            res.json({
                success: true,
                data: {
                    ...profile,
                    achievements: achievements || []
                }
            });
        } catch (error) {
            console.error('GetMe error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch profile.'
            });
        }
    },

    /**
     * Logout
     */
    logout: async (req, res) => {
        try {
            await supabase.auth.signOut();
            res.json({
                success: true,
                message: 'Logged out successfully.'
            });
        } catch (error) {
            res.status(500).json({
                success: false,
                message: 'Logout failed.'
            });
        }
    }
};

module.exports = authController;

server/src/routes/tasks.js

// ====================================
// Task Routes
// ====================================
const express = require('express');
const router = express.Router();
const taskController = require('../controllers/taskController');
const { authenticate } = require('../middleware/auth');
const { upload } = require('../config/cloudinary');

// GET /api/tasks - Get available tasks for user
router.get('/', authenticate, taskController.getAvailableTasks);

// GET /api/tasks/my - Get user's assigned/completed tasks
router.get('/my', authenticate, taskController.getMyTasks);

// GET /api/tasks/today - Get today's recommended task
router.get('/today', authenticate, taskController.getTodaysTask);

// GET /api/tasks/:id - Get single task details
router.get('/:id', authenticate, taskController.getTaskById);

// POST /api/tasks/:id/accept - Accept a task
router.post('/:id/accept', authenticate, taskController.acceptTask);

// POST /api/tasks/:id/submit - Submit task completion
router.post('/:id/submit', authenticate, upload.single('proof'), taskController.submitTask);

// GET /api/tasks/categories/list - Get task categories
router.get('/categories/list', authenticate, taskController.getCategories);

module.exports = router;

server/src/controllers/taskController.js

// ====================================
// Task Controller
// ====================================
const { supabaseAdmin } = require('../config/database');
const { updateStreakAndXP } = require('../services/streakService');

const taskController = {
    /**
     * Get available tasks for the authenticated user
     * Filters by ward, skill level, interests
     */
    getAvailableTasks: async (req, res) => {
        try {
            const { category, difficulty, interest, time } = req.query;

            // Get user profile for smart assignment
            const { data: profile } = await supabaseAdmin
                .from('profiles')
                .select('ward_id, skill_level, interests')
                .eq('id', req.userId)
                .single();

            let query = supabaseAdmin
                .from('micro_tasks')
                .select('*')
                .eq('is_active', true)
                .or(`ward_id.is.null,ward_id.eq.${profile?.ward_id}`);

            // Apply filters
            if (category) {
                query = query.eq('category', category);
            }
            if (difficulty) {
                query = query.eq('difficulty', difficulty);
            }
            if (time) {
                query = query.lte('estimated_minutes', parseInt(time));
            }

            // Order by relevance
            query = query.order('created_at', { ascending: false });

            const { data: tasks, error } = await query.limit(20);

            if (error) throw error;

            // Get user's already assigned/completed tasks to exclude
            const { data: userTasks } = await supabaseAdmin
                .from('user_tasks')
                .select('task_id')
                .eq('user_id', req.userId)
                .in('status', ['assigned', 'in_progress', 'submitted', 'verified']);

            const completedTaskIds = new Set(
                (userTasks || []).map(ut => ut.task_id)
            );

            // Filter out already assigned tasks
            const availableTasks = tasks.filter(
                task => !completedTaskIds.has(task.id)
            );

            // Smart sorting: prioritize by user interests
            if (profile?.interests?.length > 0) {
                availableTasks.sort((a, b) => {
                    const aMatch = a.interest_tags?.some(t =>
                        profile.interests.includes(t)
                    ) ? 1 : 0;
                    const bMatch = b.interest_tags?.some(t =>
                        profile.interests.includes(t)
                    ) ? 1 : 0;
                    return bMatch - aMatch;
                });
            }

            res.json({
                success: true,
                data: availableTasks,
                count: availableTasks.length
            });
        } catch (error) {
            console.error('Get tasks error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch tasks.'
            });
        }
    },

    /**
     * Get user's own tasks (assigned, in-progress, completed)
     */
    getMyTasks: async (req, res) => {
        try {
            const { status } = req.query;

            let query = supabaseAdmin
                .from('user_tasks')
                .select(`
                    *,
                    micro_tasks(*)
                `)
                .eq('user_id', req.userId)
                .order('assigned_at', { ascending: false });

            if (status) {
                query = query.eq('status', status);
            }

            const { data: tasks, error } = await query;

            if (error) throw error;

            res.json({
                success: true,
                data: tasks
            });
        } catch (error) {
            console.error('Get my tasks error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch your tasks.'
            });
        }
    },

    /**
     * Get today's recommended task (Civic Bite of the day)
     */
    getTodaysTask: async (req, res) => {
        try {
            const { data: profile } = await supabaseAdmin
                .from('profiles')
                .select('ward_id, skill_level, interests')
                .eq('id', req.userId)
                .single();

            // Get a task the user hasn't done yet
            const { data: completedIds } = await supabaseAdmin
                .from('user_tasks')
                .select('task_id')
                .eq('user_id', req.userId);

            const excludeIds = (completedIds || []).map(t => t.task_id);

            let query = supabaseAdmin
                .from('micro_tasks')
                .select('*')
                .eq('is_active', true)
                .lte('estimated_minutes', 15)
                .limit(1);

            if (excludeIds.length > 0) {
                query = query.not('id', 'in', `(${excludeIds.join(',')})`);
            }

            if (profile?.ward_id) {
                query = query.or(`ward_id.is.null,ward_id.eq.${profile.ward_id}`);
            }

            const { data: tasks } = await query;

            res.json({
                success: true,
                data: tasks?.[0] || null,
                message: tasks?.[0]
                    ? "Here's your Civic Bite for today! 🎯"
                    : "You've completed all available tasks! Check back tomorrow 🌟"
            });
        } catch (error) {
            console.error('Get today task error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch today\'s task.'
            });
        }
    },

    /**
     * Get single task by ID
     */
    getTaskById: async (req, res) => {
        try {
            const { id } = req.params;

            const { data: task, error } = await supabaseAdmin
                .from('micro_tasks')
                .select('*')
                .eq('id', id)
                .single();

            if (error || !task) {
                return res.status(404).json({
                    success: false,
                    message: 'Task not found.'
                });
            }

            // Check if user has already accepted this task
            const { data: userTask } = await supabaseAdmin
                .from('user_tasks')
                .select('*')
                .eq('user_id', req.userId)
                .eq('task_id', id)
                .order('assigned_at', { ascending: false })
                .limit(1)
                .single();

            res.json({
                success: true,
                data: {
                    ...task,
                    user_status: userTask || null
                }
            });
        } catch (error) {
            console.error('Get task error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch task.'
            });
        }
    },

    /**
     * Accept/Start a task
     */
    acceptTask: async (req, res) => {
        try {
            const { id } = req.params;

            // Verify task exists and is active
            const { data: task } = await supabaseAdmin
                .from('micro_tasks')
                .select('*')
                .eq('id', id)
                .eq('is_active', true)
                .single();

            if (!task) {
                return res.status(404).json({
                    success: false,
                    message: 'Task not found or no longer available.'
                });
            }

            // Check if already accepted
            const { data: existing } = await supabaseAdmin
                .from('user_tasks')
                .select('id, status')
                .eq('user_id', req.userId)
                .eq('task_id', id)
                .in('status', ['assigned', 'in_progress'])
                .single();

            if (existing) {
                return res.status(400).json({
                    success: false,
                    message: 'You have already accepted this task.'
                });
            }

            // Create assignment
            const { data: assignment, error } = await supabaseAdmin
                .from('user_tasks')
                .insert({
                    user_id: req.userId,
                    task_id: id,
                    status: 'in_progress',
                    started_at: new Date().toISOString(),
                    expires_at: new Date(
                        Date.now() + 7 * 24 * 60 * 60 * 1000
                    ).toISOString()
                })
                .select()
                .single();

            if (error) throw error;

            res.status(201).json({
                success: true,
                message: `Task accepted! You have 7 days to complete it. 💪`,
                data: assignment
            });
        } catch (error) {
            console.error('Accept task error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to accept task.'
            });
        }
    },

    /**
     * Submit task completion with proof
     */
    submitTask: async (req, res) => {
        try {
            const { id } = req.params; // task_id
            const { proof_text, quiz_score } = req.body;
            const proof_url = req.file ? req.file.path : null;

            // Find the user's assignment
            const { data: userTask } = await supabaseAdmin
                .from('user_tasks')
                .select('*, micro_tasks(*)')
                .eq('user_id', req.userId)
                .eq('task_id', id)
                .in('status', ['assigned', 'in_progress'])
                .order('assigned_at', { ascending: false })
                .limit(1)
                .single();

            if (!userTask) {
                return res.status(404).json({
                    success: false,
                    message: 'No active assignment found for this task.'
                });
            }

            // Validate proof based on task requirement
            const task = userTask.micro_tasks;
            if (task.required_proof === 'photo' && !proof_url) {
                return res.status(400).json({
                    success: false,
                    message: 'This task requires a photo as proof.'
                });
            }
            if (task.required_proof === 'text' && !proof_text) {
                return res.status(400).json({
                    success: false,
                    message: 'This task requires a text submission.'
                });
            }

            // Calculate XP earned
            const xpEarned = task.xp_reward || 20;

            // Update the assignment
            const { data: updated, error } = await supabaseAdmin
                .from('user_tasks')
                .update({
                    status: 'submitted',
                    proof_url,
                    proof_text,
                    quiz_score,
                    submitted_at: new Date().toISOString(),
                    xp_earned: xpEarned
                })
                .eq('id', userTask.id)
                .select()
                .single();

            if (error) throw error;

            // Update user stats (XP, streak, task count)
            await updateStreakAndXP(req.userId, xpEarned);

            // Update task completion count
            await supabaseAdmin
                .from('micro_tasks')
                .update({
                    times_completed: task.times_completed + 1
                })
                .eq('id', id);

            // Check for new achievements
            const newAchievements = await checkAchievements(req.userId);

            res.json({
                success: true,
                message: `Task completed! +${xpEarned} XP earned 🎉`,
                data: {
                    submission: updated,
                    xp_earned: xpEarned,
                    new_achievements: newAchievements
                }
            });
        } catch (error) {
            console.error('Submit task error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to submit task.'
            });
        }
    },

    /**
     * Get task categories with counts
     */
    getCategories: async (req, res) => {
        try {
            const categories = [
                { key: 'DOCUMENT', label: '📸 Document', description: 'Photograph civic issues, infrastructure, progress' },
                { key: 'LEARN', label: '📝 Learn', description: 'Micro-lessons on governance, RTI, budgets' },
                { key: 'VOICE', label: '🌊 Voice', description: 'Sign petitions, submit feedback, attend hearings' },
                { key: 'CONNECT', label: '🤝 Connect', description: 'Talk to a neighbor, interview a local vendor' },
                { key: 'TRACK', label: '📊 Track', description: 'Follow up on a previously reported issue' },
                { key: 'MENTOR', label: '🎓 Mentor', description: 'Help a junior volunteer complete their first task' }
            ];

            // Get counts for each category
            for (let cat of categories) {
                const { count } = await supabaseAdmin
                    .from('micro_tasks')
                    .select('id', { count: 'exact', head: true })
                    .eq('category', cat.key)
                    .eq('is_active', true);
                cat.count = count || 0;
            }

            res.json({
                success: true,
                data: categories
            });
        } catch (error) {
            console.error('Get categories error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch categories.'
            });
        }
    }
};

/**
 * Check and award achievements
 */
async function checkAchievements(userId) {
    const newAchievements = [];

    try {
        // Get user profile
        const { data: profile } = await supabaseAdmin
            .from('profiles')
            .select('*')
            .eq('id', userId)
            .single();

        // Get all achievements
        const { data: allAchievements } = await supabaseAdmin
            .from('achievements')
            .select('*');

        // Get user's existing achievements
        const { data: userAchievements } = await supabaseAdmin
            .from('user_achievements')
            .select('achievement_id')
            .eq('user_id', userId);

        const earnedIds = new Set(
            (userAchievements || []).map(ua => ua.achievement_id)
        );

        for (const achievement of allAchievements || []) {
            if (earnedIds.has(achievement.id)) continue;

            let earned = false;

            switch (achievement.requirement_type) {
                case 'tasks_completed':
                    earned = profile.tasks_completed >= achievement.requirement_value;
                    break;
                case 'streak_days':
                    earned = profile.current_streak >= achievement.requirement_value;
                    break;
                case 'xp_earned':
                    earned = profile.xp_points >= achievement.requirement_value;
                    break;
                case 'issues_reported':
                    earned = profile.issues_reported >= achievement.requirement_value;
                    break;
                case 'issues_resolved':
                    earned = profile.issues_resolved >= achievement.requirement_value;
                    break;
                case 'rtis_filed':
                    earned = profile.rtis_filed >= achievement.requirement_value;
                    break;
                case 'meetings_attended':
                    earned = profile.meetings_attended >= achievement.requirement_value;
                    break;
                case 'people_mentored':
                    earned = profile.people_mentored >= achievement.requirement_value;
                    break;
            }

            if (earned) {
                await supabaseAdmin
                    .from('user_achievements')
                    .insert({
                        user_id: userId,
                        achievement_id: achievement.id
                    });

                // Award bonus XP
                if (achievement.xp_bonus > 0) {
                    await supabaseAdmin
                        .from('profiles')
                        .update({
                            xp_points: profile.xp_points + achievement.xp_bonus
                        })
                        .eq('id', userId);
                }

                newAchievements.push(achievement);
            }
        }
    } catch (error) {
        console.error('Check achievements error:', error);
    }

    return newAchievements;
}

module.exports = taskController;

3.5 Services

server/src/services/streakService.js

// ====================================
// Streak & XP Service
// ====================================
const { supabaseAdmin } = require('../config/database');

/**
 * Update user's streak and XP after task completion
 */
const updateStreakAndXP = async (userId, xpEarned) => {
    try {
        // Get current profile
        const { data: profile } = await supabaseAdmin
            .from('profiles')
            .select('*')
            .eq('id', userId)
            .single();

        if (!profile) throw new Error('Profile not found');

        const today = new Date().toISOString().split('T')[0];
        const lastActivity = profile.last_activity_date;

        let newStreak = profile.current_streak;
        let streakBonus = 0;

        if (lastActivity === today) {
            // Already active today — no streak change, just XP
        } else if (lastActivity === getYesterday()) {
            // Consecutive day — increment streak
            newStreak += 1;
            streakBonus = 5; // Daily streak bonus
        } else {
            // Streak broken — reset to 1
            newStreak = 1;
        }

        const longestStreak = Math.max(newStreak, profile.longest_streak);
        const totalXP = xpEarned + streakBonus;

        // Determine level based on streak
        const level = calculateLevel(newStreak);

        // Update profile
        await supabaseAdmin
            .from('profiles')
            .update({
                xp_points: profile.xp_points + totalXP,
                current_streak: newStreak,
                longest_streak: longestStreak,
                last_activity_date: today,
                tasks_completed: profile.tasks_completed + 1,
                level: level,
                updated_at: new Date().toISOString()
            })
            .eq('id', userId);

        // Record in streak history
        await supabaseAdmin
            .from('streak_history')
            .upsert({
                user_id: userId,
                activity_date: today,
                tasks_completed: 1,
                xp_earned: totalXP
            }, {
                onConflict: 'user_id,activity_date'
            });

        return {
            newStreak,
            totalXP,
            streakBonus,
            level
        };
    } catch (error) {
        console.error('Update streak error:', error);
        throw error;
    }
};

/**
 * Calculate user level based on streak days
 */
function calculateLevel(streakDays) {
    if (streakDays >= 365) return 'CivicStreak Fellow';
    if (streakDays >= 180) return 'Civic Champion';
    if (streakDays >= 90) return 'Ward Warrior';
    if (streakDays >= 30) return 'Active Citizen';
    if (streakDays >= 7) return 'Curious Citizen';
    return 'Newcomer';
}

/**
 * Get yesterday's date in YYYY-MM-DD format
 */
function getYesterday() {
    const d = new Date();
    d.setDate(d.getDate() - 1);
    return d.toISOString().split('T')[0];
}

/**
 * Reset streaks for users who missed a day (run via cron)
 */
const resetBrokenStreaks = async () => {
    try {
        const yesterday = getYesterday();

        const { data: staleUsers } = await supabaseAdmin
            .from('profiles')
            .select('id')
            .lt('last_activity_date', yesterday)
            .gt('current_streak', 0);

        if (staleUsers && staleUsers.length > 0) {
            for (const user of staleUsers) {
                await supabaseAdmin
                    .from('profiles')
                    .update({
                        current_streak: 0,
                        updated_at: new Date().toISOString()
                    })
                    .eq('id', user.id);
            }
            console.log(`Reset streaks for ${staleUsers.length} users`);
        }
    } catch (error) {
        console.error('Reset streaks error:', error);
    }
};

module.exports = { updateStreakAndXP, resetBrokenStreaks, calculateLevel };

server/src/services/aiService.js

// ====================================
// AI Service — Google Gemini Integration
// ====================================
const { GoogleGenerativeAI } = require('@google/generative-ai');

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);

const aiService = {
    /**
     * Generate personalized task suggestions based on user profile
     */
    suggestTasks: async (userProfile, wardInfo) => {
        try {
            const model = genAI.getGenerativeModel({ model: 'gemini-pro' });

            const prompt = `
You are CivicStreak AI, a civic engagement assistant for Indian youth.

User Profile:
- Name: ${userProfile.full_name}
- Ward: ${wardInfo?.ward_name || 'Unknown'} (${wardInfo?.ward_number || ''})
- Interests: ${userProfile.interests?.join(', ') || 'General'}
- Skill Level: ${userProfile.skill_level}
- Tasks Completed: ${userProfile.tasks_completed}
- Current Streak: ${userProfile.current_streak} days

Suggest 3 personalized micro-tasks (5-15 minutes each) that this user can do TODAY in their ward. 
Each task should be specific, actionable, and location-aware.

Format each task as JSON:
{
    "title": "Short engaging title with emoji",
    "description": "One line description",
    "category": "DOCUMENT|LEARN|VOICE|CONNECT|TRACK|MENTOR",
    "estimated_minutes": 5-15,
    "xp_reward": 10-50,
    "instructions": "Step by step what to do"
}

Return only a JSON array of 3 tasks. No other text.
            `;

            const result = await model.generateContent(prompt);
            const response = await result.response;
            const text = response.text();

            // Parse JSON from response
            const jsonMatch = text.match(/\[[\s\S]*\]/);
            if (jsonMatch) {
                return JSON.parse(jsonMatch[0]);
            }

            return [];
        } catch (error) {
            console.error('AI suggest tasks error:', error);
            return [];
        }
    },

    /**
     * Generate a summary for user's civic portfolio
     */
    generatePortfolioSummary: async (userStats) => {
        try {
            const model = genAI.getGenerativeModel({ model: 'gemini-pro' });

            const prompt = `
Generate a brief, impressive 3-line civic portfolio summary for a student:
- Tasks completed: ${userStats.tasks_completed}
- Issues reported: ${userStats.issues_reported}
- Issues resolved: ${userStats.issues_resolved}
- RTIs filed: ${userStats.rtis_filed}
- Meetings attended: ${userStats.meetings_attended}
- People mentored: ${userStats.people_mentored}
- Streak: ${userStats.current_streak} days
- Level: ${userStats.level}

Write it in first person, suitable for a college application or LinkedIn. 
Keep it under 100 words.
            `;

            const result = await model.generateContent(prompt);
            const response = await result.response;
            return response.text().trim();
        } catch (error) {
            console.error('AI portfolio summary error:', error);
            return null;
        }
    },

    /**
     * Analyze civic issue and suggest action plan
     */
    analyzeIssue: async (issueDescription, category) => {
        try {
            const model = genAI.getGenerativeModel({ model: 'gemini-pro' });

            const prompt = `
As a civic governance expert in India, analyze this civic issue and suggest an action plan:

Issue: ${issueDescription}
Category: ${category}

Provide:
1. Which government department is responsible
2. What RTI can be filed (one-liner)
3. Who to escalate to if not resolved in 30 days
4. A template complaint letter (2-3 lines)

Keep response under 200 words. Be specific to Indian municipal governance (BMC/municipal corporation context).
            `;

            const result = await model.generateContent(prompt);
            const response = await result.response;
            return response.text().trim();
        } catch (error) {
            console.error('AI analyze issue error:', error);
            return null;
        }
    },

    /**
     * Generate quiz questions about a ward
     */
    generateWardQuiz: async (wardInfo) => {
        try {
            const model = genAI.getGenerativeModel({ model: 'gemini-pro' });

            const prompt = `
Generate 5 multiple-choice quiz questions about civic governance for Indian youth, 
specifically for ward "${wardInfo.ward_name}" in ${wardInfo.city || 'Mumbai'}.

Topics: local governance structure, BMC functions, RTI basics, civic rights, ward committees.

Format as JSON array:
[
    {
        "question": "Question text",
        "options": ["A", "B", "C", "D"],
        "correct": 0,
        "explanation": "Brief explanation"
    }
]

Return only the JSON array.
            `;

            const result = await model.generateContent(prompt);
            const response = await result.response;
            const text = response.text();

            const jsonMatch = text.match(/\[[\s\S]*\]/);
            if (jsonMatch) {
                return JSON.parse(jsonMatch[0]);
            }
            return [];
        } catch (error) {
            console.error('AI quiz generation error:', error);
            return [];
        }
    }
};

module.exports = aiService;

server/src/services/whatsappService.js

// ====================================
// WhatsApp Bot Service
// ====================================
const { sendWhatsAppMessage } = require('../config/twilio');
const { supabaseAdmin } = require('../config/database');
const aiService = require('./aiService');

const whatsappService = {
    /**
     * Handle incoming WhatsApp message
     */
    handleIncoming: async (from, body) => {
        const phoneNumber = from.replace('whatsapp:', '');
        const message = body.trim().toLowerCase();

        // Find user by phone
        const { data: user } = await supabaseAdmin
            .from('profiles')
            .select('*, wards(ward_name, ward_number)')
            .eq('phone', phoneNumber)
            .single();

        // New user — not registered
        if (!user) {
            return whatsappService.sendWelcomeMessage(phoneNumber);
        }

        // Route based on message content
        switch (message) {
            case 'hi':
            case 'hello':
            case 'hey':
            case 'start':
                return whatsappService.sendDailyBrief(user);

            case '1':
            case 'task':
                return whatsappService.sendTodaysTask(user);

            case '2':
            case 'portfolio':
                return whatsappService.sendPortfolioSummary(user);

            case '3':
            case 'circle':
                return whatsappService.sendCircleUpdate(user);

            case '4':
            case 'learn':
                return whatsappService.sendLearningContent(user);

            case '5':
            case 'report':
                return whatsappService.startIssueReport(user);

            case 'streak':
                return whatsappService.sendStreakInfo(user);

            case 'help':
                return whatsappService.sendHelpMessage(user);

            default:
                // Check if it's a task submission (photo will come separately)
                if (message.startsWith('done') || message.startsWith('completed')) {
                    return whatsappService.handleTaskSubmission(user, body);
                }
                return whatsappService.sendDefaultResponse(user);
        }
    },

    /**
     * Welcome message for new users
     */
    sendWelcomeMessage: async (phoneNumber) => {
        const message = `🏙️ *Welcome to CivicStreak!*

CivicStreak turns democracy from a once-in-5-years vote into a daily 5-minute habit.

To get started, please register on our web app:
🌐 https://CivicStreak.vercel.app/register

Once registered with this phone number, you'll get daily Civic Bites right here on WhatsApp!

_Your City, Your Commitment_ 🇮🇳`;

        await sendWhatsAppMessage(phoneNumber, message);
    },

    /**
     * Daily brief — main menu
     */
    sendDailyBrief: async (user) => {
        const ward = user.wards;

        const message = `🙏 *Welcome to CivicStreak!*

📍 Your ward: ${ward?.ward_name || 'Not set'} (Ward ${ward?.ward_number || '—'})

*Today's Civic Bite* 🍽️:
📸 "There's a reported broken footpath near your ward. If you pass by today, take a quick photo and send it here. (5 min task)"

🔥 Your streak: *${user.current_streak} days*
⭐ Your XP: *${user.xp_points}*
🏅 Level: *${user.level}*

*Reply with:*
1️⃣ — Do today's task
2️⃣ — See my portfolio
3️⃣ — Circle check-in
4️⃣ — Learn something new
5️⃣ — Report a new issue

Type *help* for more options.`;

        await sendWhatsAppMessage(user.phone, message);
    },

    /**
     * Send today's task details
     */
    sendTodaysTask: async (user) => {
        // Get a task for the user
        const { data: tasks } = await supabaseAdmin
            .from('micro_tasks')
            .select('*')
            .eq('is_active', true)
            .or(`ward_id.is.null,ward_id.eq.${user.ward_id}`)
            .limit(1);

        const task = tasks?.[0];

        if (!task) {
            await sendWhatsAppMessage(user.phone,
                "🌟 Amazing! You've completed all available tasks. Check back tomorrow for new Civic Bites!");
            return;
        }

        const message = `🎯 *Today's Task:*

*${task.title}*

📝 ${task.description}

⏱️ Time: ~${task.estimated_minutes} minutes
⭐ Reward: ${task.xp_reward} XP
📋 Category: ${task.category}

*Instructions:*
${task.instructions || 'Follow the task description above.'}

*To submit:*
${task.required_proof === 'photo'
                ? '📸 Send a photo as proof'
                : '✍️ Reply with "done" and your response'}

_Reply *skip* to get a different task._`;

        await sendWhatsAppMessage(user.phone, message);

        // Auto-assign the task
        await supabaseAdmin
            .from('user_tasks')
            .insert({
                user_id: user.id,
                task_id: task.id,
                status: 'in_progress',
                started_at: new Date().toISOString()
            });
    },

    /**
     * Send portfolio summary
     */
    sendPortfolioSummary: async (user) => {
        const message = `📋 *Your Civic Portfolio*
━━━━━━━━━━━━━━━━━━

👤 *${user.full_name}*
📍 ${user.wards?.ward_name || 'Ward not set'}

🔥 Streak: ${user.current_streak} days
⭐ XP: ${user.xp_points}
🏅 Level: ${user.level}

📊 *Impact Stats:*
${user.tasks_completed} tasks completed
${user.issues_reported} issues documented
${user.issues_resolved} issues resolved
${user.rtis_filed} RTIs filed
${user.meetings_attended} ward meetings attended
${user.people_mentored} juniors mentored

🌐 View full portfolio:
https://CivicStreak.vercel.app/portfolio/${user.id}

_Share this with colleges & employers!_ 🎓`;

        await sendWhatsAppMessage(user.phone, message);
    },

    /**
     * Send circle update
     */
    sendCircleUpdate: async (user) => {
        const { data: membership } = await supabaseAdmin
            .from('circle_members')
            .select('*, circles(*, wards(ward_name))')
            .eq('user_id', user.id)
            .eq('is_active', true)
            .single();

        if (!membership) {
            await sendWhatsAppMessage(user.phone,
                `🤝 You haven't joined a Circle yet!\n\nCircles are teams of 5-8 youth from your ward who adopt a civic issue together.\n\n🌐 Join one at: https://CivicStreak.vercel.app/circles`
            );
            return;
        }

        const circle = membership.circles;
        const message = `🤝 *Circle Update: ${circle.name}*

📍 Ward: ${circle.wards?.ward_name}
👥 Members: ${circle.member_count}
🎯 Adopted Issue: ${circle.adopted_issue_id ? 'Active' : 'None yet'}
⭐ Circle XP: ${circle.total_xp}

Your Role: *${membership.role}*

Next check-in: ${circle.next_checkin
                ? new Date(circle.next_checkin).toLocaleDateString()
                : 'Not scheduled'}

🌐 Circle dashboard:
https://CivicStreak.vercel.app/circles/${circle.id}`;

        await sendWhatsAppMessage(user.phone, message);
    },

    /**
     * Send learning content
     */
    sendLearningContent: async (user) => {
        const lessons = [
            {
                title: "📜 RTI 101: Your Right to Know",
                content: `*What is RTI?*\nThe Right to Information Act (2005) allows any Indian citizen to request information from government bodies.\n\n*Key Facts:*\n• Fee: ₹10 per application\n• Response time: 30 days\n• You can ask about budgets, spending, decisions\n• File online at: rtionline.gov.in\n\n_Tomorrow: How to write an RTI application_`
            },
            {
                title: "🏛️ How Your Ward Works",
                content: `*Ward Committee Structure:*\n• Your ward is the smallest unit of city governance\n• Ward councilor is elected every 5 years\n• Ward committee meets monthly (open to public!)\n• Budget allocated per ward for roads, water, sanitation\n\n_Attend your next ward meeting to earn 150 XP!_`
            },
            {
                title: "📊 Reading the BMC Budget",
                content: `*Understanding Ward Budgets:*\n• Every ward gets a development fund\n• Budget boards must be displayed at ward offices\n• Key categories: Roads, Water, Sanitation, Electricity\n• You can RTI the spending details!\n\n_Task: Photograph your ward's budget board for 25 XP_`
            }
        ];

        // Pick one based on day
        const dayIndex = new Date().getDate() % lessons.length;
        const lesson = lessons[dayIndex];

        const message = `📚 *Today's Civic Lesson:*\n\n${lesson.title}\n\n${lesson.content}`;

        await sendWhatsAppMessage(user.phone, message);
    },

    /**
     * Handle task submission via WhatsApp
     */
    handleTaskSubmission: async (user, messageBody) => {
        // Find the user's most recent in-progress task
        const { data: activeTask } = await supabaseAdmin
            .from('user_tasks')
            .select('*, micro_tasks(*)')
            .eq('user_id', user.id)
            .eq('status', 'in_progress')
            .order('started_at', { ascending: false })
            .limit(1)
            .single();

        if (!activeTask) {
            await sendWhatsAppMessage(user.phone,
                "❓ No active task found. Reply *1* to get today's task!");
            return;
        }

        // Mark as submitted
        const xpEarned = activeTask.micro_tasks.xp_reward;

        await supabaseAdmin
            .from('user_tasks')
            .update({
                status: 'submitted',
                proof_text: messageBody,
                submitted_at: new Date().toISOString(),
                xp_earned: xpEarned
            })
            .eq('id', activeTask.id);

        // Update streak and XP
        const { updateStreakAndXP } = require('./streakService');
        const streakInfo = await updateStreakAndXP(user.id, xpEarned);

        const message = `✅ *Task Completed!*

🎉 "${activeTask.micro_tasks.title}"

⭐ +${xpEarned} XP earned
🔥 Streak: ${streakInfo.newStreak} days
${streakInfo.streakBonus > 0 ? `🎁 Streak bonus: +${streakInfo.streakBonus} XP` : ''}
🏅 Level: ${streakInfo.level}

Keep going! Reply *1* for your next task.
_Every small action builds a better city._ 🏙️`;

        await sendWhatsAppMessage(user.phone, message);
    },

    /**
     * Start issue report flow
     */
    startIssueReport: async (user) => {
        const message = `📢 *Report a Civic Issue*

To report an issue, please visit our web app:
🌐 https://CivicStreak.vercel.app/issues/new

Or reply with a description of the issue and we'll log it for you:
Example: "Broken streetlight on Link Road near Andheri station"

📸 You can also send a photo of the issue.`;

        await sendWhatsAppMessage(user.phone, message);
    },

    /**
     * Send streak info
     */
    sendStreakInfo: async (user) => {
        const streakEmoji = user.current_streak >= 7 ? '🔥' : '💪';
        const milestones = [
            { days: 7, name: 'Curious Citizen', icon: '🌱' },
            { days: 30, name: 'Active Citizen', icon: '🌿' },
            { days: 90, name: 'Ward Warrior', icon: '⚔️' },
            { days: 180, name: 'Civic Champion', icon: '🏆' },
            { days: 365, name: 'CivicStreak Fellow', icon: '🎖️' }
        ];

        const nextMilestone = milestones.find(m => m.days > user.current_streak);
        const daysToNext = nextMilestone
            ? nextMilestone.days - user.current_streak
            : 0;

        const message = `${streakEmoji} *Your Streak Status*

Current Streak: *${user.current_streak} days*
Longest Streak: *${user.longest_streak} days*
Level: *${user.level}*

${nextMilestone
                ? `🎯 Next milestone: *${nextMilestone.name}* ${nextMilestone.icon}\n   ${daysToNext} days to go!`
                : '🎖️ You\'ve reached the highest level! CivicStreak Fellow!'}

*Milestones:*
${milestones.map(m =>
                    `${user.current_streak >= m.days ? '✅' : '⬜'} Day ${m.days}: ${m.name} ${m.icon}`
                ).join('\n')}

_Complete one task daily to maintain your streak!_`;

        await sendWhatsAppMessage(user.phone, message);
    },

    /**
     * Help message
     */
    sendHelpMessage: async (user) => {
        const message = `ℹ️ *CivicStreak Help*

*Commands:*
• *hi/hello* — Daily brief & menu
• *1 or task* — Get today's task
• *2 or portfolio* — View your civic portfolio
• *3 or circle* — Circle updates
• *4 or learn* — Daily civic lesson
• *5 or report* — Report a civic issue
• *streak* — View streak status
• *done [response]* — Submit task completion

🌐 *Web Dashboard:*
https://CivicStreak.vercel.app

📧 *Support:*
CivicStreak.help@gmail.com

_CivicStreak — Your City, Your Commitment_ 🏙️`;

        await sendWhatsAppMessage(user.phone, message);
    },

    /**
     * Default response for unrecognized messages
     */
    sendDefaultResponse: async (user) => {
        await sendWhatsAppMessage(user.phone,
            `🤔 I didn't understand that. Reply *help* to see available commands, or *hi* to start!`
        );
    }
};

module.exports = whatsappService;

3.6 Remaining Routes & Controllers

server/src/routes/issues.js

// ====================================
// Issue Routes
// ====================================
const express = require('express');
const router = express.Router();
const issueController = require('../controllers/issueController');
const { authenticate, optionalAuth } = require('../middleware/auth');
const { upload } = require('../config/cloudinary');

// GET /api/issues - Get all issues (filterable by ward)
router.get('/', optionalAuth, issueController.getIssues);

// GET /api/issues/:id - Get single issue with timeline
router.get('/:id', optionalAuth, issueController.getIssueById);

// POST /api/issues - Report a new issue
router.post('/', authenticate, upload.array('photos', 5), issueController.createIssue);

// POST /api/issues/:id/update - Add update to issue timeline
router.post('/:id/update', authenticate, upload.single('photo'), issueController.addUpdate);

// POST /api/issues/:id/upvote - Upvote an issue
router.post('/:id/upvote', authenticate, issueController.upvoteIssue);

// GET /api/issues/:id/ai-analysis - Get AI analysis of issue
router.get('/:id/ai-analysis', authenticate, issueController.getAIAnalysis);

module.exports = router;

server/src/controllers/issueController.js

// ====================================
// Issue Controller
// ====================================
const { supabaseAdmin } = require('../config/database');
const aiService = require('../services/aiService');

const issueController = {
    /**
     * Get all issues with filtering
     */
    getIssues: async (req, res) => {
        try {
            const { ward_id, status, category, sort } = req.query;

            let query = supabaseAdmin
                .from('civic_issues')
                .select(`
                    *,
                    profiles!reported_by(full_name, avatar_url),
                    wards(ward_name, ward_number)
                `);

            if (ward_id) query = query.eq('ward_id', ward_id);
            if (status) query = query.eq('status', status);
            if (category) query = query.eq('category', category);

            // Sorting
            switch (sort) {
                case 'oldest':
                    query = query.order('created_at', { ascending: true });
                    break;
                case 'upvotes':
                    query = query.order('upvotes', { ascending: false });
                    break;
                default:
                    query = query.order('created_at', { ascending: false });
            }

            const { data: issues, error } = await query.limit(50);

            if (error) throw error;

            res.json({
                success: true,
                data: issues,
                count: issues.length
            });
        } catch (error) {
            console.error('Get issues error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch issues.'
            });
        }
    },

    /**
     * Get single issue with full timeline
     */
    getIssueById: async (req, res) => {
        try {
            const { id } = req.params;

            const { data: issue, error } = await supabaseAdmin
                .from('civic_issues')
                .select(`
                    *,
                    profiles!reported_by(full_name, avatar_url),
                    wards(ward_name, ward_number)
                `)
                .eq('id', id)
                .single();

            if (error || !issue) {
                return res.status(404).json({
                    success: false,
                    message: 'Issue not found.'
                });
            }

            // Get timeline updates
            const { data: updates } = await supabaseAdmin
                .from('issue_updates')
                .select(`
                    *,
                    profiles!updated_by(full_name, avatar_url)
                `)
                .eq('issue_id', id)
                .order('created_at', { ascending: true });

            res.json({
                success: true,
                data: {
                    ...issue,
                    timeline: updates || []
                }
            });
        } catch (error) {
            console.error('Get issue error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch issue.'
            });
        }
    },

    /**
     * Create a new civic issue
     */
    createIssue: async (req, res) => {
        try {
            const {
                title, description, category,
                address, latitude, longitude, ward_id
            } = req.body;

            // Get photo URLs from uploaded files
            const photo_urls = req.files
                ? req.files.map(f => f.path)
                : [];

            // Determine ward from user profile if not provided
            let issueWardId = ward_id;
            if (!issueWardId) {
                const { data: profile } = await supabaseAdmin
                    .from('profiles')
                    .select('ward_id')
                    .eq('id', req.userId)
                    .single();
                issueWardId = profile?.ward_id;
            }

            const { data: issue, error } = await supabaseAdmin
                .from('civic_issues')
                .insert({
                    reported_by: req.userId,
                    ward_id: issueWardId,
                    title,
                    description,
                    category,
                    address,
                    latitude,
                    longitude,
                    photo_urls,
                    status: 'reported'
                })
                .select()
                .single();

            if (error) throw error;

            // Update user stats
            await supabaseAdmin
                .from('profiles')
                .update({
                    issues_reported: supabaseAdmin.rpc('increment_field', {
                        row_id: req.userId,
                        field_name: 'issues_reported'
                    })
                })
                .eq('id', req.userId);

            // Simpler: just increment directly
            const { data: profile } = await supabaseAdmin
                .from('profiles')
                .select('issues_reported')
                .eq('id', req.userId)
                .single();

            await supabaseAdmin
                .from('profiles')
                .update({
                    issues_reported: (profile?.issues_reported || 0) + 1
                })
                .eq('id', req.userId);

            // Add initial timeline entry
            await supabaseAdmin
                .from('issue_updates')
                .insert({
                    issue_id: issue.id,
                    updated_by: req.userId,
                    update_text: `Issue reported: ${title}`,
                    new_status: 'reported'
                });

            res.status(201).json({
                success: true,
                message: 'Issue reported successfully! 📢',
                data: issue
            });
        } catch (error) {
            console.error('Create issue error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to report issue.'
            });
        }
    },

    /**
     * Add update to issue timeline
     */
    addUpdate: async (req, res) => {
        try {
            const { id } = req.params;
            const { update_text, new_status } = req.body;
            const photo_url = req.file ? req.file.path : null;

            const { data: update, error } = await supabaseAdmin
                .from('issue_updates')
                .insert({
                    issue_id: id,
                    updated_by: req.userId,
                    update_text,
                    photo_url,
                    new_status
                })
                .select()
                .single();

            if (error) throw error;

            // Update issue status if changed
            if (new_status) {
                const updateData = {
                    status: new_status,
                    updated_at: new Date().toISOString()
                };

                if (new_status === 'resolved') {
                    updateData.resolution_date = new Date().toISOString();
                }

                await supabaseAdmin
                    .from('civic_issues')
                    .update(updateData)
                    .eq('id', id);
            }

            res.json({
                success: true,
                message: 'Update added!',
                data: update
            });
        } catch (error) {
            console.error('Add update error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to add update.'
            });
        }
    },

    /**
     * Upvote an issue
     */
    upvoteIssue: async (req, res) => {
        try {
            const { id } = req.params;

            const { data: issue } = await supabaseAdmin
                .from('civic_issues')
                .select('upvotes')
                .eq('id', id)
                .single();

            await supabaseAdmin
                .from('civic_issues')
                .update({ upvotes: (issue?.upvotes || 0) + 1 })
                .eq('id', id);

            res.json({
                success: true,
                message: 'Issue upvoted!'
            });
        } catch (error) {
            console.error('Upvote error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to upvote.'
            });
        }
    },

    /**
     * Get AI analysis of an issue
     */
    getAIAnalysis: async (req, res) => {
        try {
            const { id } = req.params;

            const { data: issue } = await supabaseAdmin
                .from('civic_issues')
                .select('title, description, category')
                .eq('id', id)
                .single();

            if (!issue) {
                return res.status(404).json({
                    success: false,
                    message: 'Issue not found.'
                });
            }

            const analysis = await aiService.analyzeIssue(
                `${issue.title}: ${issue.description}`,
                issue.category
            );

            res.json({
                success: true,
                data: { analysis }
            });
        } catch (error) {
            console.error('AI analysis error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to generate AI analysis.'
            });
        }
    }
};

module.exports = issueController;

server/src/routes/circles.js

// ====================================
// Circle Routes
// ====================================
const express = require('express');
const router = express.Router();
const circleController = require('../controllers/circleController');
const { authenticate } = require('../middleware/auth');

router.get('/', authenticate, circleController.getCircles);
router.get('/:id', authenticate, circleController.getCircleById);
router.post('/', authenticate, circleController.createCircle);
router.post('/:id/join', authenticate, circleController.joinCircle);
router.post('/:id/adopt-issue', authenticate, circleController.adoptIssue);
router.get('/:id/members', authenticate, circleController.getMembers);

module.exports = router;

server/src/controllers/circleController.js

// ====================================
// Circle Controller
// ====================================
const { supabaseAdmin } = require('../config/database');

const circleController = {
    /**
     * Get circles (filterable by ward)
     */
    getCircles: async (req, res) => {
        try {
            const { ward_id } = req.query;

            let query = supabaseAdmin
                .from('circles')
                .select(`
                    *,
                    wards(ward_name, ward_number),
                    circle_members(
                        user_id,
                        role,
                        profiles(full_name, avatar_url)
                    )
                `)
                .eq('is_active', true)
                .order('total_xp', { ascending: false });

            if (ward_id) {
                query = query.eq('ward_id', ward_id);
            }

            const { data: circles, error } = await query;

            if (error) throw error;

            res.json({
                success: true,
                data: circles
            });
        } catch (error) {
            console.error('Get circles error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch circles.'
            });
        }
    },

    /**
     * Get single circle with members
     */
    getCircleById: async (req, res) => {
        try {
            const { id } = req.params;

            const { data: circle, error } = await supabaseAdmin
                .from('circles')
                .select(`
                    *,
                    wards(ward_name, ward_number),
                    circle_members(
                        user_id, role, joined_at,
                        profiles(full_name, avatar_url, xp_points, current_streak, level)
                    ),
                    civic_issues!adopted_issue_id(title, status, description)
                `)
                .eq('id', id)
                .single();

            if (error || !circle) {
                return res.status(404).json({
                    success: false,
                    message: 'Circle not found.'
                });
            }

            res.json({ success: true, data: circle });
        } catch (error) {
            console.error('Get circle error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch circle.'
            });
        }
    },

    /**
     * Create a new circle
     */
    createCircle: async (req, res) => {
        try {
            const { name, description, ward_id } = req.body;

            const { data: circle, error } = await supabaseAdmin
                .from('circles')
                .insert({
                    name,
                    description,
                    ward_id,
                    created_by: req.userId,
                    member_count: 1
                })
                .select()
                .single();

            if (error) throw error;

            // Add creator as captain
            await supabaseAdmin
                .from('circle_members')
                .insert({
                    circle_id: circle.id,
                    user_id: req.userId,
                    role: 'Captain'
                });

            res.status(201).json({
                success: true,
                message: 'Circle created! Invite 4-7 more members from your ward. 🤝',
                data: circle
            });
        } catch (error) {
            console.error('Create circle error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to create circle.'
            });
        }
    },

    /**
     * Join a circle
     */
    joinCircle: async (req, res) => {
        try {
            const { id } = req.params;

            // Check circle capacity
            const { data: circle } = await supabaseAdmin
                .from('circles')
                .select('member_count, max_members')
                .eq('id', id)
                .single();

            if (!circle) {
                return res.status(404).json({
                    success: false,
                    message: 'Circle not found.'
                });
            }

            if (circle.member_count >= circle.max_members) {
                return res.status(400).json({
                    success: false,
                    message: 'This circle is full. Try another one!'
                });
            }

            // Check if already a member
            const { data: existing } = await supabaseAdmin
                .from('circle_members')
                .select('id')
                .eq('circle_id', id)
                .eq('user_id', req.userId)
                .single();

            if (existing) {
                return res.status(400).json({
                    success: false,
                    message: 'You are already a member of this circle.'
                });
            }

            // Add member
            await supabaseAdmin
                .from('circle_members')
                .insert({
                    circle_id: id,
                    user_id: req.userId,
                    role: 'Member'
                });

            // Update member count
            await supabaseAdmin
                .from('circles')
                .update({ member_count: circle.member_count + 1 })
                .eq('id', id);

            res.json({
                success: true,
                message: 'Welcome to the circle! 🤝'
            });
        } catch (error) {
            console.error('Join circle error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to join circle.'
            });
        }
    },

    /**
     * Adopt a civic issue for the circle
     */
    adoptIssue: async (req, res) => {
        try {
            const { id } = req.params;
            const { issue_id } = req.body;

            await supabaseAdmin
                .from('circles')
                .update({ adopted_issue_id: issue_id })
                .eq('id', id);

            res.json({
                success: true,
                message: 'Issue adopted! Your circle will track this for 6 months. 🎯'
            });
        } catch (error) {
            console.error('Adopt issue error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to adopt issue.'
            });
        }
    },

    /**
     * Get circle members
     */
    getMembers: async (req, res) => {
        try {
            const { id } = req.params;

            const { data: members, error } = await supabaseAdmin
                .from('circle_members')
                .select(`
                    *,
                    profiles(full_name, avatar_url, xp_points, current_streak, level)
                `)
                .eq('circle_id', id)
                .eq('is_active', true);

            if (error) throw error;

            res.json({ success: true, data: members });
        } catch (error) {
            console.error('Get members error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch members.'
            });
        }
    }
};

module.exports = circleController;

server/src/routes/ward.js

// ====================================
// Ward Routes
// ====================================
const express = require('express');
const router = express.Router();
const wardController = require('../controllers/wardController');
const { authenticate, optionalAuth } = require('../middleware/auth');

router.get('/', optionalAuth, wardController.getAllWards);
router.get('/:id/dashboard', optionalAuth, wardController.getWardDashboard);
router.get('/leaderboard/global', optionalAuth, wardController.getLeaderboard);
router.get('/:id/leaderboard', optionalAuth, wardController.getWardLeaderboard);

module.exports = router;

server/src/controllers/wardController.js

// ====================================
// Ward Controller
// ====================================
const { supabaseAdmin } = require('../config/database');

const wardController = {
    /**
     * Get all wards
     */
    getAllWards: async (req, res) => {
        try {
            const { data: wards, error } = await supabaseAdmin
                .from('wards')
                .select('*')
                .order('ward_number');

            if (error) throw error;

            res.json({ success: true, data: wards });
        } catch (error) {
            console.error('Get wards error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch wards.'
            });
        }
    },

    /**
     * Get ward impact dashboard
     */
    getWardDashboard: async (req, res) => {
        try {
            const { id } = req.params;

            // Get ward info
            const { data: ward } = await supabaseAdmin
                .from('wards')
                .select('*')
                .eq('id', id)
                .single();

            if (!ward) {
                return res.status(404).json({
                    success: false,
                    message: 'Ward not found.'
                });
            }

            // Get active CivicStreaks count
            const { count: activeUsers } = await supabaseAdmin
                .from('profiles')
                .select('id', { count: 'exact', head: true })
                .eq('ward_id', id)
                .gt('xp_points', 0);

            // Get circles count
            const { count: activeCircles } = await supabaseAdmin
                .from('circles')
                .select('id', { count: 'exact', head: true })
                .eq('ward_id', id)
                .eq('is_active', true);

            // Get issues stats
            const { data: issues } = await supabaseAdmin
                .from('civic_issues')
                .select('id, status, title, category, created_at, upvotes')
                .eq('ward_id', id)
                .order('created_at', { ascending: false });

            const issueStats = {
                total: issues?.length || 0,
                reported: issues?.filter(i => i.status === 'reported').length || 0,
                in_progress: issues?.filter(i => i.status === 'in_progress').length || 0,
                resolved: issues?.filter(i => i.status === 'resolved').length || 0,
                stale: issues?.filter(i => i.status === 'stale').length || 0
            };

            // Get top issues
            const topIssues = (issues || []).slice(0, 5).map(issue => ({
                id: issue.id,
                title: issue.title,
                category: issue.category,
                status: issue.status,
                upvotes: issue.upvotes,
                days_ago: Math.floor(
                    (Date.now() - new Date(issue.created_at).getTime()) /
                    (1000 * 60 * 60 * 24)
                )
            }));

            // Get ward leaderboard (top 5)
            const { data: topUsers } = await supabaseAdmin
                .from('profiles')
                .select('full_name, avatar_url, xp_points, current_streak, level')
                .eq('ward_id', id)
                .order('xp_points', { ascending: false })
                .limit(5);

            res.json({
                success: true,
                data: {
                    ward,
                    stats: {
                        active_CivicStreaks: activeUsers || 0,
                        active_circles: activeCircles || 0,
                        issues: issueStats
                    },
                    top_issues: topIssues,
                    top_users: topUsers || [],
                    responsiveness_score: ward.responsiveness_score
                }
            });
        } catch (error) {
            console.error('Ward dashboard error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch ward dashboard.'
            });
        }
    },

    /**
     * Global leaderboard
     */
    getLeaderboard: async (req, res) => {
        try {
            const { limit = 50 } = req.query;

            const { data: leaderboard, error } = await supabaseAdmin
                .from('profiles')
                .select(`
                    id, full_name, avatar_url, xp_points, 
                    current_streak, level, tasks_completed,
                    wards(ward_name, ward_number)
                `)
                .gt('xp_points', 0)
                .order('xp_points', { ascending: false })
                .limit(parseInt(limit));

            if (error) throw error;

            // Add rank
            const ranked = leaderboard.map((user, index) => ({
                ...user,
                rank: index + 1
            }));

            res.json({ success: true, data: ranked });
        } catch (error) {
            console.error('Leaderboard error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch leaderboard.'
            });
        }
    },

    /**
     * Ward-specific leaderboard
     */
    getWardLeaderboard: async (req, res) => {
        try {
            const { id } = req.params;

            const { data: leaderboard, error } = await supabaseAdmin
                .from('profiles')
                .select('id, full_name, avatar_url, xp_points, current_streak, level, tasks_completed')
                .eq('ward_id', id)
                .gt('xp_points', 0)
                .order('xp_points', { ascending: false })
                .limit(20);

            if (error) throw error;

            const ranked = leaderboard.map((user, index) => ({
                ...user,
                rank: index + 1
            }));

            res.json({ success: true, data: ranked });
        } catch (error) {
            console.error('Ward leaderboard error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch ward leaderboard.'
            });
        }
    }
};

module.exports = wardController;

server/src/routes/portfolio.js

// ====================================
// Portfolio Routes
// ====================================
const express = require('express');
const router = express.Router();
const portfolioController = require('../controllers/portfolioController');
const { authenticate, optionalAuth } = require('../middleware/auth');

router.get('/:userId', optionalAuth, portfolioController.getPortfolio);
router.get('/:userId/generate-summary', authenticate, portfolioController.generateSummary);
router.get('/:userId/certificate', authenticate, portfolioController.generateCertificate);

module.exports = router;

server/src/controllers/portfolioController.js

// ====================================
// Portfolio Controller
// ====================================
const { supabaseAdmin } = require('../config/database');
const aiService = require('../services/aiService');

const portfolioController = {
    /**
     * Get user's full civic portfolio (public)
     */
    getPortfolio: async (req, res) => {
        try {
            const { userId } = req.params;

            // Get profile
            const { data: profile, error } = await supabaseAdmin
                .from('profiles')
                .select(`
                    id, full_name, avatar_url, college, age,
                    xp_points, current_streak, longest_streak,
                    level, tasks_completed, issues_reported,
                    issues_resolved, rtis_filed, meetings_attended,
                    people_mentored, interests, skill_level, created_at,
                    wards(ward_name, ward_number, city)
                `)
                .eq('id', userId)
                .single();

            if (error || !profile) {
                return res.status(404).json({
                    success: false,
                    message: 'Portfolio not found.'
                });
            }

            // Get completed tasks history
            const { data: completedTasks } = await supabaseAdmin
                .from('user_tasks')
                .select(`
                    id, status, xp_earned, submitted_at, proof_url, proof_text,
                    micro_tasks(title, category, difficulty)
                `)
                .eq('user_id', userId)
                .in('status', ['submitted', 'verified'])
                .order('submitted_at', { ascending: false })
                .limit(50);

            // Get reported issues
            const { data: reportedIssues } = await supabaseAdmin
                .from('civic_issues')
                .select('id, title, category, status, created_at, upvotes')
                .eq('reported_by', userId)
                .order('created_at', { ascending: false });

            // Get achievements
            const { data: achievements } = await supabaseAdmin
                .from('user_achievements')
                .select(`
                    earned_at,
                    achievements(name, description, icon, category)
                `)
                .eq('user_id', userId)
                .order('earned_at', { ascending: false });

            // Get circle memberships
            const { data: circles } = await supabaseAdmin
                .from('circle_members')
                .select(`
                    role, joined_at,
                    circles(name, total_xp, tasks_completed, wards(ward_name))
                `)
                .eq('user_id', userId)
                .eq('is_active', true);

            // Get streak history for activity heatmap
            const { data: streakHistory } = await supabaseAdmin
                .from('streak_history')
                .select('activity_date, tasks_completed, xp_earned')
                .eq('user_id', userId)
                .order('activity_date', { ascending: false })
                .limit(365);

            // Calculate category breakdown
            const categoryBreakdown = {};
            (completedTasks || []).forEach(task => {
                const cat = task.micro_tasks?.category || 'Unknown';
                categoryBreakdown[cat] = (categoryBreakdown[cat] || 0) + 1;
            });

            // Calculate skills earned
            const skills = [];
            if (profile.rtis_filed > 0) skills.push({ name: 'RTI Filing', verified: true });
            if (profile.issues_reported >= 5) skills.push({ name: 'Issue Documentation', verified: true });
            if (profile.meetings_attended > 0) skills.push({ name: 'Ward Meeting Participation', verified: true });
            if (profile.people_mentored > 0) skills.push({ name: 'Community Mentoring', verified: true });
            if (categoryBreakdown['DOCUMENT'] >= 5) skills.push({ name: 'Civic Photography', verified: true });
            if (categoryBreakdown['VOICE'] >= 3) skills.push({ name: 'Petition & Advocacy', verified: true });
            if (categoryBreakdown['CONNECT'] >= 3) skills.push({ name: 'Community Outreach', verified: true });
            if (profile.tasks_completed >= 50) skills.push({ name: 'Consistent Civic Engagement', verified: true });

            // Days since joining
            const daysSinceJoining = Math.floor(
                (Date.now() - new Date(profile.created_at).getTime()) /
                (1000 * 60 * 60 * 24)
            );

            res.json({
                success: true,
                data: {
                    profile: {
                        ...profile,
                        days_since_joining: daysSinceJoining
                    },
                    completed_tasks: completedTasks || [],
                    reported_issues: reportedIssues || [],
                    achievements: achievements || [],
                    circles: circles || [],
                    streak_history: streakHistory || [],
                    category_breakdown: categoryBreakdown,
                    skills
                }
            });
        } catch (error) {
            console.error('Get portfolio error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch portfolio.'
            });
        }
    },

    /**
     * Generate AI portfolio summary
     */
    generateSummary: async (req, res) => {
        try {
            const { userId } = req.params;

            const { data: profile } = await supabaseAdmin
                .from('profiles')
                .select('*')
                .eq('id', userId)
                .single();

            if (!profile) {
                return res.status(404).json({
                    success: false,
                    message: 'Profile not found.'
                });
            }

            const summary = await aiService.generatePortfolioSummary(profile);

            res.json({
                success: true,
                data: { summary }
            });
        } catch (error) {
            console.error('Generate summary error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to generate summary.'
            });
        }
    },

    /**
     * Generate certificate data (PDF generation on frontend)
     */
    generateCertificate: async (req, res) => {
        try {
            const { userId } = req.params;

            const { data: profile } = await supabaseAdmin
                .from('profiles')
                .select(`
                    full_name, xp_points, current_streak, longest_streak,
                    level, tasks_completed, issues_reported, issues_resolved,
                    created_at,
                    wards(ward_name, ward_number, city)
                `)
                .eq('id', userId)
                .single();

            if (!profile) {
                return res.status(404).json({
                    success: false,
                    message: 'Profile not found.'
                });
            }

            const daysSinceJoining = Math.floor(
                (Date.now() - new Date(profile.created_at).getTime()) /
                (1000 * 60 * 60 * 24)
            );

            const certificateData = {
                name: profile.full_name,
                level: profile.level,
                ward: `${profile.wards?.ward_name} (Ward ${profile.wards?.ward_number})`,
                city: profile.wards?.city || 'Mumbai',
                xp_points: profile.xp_points,
                tasks_completed: profile.tasks_completed,
                issues_reported: profile.issues_reported,
                issues_resolved: profile.issues_resolved,
                longest_streak: profile.longest_streak,
                days_active: daysSinceJoining,
                issued_date: new Date().toISOString(),
                certificate_id: `NM-${Date.now().toString(36).toUpperCase()}`
            };

            res.json({
                success: true,
                data: certificateData
            });
        } catch (error) {
            console.error('Generate certificate error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to generate certificate.'
            });
        }
    }
};

module.exports = portfolioController;

server/src/routes/whatsapp.js

// ====================================
// WhatsApp Webhook Routes
// ====================================
const express = require('express');
const router = express.Router();
const whatsappService = require('../services/whatsappService');

// POST /api/whatsapp/webhook — Twilio sends incoming messages here
router.post('/webhook', async (req, res) => {
    try {
        const { From, Body, MediaUrl0 } = req.body;

        console.log(`📱 WhatsApp from ${From}: ${Body}`);

        // Handle incoming message
        await whatsappService.handleIncoming(From, Body, MediaUrl0);

        // Twilio expects a TwiML response (can be empty)
        res.set('Content-Type', 'text/xml');
        res.send('<Response></Response>');
    } catch (error) {
        console.error('WhatsApp webhook error:', error);
        res.set('Content-Type', 'text/xml');
        res.send('<Response></Response>');
    }
});

// GET /api/whatsapp/webhook — Twilio verification
router.get('/webhook', (req, res) => {
    res.status(200).send('CivicStreak WhatsApp Bot is active 🏙️');
});

module.exports = router;

server/src/routes/users.js

// ====================================
// User Routes
// ====================================
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const { authenticate } = require('../middleware/auth');
const { upload } = require('../config/cloudinary');

// GET /api/users/profile
router.get('/profile', authenticate, userController.getProfile);

// PUT /api/users/profile
router.put('/profile', authenticate, userController.updateProfile);

// PUT /api/users/avatar
router.put('/avatar', authenticate, upload.single('avatar'), userController.updateAvatar);

// POST /api/users/onboarding
router.post('/onboarding', authenticate, userController.completeOnboarding);

// GET /api/users/stats
router.get('/stats', authenticate, userController.getStats);

// GET /api/users/notifications
router.get('/notifications', authenticate, userController.getNotifications);

// PUT /api/users/notifications/:id/read
router.put('/notifications/:id/read', authenticate, userController.markNotificationRead);

module.exports = router;

server/src/controllers/userController.js

// ====================================
// User Controller
// ====================================
const { supabaseAdmin } = require('../config/database');

const userController = {
    /**
     * Get authenticated user's profile
     */
    getProfile: async (req, res) => {
        try {
            const { data: profile, error } = await supabaseAdmin
                .from('profiles')
                .select(`
                    *,
                    wards(*)
                `)
                .eq('id', req.userId)
                .single();

            if (error || !profile) {
                return res.status(404).json({
                    success: false,
                    message: 'Profile not found.'
                });
            }

            res.json({ success: true, data: profile });
        } catch (error) {
            console.error('Get profile error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch profile.'
            });
        }
    },

    /**
     * Update profile
     */
    updateProfile: async (req, res) => {
        try {
            const allowedFields = [
                'full_name', 'phone', 'ward_id', 'age',
                'college', 'interests', 'preferred_language',
                'whatsapp_opted_in'
            ];

            const updates = {};
            for (const field of allowedFields) {
                if (req.body[field] !== undefined) {
                    updates[field] = req.body[field];
                }
            }
            updates.updated_at = new Date().toISOString();

            const { data: profile, error } = await supabaseAdmin
                .from('profiles')
                .update(updates)
                .eq('id', req.userId)
                .select()
                .single();

            if (error) throw error;

            res.json({
                success: true,
                message: 'Profile updated!',
                data: profile
            });
        } catch (error) {
            console.error('Update profile error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to update profile.'
            });
        }
    },

    /**
     * Update avatar
     */
    updateAvatar: async (req, res) => {
        try {
            if (!req.file) {
                return res.status(400).json({
                    success: false,
                    message: 'No image provided.'
                });
            }

            const avatar_url = req.file.path;

            const { data: profile, error } = await supabaseAdmin
                .from('profiles')
                .update({ avatar_url, updated_at: new Date().toISOString() })
                .eq('id', req.userId)
                .select()
                .single();

            if (error) throw error;

            res.json({
                success: true,
                message: 'Avatar updated!',
                data: { avatar_url: profile.avatar_url }
            });
        } catch (error) {
            console.error('Update avatar error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to update avatar.'
            });
        }
    },

    /**
     * Complete onboarding flow
     */
    completeOnboarding: async (req, res) => {
        try {
            const { ward_id, interests, college, age, preferred_language } = req.body;

            const { data: profile, error } = await supabaseAdmin
                .from('profiles')
                .update({
                    ward_id,
                    interests,
                    college,
                    age,
                    preferred_language,
                    onboarding_completed: true,
                    updated_at: new Date().toISOString()
                })
                .eq('id', req.userId)
                .select(`*, wards(ward_name, ward_number)`)
                .single();

            if (error) throw error;

            res.json({
                success: true,
                message: `Welcome to CivicStreak, ${profile.full_name}! 🎉 Your civic journey begins now.`,
                data: profile
            });
        } catch (error) {
            console.error('Onboarding error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to complete onboarding.'
            });
        }
    },

    /**
     * Get user statistics
     */
    getStats: async (req, res) => {
        try {
            const { data: profile } = await supabaseAdmin
                .from('profiles')
                .select(`
                    xp_points, current_streak, longest_streak, level,
                    tasks_completed, issues_reported, issues_resolved,
                    rtis_filed, meetings_attended, people_mentored
                `)
                .eq('id', req.userId)
                .single();

            // Get task breakdown by category
            const { data: taskBreakdown } = await supabaseAdmin
                .from('user_tasks')
                .select(`
                    micro_tasks(category)
                `)
                .eq('user_id', req.userId)
                .in('status', ['submitted', 'verified']);

            const categoryCount = {};
            (taskBreakdown || []).forEach(t => {
                const cat = t.micro_tasks?.category || 'Unknown';
                categoryCount[cat] = (categoryCount[cat] || 0) + 1;
            });

            // Get streak history (last 30 days)
            const thirtyDaysAgo = new Date();
            thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

            const { data: recentStreak } = await supabaseAdmin
                .from('streak_history')
                .select('activity_date, tasks_completed, xp_earned')
                .eq('user_id', req.userId)
                .gte('activity_date', thirtyDaysAgo.toISOString().split('T')[0])
                .order('activity_date', { ascending: true });

            // Get global rank
            const { data: allUsers } = await supabaseAdmin
                .from('profiles')
                .select('id')
                .gt('xp_points', profile?.xp_points || 0);

            const globalRank = (allUsers?.length || 0) + 1;

            res.json({
                success: true,
                data: {
                    ...profile,
                    category_breakdown: categoryCount,
                    recent_activity: recentStreak || [],
                    global_rank: globalRank
                }
            });
        } catch (error) {
            console.error('Get stats error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch stats.'
            });
        }
    },

    /**
     * Get user notifications
     */
    getNotifications: async (req, res) => {
        try {
            const { data: notifications, error } = await supabaseAdmin
                .from('notifications')
                .select('*')
                .eq('user_id', req.userId)
                .order('created_at', { ascending: false })
                .limit(30);

            if (error) throw error;

            const unreadCount = (notifications || []).filter(n => !n.is_read).length;

            res.json({
                success: true,
                data: {
                    notifications: notifications || [],
                    unread_count: unreadCount
                }
            });
        } catch (error) {
            console.error('Get notifications error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to fetch notifications.'
            });
        }
    },

    /**
     * Mark notification as read
     */
    markNotificationRead: async (req, res) => {
        try {
            const { id } = req.params;

            await supabaseAdmin
                .from('notifications')
                .update({ is_read: true })
                .eq('id', id)
                .eq('user_id', req.userId);

            res.json({ success: true, message: 'Marked as read.' });
        } catch (error) {
            console.error('Mark read error:', error);
            res.status(500).json({
                success: false,
                message: 'Failed to mark notification.'
            });
        }
    }
};

module.exports = userController;