import { Request, Response } from 'express'; import { HolidayModel as Holiday, HolidayType } from '../models/mongoose/Holiday.schema'; import { holidayMongoService as holidayService } from '../services/holiday.service'; import { activityTypeService } from '../services/activityType.service'; import { adminConfigMongoService } from '../services/adminConfig.service'; import logger from '../utils/logger'; import dayjs from 'dayjs'; import { initializeHolidaysCache, clearWorkingHoursCache } from '../utils/tatTimeUtils'; import { clearConfigCache } from '../services/configReader.service'; import { UserModel as User, IUser } from '../models/mongoose/User.schema'; import { UserRole } from '../types/user.types'; /** * Get all holidays (with optional year filter) */ export const getAllHolidays = async (req: Request, res: Response): Promise => { try { const { year } = req.query; const yearNum = year ? parseInt(year as string) : undefined; const holidays = await holidayService.getAllActiveHolidays(yearNum); // Format response to match legacy structure const formattedHolidays = holidays.map(mapToLegacyHoliday); res.json({ success: true, data: formattedHolidays, count: formattedHolidays.length }); } catch (error) { logger.error('[Admin] Error fetching holidays:', error); res.status(500).json({ success: false, error: 'Failed to fetch holidays' }); } }; /** * Get holiday calendar for a specific year */ export const getHolidayCalendar = async (req: Request, res: Response): Promise => { try { const { year } = req.params; const yearNum = parseInt(year); if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) { res.status(400).json({ success: false, error: 'Invalid year' }); return; } // Use getAllActiveHolidays to get full docs, then filter by year in memory or update service // Service has getHolidayCalendar(year) which returns partial objects. // Better to use getAllActiveHolidays(year) and map ourselves. const holidays = await holidayService.getAllActiveHolidays(yearNum); const formattedHolidays = holidays.map(mapToLegacyHoliday); res.json({ success: true, year: yearNum, holidays: formattedHolidays, count: formattedHolidays.length }); } catch (error) { logger.error('[Admin] Error fetching holiday calendar:', error); res.status(500).json({ success: false, error: 'Failed to fetch holiday calendar' }); } }; /** * Create a new holiday */ export const createHoliday = async (req: Request, res: Response): Promise => { try { const userId = req.user?.userId; if (!userId) { res.status(401).json({ success: false, error: 'User not authenticated' }); return; } const { holidayDate, holidayName, description, holidayType, isRecurring, recurrenceRule, appliesToDepartments, appliesToLocations } = req.body; // Validate required fields if (!holidayDate || !holidayName) { res.status(400).json({ success: false, error: 'Holiday date and name are required' }); return; } const holiday = await holidayService.createHoliday({ holidayDate, holidayName, holidayType: (holidayType as any) || HolidayType.ORGANIZATIONAL, year: new Date(holidayDate).getFullYear(), appliesToDepartments, appliesToLocations, description, isRecurring, recurrenceRule, createdBy: userId }); // Reload holidays cache await initializeHolidaysCache(); // Format response to match legacy structure const legacyResponse = mapToLegacyHoliday(holiday); res.status(201).json({ success: true, message: 'Holiday created successfully', data: [legacyResponse] // Returning array as requested }); } catch (error: any) { logger.error('[Admin] Error creating holiday:', error); res.status(500).json({ success: false, error: error.message || 'Failed to create holiday' }); } }; /** * Helper to map Mongoose document to Legacy JSON format */ const mapToLegacyHoliday = (holiday: any) => ({ holidayId: holiday._id, holidayDate: dayjs(holiday.holidayDate).format('YYYY-MM-DD'), holidayName: holiday.holidayName, description: holiday.description || null, isRecurring: holiday.isRecurring || false, recurrenceRule: holiday.recurrenceRule || null, holidayType: holiday.holidayType, isActive: holiday.isActive !== undefined ? holiday.isActive : true, appliesToDepartments: (holiday.appliesToDepartments && holiday.appliesToDepartments.length > 0) ? holiday.appliesToDepartments : null, appliesToLocations: (holiday.appliesToLocations && holiday.appliesToLocations.length > 0) ? holiday.appliesToLocations : null, createdBy: holiday.createdBy || null, updatedBy: holiday.updatedBy || null, createdAt: holiday.createdAt, updatedAt: holiday.updatedAt, created_at: holiday.createdAt, updated_at: holiday.updatedAt }); /** * Update a holiday */ export const updateHoliday = async (req: Request, res: Response): Promise => { try { const userId = req.user?.userId; if (!userId) { res.status(401).json({ success: false, error: 'User not authenticated' }); return; } const { holidayId } = req.params; const updates = req.body; const holiday = await holidayService.updateHoliday(holidayId, updates); if (!holiday) { res.status(404).json({ success: false, error: 'Holiday not found' }); return; } // Reload holidays cache await initializeHolidaysCache(); res.json({ success: true, message: 'Holiday updated successfully', data: [mapToLegacyHoliday(holiday)] // Returning array for consistency }); } catch (error: any) { logger.error('[Admin] Error updating holiday:', error); res.status(500).json({ success: false, error: error.message || 'Failed to update holiday' }); } }; /** * Delete (deactivate) a holiday */ export const deleteHoliday = async (req: Request, res: Response): Promise => { try { const { holidayId } = req.params; await holidayService.deleteHoliday(holidayId); // Reload holidays cache await initializeHolidaysCache(); res.json({ success: true, message: 'Holiday deleted successfully' }); } catch (error: any) { logger.error('[Admin] Error deleting holiday:', error); res.status(500).json({ success: false, error: error.message || 'Failed to delete holiday' }); } }; /** * Bulk import holidays from CSV/JSON */ export const bulkImportHolidays = async (req: Request, res: Response): Promise => { try { const userId = req.user?.userId; if (!userId) { res.status(401).json({ success: false, error: 'User not authenticated' }); return; } const { holidays } = req.body; if (!Array.isArray(holidays) || holidays.length === 0) { res.status(400).json({ success: false, error: 'Holidays array is required' }); return; } const result = await holidayService.bulkImportHolidays(holidays); // Reload holidays cache await initializeHolidaysCache(); res.json({ success: true, message: `Imported ${result.success} holidays, ${result.failed} failed`, data: result }); } catch (error: any) { logger.error('[Admin] Error bulk importing holidays:', error); res.status(500).json({ success: false, error: error.message || 'Failed to import holidays' }); } }; /** * Get public configurations (read-only, non-sensitive) * Accessible to all authenticated users */ export const getPublicConfigurations = async (req: Request, res: Response): Promise => { try { const { category } = req.query; // Only allow certain categories for public access const allowedCategories = ['DOCUMENT_POLICY', 'TAT_SETTINGS', 'WORKFLOW_SHARING', 'SYSTEM_SETTINGS']; if (category && !allowedCategories.includes(category as string)) { res.status(403).json({ success: false, error: 'Access denied to this configuration category' }); return; } const configurations = await adminConfigMongoService.getPublicConfigurations(category as string); res.json({ success: true, data: configurations, count: configurations.length }); } catch (error) { logger.error('[Admin] Error fetching public configurations:', error); res.status(500).json({ success: false, error: 'Failed to fetch configurations' }); } }; /** * Get all admin configurations */ export const getAllConfigurations = async (req: Request, res: Response): Promise => { try { const { category } = req.query; const configurations = await adminConfigMongoService.getAllConfigurations(category as string); res.json({ success: true, data: configurations, count: configurations.length }); } catch (error) { logger.error('[Admin] Error fetching configurations:', error); res.status(500).json({ success: false, error: 'Failed to fetch configurations' }); } }; /** * Update a configuration */ export const updateConfiguration = async (req: Request, res: Response): Promise => { try { const userId = req.user?.userId; if (!userId) { res.status(401).json({ success: false, error: 'User not authenticated' }); return; } const { configKey } = req.params; const { configValue } = req.body; if (configValue === undefined) { res.status(400).json({ success: false, error: 'Config value is required' }); return; } // Update configuration const config = await adminConfigMongoService.updateConfig(configKey, configValue, userId); if (!config) { res.status(404).json({ success: false, error: 'Configuration not found or not editable' }); return; } // Clear config cache so new values are used immediately clearConfigCache(); // If working hours config was updated, also clear working hours cache const workingHoursKeys = ['WORK_START_HOUR', 'WORK_END_HOUR', 'WORK_START_DAY', 'WORK_END_DAY']; if (workingHoursKeys.includes(configKey)) { await clearWorkingHoursCache(); logger.info(`[Admin] Working hours configuration '${configKey}' updated - cache cleared and reloaded`); } // If AI config was updated, reinitialize AI service const aiConfigKeys = ['AI_ENABLED']; if (aiConfigKeys.includes(configKey)) { try { const { aiService } = require('../services/ai.service'); await aiService.reinitialize(); logger.info(`[Admin] AI configuration '${configKey}' updated - AI service reinitialized with ${aiService.getProviderName()}`); } catch (error) { logger.error(`[Admin] Failed to reinitialize AI service:`, error); } } else { logger.info(`[Admin] Configuration '${configKey}' updated and cache cleared`); } res.json({ success: true, message: 'Configuration updated successfully' }); } catch (error: any) { logger.error('[Admin] Error updating configuration:', error); res.status(500).json({ success: false, error: error.message || 'Failed to update configuration' }); } }; /** * Reset configuration to default value */ export const resetConfiguration = async (req: Request, res: Response): Promise => { try { const { configKey } = req.params; const config = await adminConfigMongoService.resetConfig(configKey); if (!config) { res.status(404).json({ success: false, error: 'Configuration not found' }); return; } // Clear config cache so reset values are used immediately clearConfigCache(); // If working hours config was reset, also clear working hours cache const workingHoursKeys = ['WORK_START_HOUR', 'WORK_END_HOUR', 'WORK_START_DAY', 'WORK_END_DAY']; if (workingHoursKeys.includes(configKey)) { await clearWorkingHoursCache(); logger.info(`[Admin] Working hours configuration '${configKey}' reset to default - cache cleared and reloaded`); } else { logger.info(`[Admin] Configuration '${configKey}' reset to default and cache cleared`); } res.json({ success: true, message: 'Configuration reset to default' }); } catch (error: any) { logger.error('[Admin] Error resetting configuration:', error); res.status(500).json({ success: false, error: error.message || 'Failed to reset configuration' }); } }; /** * ============================================ * USER ROLE MANAGEMENT (RBAC) * ============================================ */ /** * Update User Role * * Purpose: Change user's role (USER, MANAGEMENT, ADMIN) * * Access: ADMIN only * * Body: { role: 'USER' | 'MANAGEMENT' | 'ADMIN' } */ export const updateUserRole = async (req: Request, res: Response): Promise => { try { const { userId } = req.params; const { role } = req.body; // Validate role const validRoles: UserRole[] = ['USER', 'MANAGEMENT', 'ADMIN']; if (!role || !validRoles.includes(role)) { res.status(400).json({ success: false, error: 'Invalid role. Must be USER, MANAGEMENT, or ADMIN' }); return; } // Find user const user = await User.findOne({ userId }); if (!user) { res.status(404).json({ success: false, error: 'User not found' }); return; } // Store old role for logging const oldRole = user.role; // Prevent self-demotion from ADMIN (safety check) const adminUser = req.user; if (adminUser?.userId === userId && role !== 'ADMIN') { res.status(400).json({ success: false, error: 'Cannot remove your own admin privileges. Ask another admin to change your role.' }); return; } // Update role user.role = role; await user.save(); logger.info(`✅ User role updated by ${adminUser?.email}: ${user.email} - ${oldRole} → ${role}`); res.json({ success: true, message: `User role updated from ${oldRole} to ${role}`, data: { userId: user.userId, email: user.email, displayName: user.displayName, role: user.role, previousRole: oldRole, updatedAt: user.updatedAt } }); } catch (error) { logger.error('[Admin] Error updating user role:', error); res.status(500).json({ success: false, error: 'Failed to update user role' }); } }; /** * Get All Users by Role (with pagination and filtering) * * Purpose: List all users with optional role filtering and pagination * * Access: ADMIN only * * Query: * - ?role=ADMIN | MANAGEMENT | USER | ALL | ELEVATED (default: ELEVATED for ADMIN+MANAGEMENT only) * - ?page=1 (default) * - ?limit=10 (default) */ export const getUsersByRole = async (req: Request, res: Response): Promise => { try { const { role, page = '1', limit = '10' } = req.query; const pageNum = parseInt(page as string) || 1; const limitNum = Math.min(parseInt(limit as string) || 10, 100); // Max 100 per page const offset = (pageNum - 1) * limitNum; const whereClause: any = { isActive: true }; // Handle role filtering if (role && role !== 'ALL' && role !== 'ELEVATED') { const validRoles: string[] = ['USER', 'MANAGEMENT', 'ADMIN']; if (!validRoles.includes(role as string)) { res.status(400).json({ success: false, error: 'Invalid role. Must be USER, MANAGEMENT, ADMIN, ALL, or ELEVATED' }); return; } whereClause.role = role; } else if (role === 'ELEVATED' || !role) { // Default: Show only ADMIN and MANAGEMENT (elevated users) whereClause.role = { $in: ['ADMIN', 'MANAGEMENT'] }; } // If role === 'ALL', don't filter by role (show all users) // Get total count for pagination const totalUsers = await User.countDocuments(whereClause); const totalPages = Math.ceil(totalUsers / limitNum); // Get paginated users const users = await User.find(whereClause) .select('userId email displayName firstName lastName department designation role manager postalAddress lastLogin createdAt') .sort({ role: 1, displayName: 1 }) .skip(offset) .limit(limitNum); // Get role summary (across all users, not just current page) const roleStatsRaw = await User.aggregate([ { $match: { isActive: true } }, { $group: { _id: '$role', count: { $sum: 1 } } }, { $sort: { _id: 1 } } ]); const summary = { ADMIN: roleStatsRaw.find((s: any) => s._id === 'ADMIN')?.count || 0, MANAGEMENT: roleStatsRaw.find((s: any) => s._id === 'MANAGEMENT')?.count || 0, USER: roleStatsRaw.find((s: any) => s._id === 'USER')?.count || 0 }; res.json({ success: true, data: { users: users, pagination: { currentPage: pageNum, totalPages: totalPages, totalUsers: totalUsers, limit: limitNum, hasNextPage: pageNum < totalPages, hasPrevPage: pageNum > 1 }, summary, filter: role || 'ELEVATED' } }); } catch (error) { logger.error('[Admin] Error fetching users by role:', error); res.status(500).json({ success: false, error: 'Failed to fetch users' }); } }; /** * Get Role Statistics * * Purpose: Get count of users in each role * * Access: ADMIN only */ export const getRoleStatistics = async (req: Request, res: Response): Promise => { try { const stats = await User.aggregate([ { $group: { _id: '$role', count: { $sum: 1 }, activeCount: { $sum: { $cond: ['$isActive', 1, 0] } }, inactiveCount: { $sum: { $cond: ['$isActive', 0, 1] } } } }, { $sort: { _id: 1 } } ]); // Format for frontend const formattedStats = stats.map((stat: any) => ({ role: stat._id, count: stat.count, active_count: stat.activeCount, inactive_count: stat.inactiveCount })); res.json({ success: true, data: { statistics: formattedStats, total: formattedStats.reduce((sum: number, stat: any) => sum + stat.count, 0) } }); } catch (error) { logger.error('[Admin] Error fetching role statistics:', error); res.status(500).json({ success: false, error: 'Failed to fetch role statistics' }); } }; /** * Assign role to user by email * * Purpose: Search user in Okta, create if doesn't exist, then assign role * * Access: ADMIN only * * Body: { email: string, role: 'USER' | 'MANAGEMENT' | 'ADMIN' } */ export const assignRoleByEmail = async (req: Request, res: Response): Promise => { try { const { email, role } = req.body; const currentUserId = req.user?.userId; // Validate inputs if (!email || !role) { res.status(400).json({ success: false, error: 'Email and role are required' }); return; } // Validate role if (!['USER', 'MANAGEMENT', 'ADMIN'].includes(role)) { res.status(400).json({ success: false, error: 'Invalid role. Must be USER, MANAGEMENT, or ADMIN' }); return; } logger.info(`[Admin] Assigning role ${role} to ${email} by user ${currentUserId}`); // First, check if user already exists in our database let user: IUser | null = await User.findOne({ email }); if (!user) { // User doesn't exist, need to fetch from Okta and create logger.info(`[Admin] User ${email} not found in database, fetching from Okta...`); // Import UserService to fetch full profile from Okta const { UserService } = await import('@services/user.service'); const userService = new UserService(); try { // Fetch full user profile from Okta Users API (includes manager, jobTitle, etc.) const oktaUserData = await userService.fetchAndExtractOktaUserByEmail(email); if (!oktaUserData) { res.status(404).json({ success: false, error: 'User not found in Okta. Please ensure the email is correct.' }); return; } // Create user in our database via centralized userService with all fields including manager user = (await userService.createOrUpdateUser({ ...oktaUserData, role: role as any, // Set the assigned role isActive: true, // Ensure user is active })) as IUser; logger.info(`[Admin] Created new user ${email} with role ${role} (manager: ${oktaUserData.manager || 'N/A'})`); } catch (oktaError: any) { logger.error('[Admin] Error fetching from Okta:', oktaError); res.status(500).json({ success: false, error: 'Failed to fetch user from Okta: ' + (oktaError.message || 'Unknown error') }); return; } } else { // User exists - fetch latest data from Okta and sync all fields including role const previousRole = user.role; // Prevent self-demotion if (user.userId === currentUserId && role !== 'ADMIN') { res.status(403).json({ success: false, error: 'You cannot demote yourself from ADMIN role' }); return; } // Import UserService to fetch latest data from Okta const { UserService } = await import('@services/user.service'); const userService = new UserService(); try { // Fetch full user profile from Okta Users API to sync manager and other fields const oktaUserData = await userService.fetchAndExtractOktaUserByEmail(email); if (oktaUserData) { // Sync all fields from Okta including the new role using centralized method user = (await userService.createOrUpdateUser({ ...oktaUserData, // Includes all fields: manager, jobTitle, postalAddress, etc. role: role as any, // Set the new role isActive: true, // Ensure user is active })) as IUser; logger.info(`[Admin] Synced user ${email} from Okta (manager: ${oktaUserData.manager || 'N/A'}) and updated role from ${previousRole} to ${role}`); } else { // Okta user not found, just update role user.role = role as any; await user.save(); logger.info(`[Admin] Updated user ${email} role from ${previousRole} to ${role} (Okta data not available)`); } } catch (oktaError: any) { // If Okta fetch fails, just update the role logger.warn(`[Admin] Failed to fetch Okta data for ${email}, updating role only:`, oktaError.message); user.role = role as any; await user.save(); logger.info(`[Admin] Updated user ${email} role from ${previousRole} to ${role} (Okta sync failed)`); } } if (!user) { res.status(500).json({ success: false, error: 'Failed to create or update user' }); return; } res.json({ success: true, message: `Successfully assigned ${role} role to ${user.displayName || email}`, data: { userId: user.userId, email: user.email, displayName: user.displayName, role: user.role } }); } catch (error) { logger.error('[Admin] Error assigning role by email:', error); res.status(500).json({ success: false, error: 'Failed to assign role' }); } }; // ==================== Activity Type Management Routes ==================== /** * Get all activity types (optionally filtered by active status) */ export const getAllActivityTypes = async (req: Request, res: Response): Promise => { try { const { activeOnly } = req.query; const activeOnlyBool = activeOnly === 'true'; const activityTypes = await activityTypeService.getAllActivityTypes(activeOnlyBool); res.json({ success: true, data: activityTypes, count: activityTypes.length }); } catch (error: any) { logger.error('[Admin] Error fetching activity types:', error); res.status(500).json({ success: false, error: error.message || 'Failed to fetch activity types' }); } }; /** * Get a single activity type by ID */ export const getActivityTypeById = async (req: Request, res: Response): Promise => { try { const { activityTypeId } = req.params; const activityType = await activityTypeService.getActivityTypeById(activityTypeId); if (!activityType) { res.status(404).json({ success: false, error: 'Activity type not found' }); return; } res.json({ success: true, data: activityType }); } catch (error: any) { logger.error('[Admin] Error fetching activity type:', error); res.status(500).json({ success: false, error: error.message || 'Failed to fetch activity type' }); } }; /** * Create a new activity type */ export const createActivityType = async (req: Request, res: Response): Promise => { try { const userId = req.user?.userId; if (!userId) { res.status(401).json({ success: false, error: 'User not authenticated' }); return; } const { title, itemCode, taxationType, sapRefNo } = req.body; // Validate required fields if (!title) { res.status(400).json({ success: false, error: 'Activity type title is required' }); return; } const activityType = await activityTypeService.createActivityType({ title, itemCode: itemCode || null, taxationType: taxationType || null, sapRefNo: sapRefNo || null, createdBy: userId }); res.status(201).json({ success: true, message: 'Activity type created successfully', data: activityType }); } catch (error: any) { logger.error('[Admin] Error creating activity type:', error); res.status(500).json({ success: false, error: error.message || 'Failed to create activity type' }); } }; /** * Update an activity type */ export const updateActivityType = async (req: Request, res: Response): Promise => { try { const userId = req.user?.userId; if (!userId) { res.status(401).json({ success: false, error: 'User not authenticated' }); return; } const { activityTypeId } = req.params; const updates = req.body; const activityType = await activityTypeService.updateActivityType(activityTypeId, updates, userId); if (!activityType) { res.status(404).json({ success: false, error: 'Activity type not found' }); return; } res.json({ success: true, message: 'Activity type updated successfully', data: activityType }); } catch (error: any) { logger.error('[Admin] Error updating activity type:', error); res.status(500).json({ success: false, error: error.message || 'Failed to update activity type' }); } }; /** * Delete (deactivate) an activity type */ export const deleteActivityType = async (req: Request, res: Response): Promise => { try { const { activityTypeId } = req.params; await activityTypeService.deleteActivityType(activityTypeId); res.json({ success: true, message: 'Activity type deleted successfully' }); } catch (error: any) { logger.error('[Admin] Error deleting activity type:', error); res.status(500).json({ success: false, error: error.message || 'Failed to delete activity type' }); } };