Website : rimsha.abasa.com
backdoor
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
var
/
www
/
mudeerapi.abasa.com
/
nodetest
/
src
/
controllers
/
Filename :
attendance.controller.js
back
Copy
import mongoose from "mongoose"; import attendance from "../models/attendance.model.js"; import eventActivity from "../models/eventActivity.models.js"; import moment from "moment"; import dotenv from "dotenv"; import transporter from "../config/transporter.js"; import { user } from "../models/user.models.js"; import UserStatus from "../models/userStatus.model.js"; import cron from "node-cron"; import { sendCheckoutNotification, sendLoginNotification } from '../services/notificationService.js'; import { validateLeaveBalance, updateLeaveBalance } from './leave.controller.js'; import { reverseLeaveBalance } from '../services/leaveBalance.service.js'; import { presentTemplate, lateTemplate, absentTemplate, paidLeaveTemplate, dayOffTemplate, occasionalOffTemplate, unpaidLeaveTemplate } from "../template/attendanceTemplates.js"; import connectRedis, { redisClient } from "../db/redis.js"; import { cacheHelper } from "../utils/cacheHelper.js"; import { invalidateCache } from "../utils/invalidationHelper.js"; import { convertMakkahTimeStringToUTC } from "../utils/timezone.js"; dotenv.config({ path: `.env`, }); export const markAttendance = async (req, res, next) => { let email = ""; const userId = req.body.user_id; let AttendanceStatus; let autoConverted = false; const today = new Date(); const dayOfWeek = today.getDay(); const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const currentDay = days[dayOfWeek]; let hours = today.getHours(); const minutes = today.getMinutes(); const ampm = hours >= 12 ? "PM" : "AM"; hours = hours % 12; hours = hours ? hours : 12; const formattedTime = `${hours}:${minutes.toString().padStart(2, "0")} ${ampm}`; console.log(`Current Time: ${formattedTime}`); try { // Use UTC to ensure consistent "today" check regardless of server timezone const todayStart = new Date(); todayStart.setUTCHours(0, 0, 0, 0); const existingAttendance = await attendance.findOne({ user_id: userId, createdAt: { $gte: todayStart }, }); if (existingAttendance) { return res.status(200).json({ message: "Attendance already marked for today.", }); } else { const User = await user.findById(userId).populate('department'); if (!User) { return res.status(404).json({ error: 'User not found' }); } // Check if user is active if (User.isActive === false) { return res.status(403).json({ error: 'User account is deactivated. Cannot mark attendance.' }); } if (req.body.attendanceStatus && req.body.attendanceStatus !== " ") { AttendanceStatus = req.body.attendanceStatus; // Validate leave balance for leave requests and Absent (Absent uses paid leaves first if available; otherwise marked as Absent) if (['Paid Leave', 'Occasional off', 'Absent'].includes(AttendanceStatus)) { const validation = await validateLeaveBalance(userId, AttendanceStatus); if (!validation.isValid) { return res.status(400).json({ error: validation.error, attendanceStatus: AttendanceStatus }); } AttendanceStatus = validation.modifiedStatus; autoConverted = validation.autoConverted || false; } } else { email = User.email; // Check if today is a day off for this user if (!User.day_off.includes(currentDay)) { console.log("Working day: ", currentDay); const currentTimeUtc = new Date(); const startTimeUtc = convertMakkahTimeStringToUTC(User.start_time, currentTimeUtc); const differenceInMillis = currentTimeUtc - startTimeUtc; const differenceInMinutes = Math.floor(differenceInMillis / 60000); console.log(`The UTC-normalized difference is ${differenceInMinutes} minutes`); const timeDifferenceInSeconds = differenceInMillis / 1000; console.log(`Time difference in seconds: ${timeDifferenceInSeconds}`); if (currentTimeUtc > startTimeUtc) { if (timeDifferenceInSeconds > 180) { console.log("Late"); AttendanceStatus = "Late"; } else { console.log("Present"); AttendanceStatus = "Present"; } } else { console.log("Present"); AttendanceStatus = "Present"; } } else { console.log("Today is a day off: ", currentDay); AttendanceStatus = "Day off"; } } // Department can be missing for some users; don't crash attendance creation. // Prefer populated department._id, then raw department, then optional request department, else null. const departmentId = (User.department && (User.department._id || User.department)) || (req.body.department && mongoose.Types.ObjectId.isValid(req.body.department) ? req.body.department : null); let attendanceTable = new attendance({ _id: new mongoose.Types.ObjectId(), user_id: userId, name: req.body.name, attendanceStatus: AttendanceStatus, department: departmentId, app_version: req.body.app_version, createdAt: new Date(), // Always use server UTC time - no client override }); const result = await attendanceTable.save(); // Update leave balance if it's a leave type if (['Paid Leave', 'Occasional off', 'Unpaid Leave'].includes(AttendanceStatus)) { const leaveUpdate = await updateLeaveBalance(userId, AttendanceStatus, result._id); if (!leaveUpdate.success) { console.error('Failed to update leave balance:', leaveUpdate.error); } } // Invalidate attendance cache for this user await invalidateCache.attendance(userId); const responseData = { message: "Attendance marked for today.", attendanceTable: result, }; if (autoConverted) { responseData.notice = AttendanceStatus === 'Absent' ? "Paid leave balance exhausted. Marked as absent." : "Absent day(s) applied as paid leave (balance used)."; } res.status(200).json(responseData); if (res.statusCode === 200) { switch (AttendanceStatus) { case "Present": await sendEmail(email, presentTemplate(req.body.name, "Sohaib")); break; case "Late": await sendEmail(email, lateTemplate(req.body.name, "Sohaib")); break; case "Absent": await sendEmail(email, absentTemplate(req.body.name, "Sohaib")); break; case "Day off": await sendEmail(email, dayOffTemplate(req.body.name, "Sohaib")); break; case "Occasional off": await sendEmail(email, occasionalOffTemplate(req.body.name, "Sohaib")); break; case "Paid Leave": await sendEmail(email, paidLeaveTemplate(req.body.name, "Sohaib")); break; case "Unpaid Leave": await sendEmail(email, unpaidLeaveTemplate(req.body.name, "Sohaib")); break; default: throw new Error("Unsupported Attendance Status"); } console.log("Attendance recorded"); // Send hierarchical login notification (check-in notification) if (['Present', 'Late'].includes(AttendanceStatus)) { let minutesLate = null; if (AttendanceStatus === 'Late' && User.start_time) { const now = new Date(); const startTimeUtc = convertMakkahTimeStringToUTC(User.start_time, now); minutesLate = Math.floor((now - startTimeUtc) / 60000); } sendLoginNotification(User, minutesLate) .then(result => { if (result.success) { console.log(`Login notification sent for ${User.first_name} ${User.last_name} to ${result.totalRecipients} recipients`); } else { console.error('Login notification failed:', result.error); } }) .catch(error => console.error('Login notification error:', error)); } } } } catch (err) { console.log(err); res.status(500).json({ error: err, }); } }; // Helper function to update a single attendance record const updateSingleAttendance = async (attendanceId, newAttendanceStatus) => { try { // Validate attendance ID if (!mongoose.Types.ObjectId.isValid(attendanceId)) { return { success: false, error: 'Invalid attendance ID', attendanceId }; } // Find existing attendance record const existingAttendance = await attendance.findById(attendanceId); if (!existingAttendance) { return { success: false, error: 'Attendance record not found', attendanceId }; } // Get user information const User = await user.findById(existingAttendance.user_id); if (!User) { return { success: false, error: 'User not found', attendanceId }; } // Check if user is active if (User.isActive === false) { return { success: false, error: 'User account is deactivated. Cannot update attendance.', attendanceId }; } const oldStatus = existingAttendance.attendanceStatus; let finalNewStatus = newAttendanceStatus; let autoConverted = false; // If changing to a leave type or Absent, validate leave balance // Absent uses paid leaves first, then unpaid leaves if (['Paid Leave', 'Occasional off', 'Absent'].includes(newAttendanceStatus)) { const validation = await validateLeaveBalance(existingAttendance.user_id, newAttendanceStatus); if (!validation.isValid) { return { success: false, error: validation.error, requestedStatus: newAttendanceStatus, attendanceId }; } finalNewStatus = validation.modifiedStatus; autoConverted = validation.autoConverted || false; } // Step 1: Reverse the old leave balance changes if (['Paid Leave', 'Occasional off', 'Unpaid Leave'].includes(oldStatus)) { const reverseResult = await reverseLeaveBalance(existingAttendance.user_id, oldStatus, attendanceId); if (!reverseResult.success) { return { success: false, error: 'Failed to reverse old leave balance', details: reverseResult.error, attendanceId }; } } // Step 2: Apply new leave balance changes if (['Paid Leave', 'Occasional off', 'Unpaid Leave'].includes(finalNewStatus)) { const updateResult = await updateLeaveBalance(existingAttendance.user_id, finalNewStatus, attendanceId); if (!updateResult.success) { // If new balance update fails, try to restore old balance await updateLeaveBalance(existingAttendance.user_id, oldStatus, attendanceId); return { success: false, error: 'Failed to update new leave balance', details: updateResult.error, attendanceId }; } } // Step 3: Update the attendance record (including expected start/end times for configured days) const updateDoc = { attendanceStatus: finalNewStatus, updatedAt: new Date() }; if (['Paid Leave', 'Unpaid Leave', 'Occasional off', 'Day off', 'Absent'].includes(finalNewStatus)) { const dayBase = new Date(existingAttendance.createdAt); dayBase.setUTCHours(0, 0, 0, 0); try { if (User.start_time) { const checkIn = convertMakkahTimeStringToUTC(User.start_time, dayBase); updateDoc.createdAt = checkIn; } } catch (e) { console.warn('Failed to convert start_time for attendance update', { userId: String(User._id || ''), start_time: User.start_time, error: e?.message, }); } try { if (User.end_time) { const checkOut = convertMakkahTimeStringToUTC(User.end_time, dayBase); updateDoc.checkOut = checkOut; } } catch (e) { console.warn('Failed to convert end_time for attendance update', { userId: String(User._id || ''), end_time: User.end_time, error: e?.message, }); } } const updatedAttendance = await attendance.findByIdAndUpdate( attendanceId, updateDoc, { new: true } ); return { success: true, attendanceId, oldStatus, newStatus: finalNewStatus, attendanceRecord: updatedAttendance, autoConverted, balanceChanges: { oldStatusReversed: ['Paid Leave', 'Occasional off', 'Unpaid Leave'].includes(oldStatus), newStatusApplied: ['Paid Leave', 'Occasional off', 'Unpaid Leave'].includes(finalNewStatus) } }; } catch (err) { console.error('Single attendance update error:', err); return { success: false, error: 'Failed to update attendance', details: err.message, attendanceId }; } }; // Admin-only function to update existing attendance records (single or multiple) export const updateAttendance = async (req, res, next) => { try { const { newAttendanceStatus, attendanceIds } = req.body; // Validate attendanceIds array if (!attendanceIds || !Array.isArray(attendanceIds) || attendanceIds.length === 0) { return res.status(400).json({ error: 'attendanceIds array is required and must contain at least one ID' }); } // Validate required fields if (!newAttendanceStatus) { return res.status(400).json({ error: 'newAttendanceStatus is required' }); } const results = []; const errors = []; let successCount = 0; // Process each attendance ID for (const id of attendanceIds) { const result = await updateSingleAttendance(id, newAttendanceStatus); if (result.success) { results.push(result); successCount++; // Log successful update console.log(`Attendance updated by admin: ${id} from ${result.oldStatus} to ${result.newStatus}`); // Invalidate cache for this user if (result.attendanceRecord?.user_id) { await invalidateCache.attendance(result.attendanceRecord.user_id); } } else { errors.push(result); } } // Prepare response based on results const responseData = { totalRequested: attendanceIds.length, successCount, errorCount: errors.length, results: results.map(r => ({ attendanceId: r.attendanceId, oldStatus: r.oldStatus, newStatus: r.newStatus, autoConverted: r.autoConverted, balanceChanges: r.balanceChanges })), errors: errors.length > 0 ? errors : undefined }; // Add overall message if (successCount === attendanceIds.length) { responseData.message = attendanceIds.length === 1 ? "Attendance updated successfully" : `All ${successCount} attendance records updated successfully`; } else if (successCount > 0) { responseData.message = `${successCount} of ${attendanceIds.length} attendance records updated successfully`; } else { responseData.message = "No attendance records were updated"; } // Add auto-conversion notices const autoConvertedCount = results.filter(r => r.autoConverted).length; const markedAbsentCount = results.filter(r => r.autoConverted && r.newStatus === 'Absent').length; if (autoConvertedCount > 0) { responseData.notice = markedAbsentCount > 0 ? `${markedAbsentCount} record(s) marked as absent (paid leave balance exhausted).` : `${autoConvertedCount} record(s) applied as paid leave based on balance.`; } // Set appropriate status code const statusCode = errors.length === 0 ? 200 : (successCount > 0 ? 207 : 400); res.status(statusCode).json(responseData); } catch (err) { console.error('Update attendance error:', err); res.status(500).json({ error: 'Failed to update attendance', details: err.message }); } }; // Mass update attendance for company holiday (Occasional off) across all employees export const massUpdateHoliday = async (req, res, next) => { try { const { dates } = req.body; if (!dates || !Array.isArray(dates) || dates.length === 0) { return res.status(400).json({ error: 'dates array is required and must contain at least one YYYY-MM-DD value' }); } const uniqueDates = Array.from(new Set(dates)); // Only process active employees (isActive: true or undefined for backward compatibility) const employees = await user.find({ role: { $nin: ['admin', 'super_admin'] }, $or: [ { isActive: true }, { isActive: { $exists: false } } ] }).select('_id first_name last_name department'); if (!employees.length) { return res.status(404).json({ error: 'No employees found to update' }); } const summary = { totalEmployees: employees.length, totalDates: uniqueDates.length, updatedExisting: 0, createdNew: 0, skipped: 0, errors: [] }; const affectedUserIds = new Set(); for (const dateStr of uniqueDates) { const dayStart = moment.utc(dateStr, 'YYYY-MM-DD', true); if (!dayStart.isValid()) { summary.errors.push({ date: dateStr, error: 'Invalid date format. Expected YYYY-MM-DD' }); summary.skipped += employees.length; continue; } const startOfDay = dayStart.clone().startOf('day'); const endOfDay = dayStart.clone().endOf('day').set('millisecond', 999); for (const employee of employees) { try { const existingAttendance = await attendance.findOne({ user_id: employee._id, createdAt: { $gte: startOfDay.toDate(), $lte: endOfDay.toDate() } }); if (existingAttendance) { const result = await updateSingleAttendance(existingAttendance._id, 'Occasional off'); if (result.success) { summary.updatedExisting++; affectedUserIds.add(existingAttendance.user_id.toString()); } else { summary.errors.push({ date: dateStr, userId: employee._id, error: result.error || 'Failed to update attendance record' }); } } else { const employeeName = `${employee.first_name || ''} ${employee.last_name || ''}`.trim() || employee.first_name || 'Employee'; const newAttendance = new attendance({ _id: new mongoose.Types.ObjectId(), user_id: employee._id, name: employeeName, attendanceStatus: 'Occasional off', department: employee.department?._id || employee.department || null, app_version: 'system', createdAt: startOfDay.toDate(), }); const savedAttendance = await newAttendance.save(); const leaveUpdate = await updateLeaveBalance(employee._id, 'Occasional off', savedAttendance._id); if (!leaveUpdate.success) { summary.errors.push({ date: dateStr, userId: employee._id, error: leaveUpdate.error || 'Failed to update leave balance' }); } summary.createdNew++; affectedUserIds.add(employee._id.toString()); } } catch (err) { summary.errors.push({ date: dateStr, userId: employee._id, error: err.message || 'Unexpected error processing employee' }); } } } if (affectedUserIds.size > 0) { await invalidateCache.attendanceBatch(Array.from(affectedUserIds)); } const totalSuccess = summary.updatedExisting + summary.createdNew; const errorResults = summary.errors.slice(0, 25).map((err) => ({ attendanceId: `${err.userId || 'unknown'}-${err.date}`, success: false, error: `${err.date}: ${err.error}` })); const responseData = { message: `Holiday applied to ${totalSuccess} record(s) across ${summary.totalDates} date(s)`, success: totalSuccess, failed: summary.errors.length, totalEmployees: summary.totalEmployees, totalDates: summary.totalDates, updatedExisting: summary.updatedExisting, createdNew: summary.createdNew, skipped: summary.skipped, results: errorResults, errors: summary.errors }; const statusCode = summary.errors.length === 0 ? 200 : (totalSuccess > 0 ? 207 : 400); res.status(statusCode).json(responseData); console.log(`Mass holiday update completed: ${totalSuccess} successful changes, ${summary.errors.length} errors`); } catch (err) { console.error('Mass holiday update error:', err); res.status(500).json({ error: 'Failed to process mass holiday update', details: err.message }); } }; // export const markAttendance = async (req, res, next) => { // let email = ""; // const userId = req.body.user_id; // let AttendanceStatus; // const today = new Date(); // const dayOfWeek = today.getDay(); // const days = [ // "Sunday", // "Monday", // "Tuesday", // "Wednesday", // "Thursday", // "Friday", // "Saturday", // ]; // const currentDay = days[dayOfWeek]; // let hours = today.getHours(); // const minutes = today.getMinutes(); // const ampm = hours >= 12 ? "PM" : "AM"; // hours = hours % 12; // hours = hours ? hours : 12; // the hour '0' should be '12' // const formattedTime = `${hours}:${minutes // .toString() // .padStart(2, "0")} ${ampm}`; // console.log(`Current Time: ${formattedTime}`); // function convertTo24HourFormat(time) { // const [timePart, modifier] = time.split(" "); // let [hours, minutes] = timePart.split(":").map(Number); // if (modifier === "PM" && hours !== 12) { // hours += 12; // } else if (modifier === "AM" && hours === 12) { // hours = 0; // } // return new Date(0, 0, 0, hours, minutes); // year, month, day are irrelevant // } // try { // const existingAttendance = await attendance.findOne({ // user_id: userId, // createdAt: { $gte: today.setHours(0, 0, 0, 0) }, // }); // if (existingAttendance) { // return res.status(200).json({ // message: "Attendance already marked for today.", // }); // } else { // const User = await user.findOne({ // _id: userId, // }); // if (req.body.attendanceStatus !== " ") { // AttendanceStatus = req.body.attendanceStatus; // } else { // email = User.email; // for (const Days of User.time_table) { // if (currentDay === Days.day) { // console.log("Day: ", Days.day); // const time1 = formattedTime; // const time2 = Days.start_time; // const date1 = convertTo24HourFormat(time1); // const date2 = convertTo24HourFormat(time2); // const differenceInMillis = date1 - date2; // const differenceInMinutes = Math.floor(differenceInMillis / 60000); // console.log(`The difference is ${differenceInMinutes} minutes`); // const startTime = convertTo24HourFormat(Days.start_time); // const currentTime = new Date(); // Exact current time // const timeDifferenceInSeconds = differenceInMillis / 1000; // Difference in seconds // console.log( // `Time difference in seconds: ${timeDifferenceInSeconds}` // ); // if (currentTime > startTime) { // // If the current time is after the start time // if (timeDifferenceInSeconds > 600) { // console.log("Late"); // AttendanceStatus = "Late"; // } else { // console.log("Present"); // AttendanceStatus = "Present"; // } // } else { // // If the current time is before the start time // console.log("Present"); // AttendanceStatus = "Present"; // } // } // } // } // let attendanceTable = new attendance({ // _id: new mongoose.Types.ObjectId(), // user_id: userId, // name: req.body.name, // attendanceStatus: AttendanceStatus, // department: req.body.department, // app_version: req.body.app_version, // createdAt: req.body.createdAt === "" ? new Date() : req.body.createdAt, // }); // const result = await attendanceTable.save(); // res.status(200).json({ // message: "Attendance marked for today.", // attendanceTable: result, // }); // if (res.statusCode === 200) { // switch (AttendanceStatus) { // case "Present": // await sendEmail( // email, // presentTemplate( req.body.name,"Sohaib") // ); // break; // case "Late": // await sendEmail( // email, // lateTemplate( req.body.name,"Sohaib") // ); // break; // case "Absent": // await sendEmail( // email, // absentTemplate( req.body.name,"Sohaib") // ); // break; // default: // throw new Error("Unsupported Attendance Status"); // } // console.log("Attendance recorded"); // } // } // } catch (err) { // console.log(err); // res.status(500).json({ // error: err, // }); // } // }; export const getAttendance = async (req, res, next) => { console.log("User ID:", req.params.user_id); console.log("Query Params:", req.query); const queryConditions = { user_id: req.params.user_id, }; if (req.query.date) { // Use UTC to ensure consistent date boundaries const startDate = moment.utc(req.query.date, "YYYY-MM-DD").startOf("day"); const endDate = moment.utc(req.query.date, "YYYY-MM-DD").endOf("day"); console.log("Daily filter:", startDate.toDate(), endDate.toDate()); queryConditions.createdAt = { $gte: startDate.toDate(), $lte: endDate.toDate(), }; } else if (req.query.startDay && req.query.endDay) { const startDay = moment.utc(req.query.startDay, "YYYY-MM-DD").startOf("day"); const endDay = moment.utc(req.query.endDay, "YYYY-MM-DD").endOf("day").set('millisecond', 999); console.log("Date range filter:", startDay.toDate(), endDay.toDate()); queryConditions.createdAt = { $gte: startDay.toDate(), $lte: endDay.toDate(), }; } else if (req.query.month) { const startMonth = moment.utc(req.query.month, "YYYY-MM").startOf("month"); const endMonth = moment.utc(req.query.month, "YYYY-MM").endOf("month").set('millisecond', 999); console.log("Monthly filter:", startMonth.toDate(), endMonth.toDate()); queryConditions.createdAt = { $gte: startMonth.toDate(), $lte: endMonth.toDate(), }; } else if (req.query.year) { const startYear = moment.utc(req.query.year, "YYYY").startOf("year"); const endYear = moment.utc(req.query.year, "YYYY").endOf("year").set('millisecond', 999); console.log("Yearly filter:", startYear.toDate(), endYear.toDate()); queryConditions.createdAt = { $gte: startYear.toDate(), $lte: endYear.toDate(), }; } else if (req.query.week) { const startWeek = moment.utc(req.query.week, "YYYY-WW").startOf("isoWeek"); const endWeek = moment.utc(req.query.week, "YYYY-WW").endOf("isoWeek").set('millisecond', 999); console.log("Weekly filter:", startWeek.toDate(), endWeek.toDate()); queryConditions.createdAt = { $gte: startWeek.toDate(), $lte: endWeek.toDate(), }; } // Build a cache key using helper const cacheKey = cacheHelper.buildKey('attendance', req.params.user_id, req.query); try { // Ensure Redis is connected (non-blocking app even if it fails) try { await connectRedis(); } catch (err) { console.warn("Redis connection error:", err); } // Try cache first using helper const cached = await cacheHelper.get(cacheKey); if (cached) { console.log("Cache HIT for attendance"); return res.status(200).json(cached); } console.log("Cache MISS for attendance"); const result = await attendance .find(queryConditions) .select("user_id name attendanceStatus checkOut totalWorkingHours activityPercentage breakDuration breakSessions department createdAt updatedAt app_version") .exec(); const responsePayload = { attendance: result }; // Populate cache with TTL (1 hour) using helper await cacheHelper.set(cacheKey, responsePayload, 3600); res.status(200).json(responsePayload); } catch (err) { console.error(err); res.status(500).json({ error: err, }); } }; export const deleteAttendanceByDate = async (req, res) => { try { const userId = req.params.user_id; const dateStr = req.query.date; if (!dateStr || typeof dateStr !== 'string') { return res.status(400).json({ error: 'date query param is required (YYYY-MM-DD)' }); } const dayStart = moment.utc(dateStr, 'YYYY-MM-DD', true); if (!dayStart.isValid()) { return res.status(400).json({ error: 'Invalid date format. Expected YYYY-MM-DD' }); } const startOfDay = dayStart.clone().startOf('day'); const endOfDay = dayStart.clone().endOf('day').set('millisecond', 999); const existingAttendance = await attendance.findOne({ user_id: userId, createdAt: { $gte: startOfDay.toDate(), $lte: endOfDay.toDate() }, }); if (!existingAttendance) { return res.status(204).send(); } const oldStatus = existingAttendance.attendanceStatus; const attendanceId = existingAttendance._id; if (['Paid Leave', 'Occasional off', 'Unpaid Leave'].includes(oldStatus)) { const reverseResult = await reverseLeaveBalance(userId, oldStatus, attendanceId); if (!reverseResult.success) { return res.status(500).json({ error: 'Failed to reverse leave balance before deleting attendance', details: reverseResult.error, }); } } await attendance.deleteOne({ _id: attendanceId }); await invalidateCache.attendance(userId); return res.status(200).json({ message: 'Attendance deleted successfully', userId, date: dateStr, attendanceId: String(attendanceId), }); } catch (err) { console.error('Delete attendance by date error:', err); return res.status(500).json({ error: 'Failed to delete attendance', details: err.message, }); } }; export const attendanceCheckout = async (req, res) => { try { const userId = req.body.user_id; const checkOut = new Date(); if (!userId) { return res.status(400).json({ error: 'User ID is required' }); } // Check if user is active const checkoutUser = await user.findById(userId); if (!checkoutUser) { return res.status(404).json({ error: 'User not found' }); } if (checkoutUser.isActive === false) { return res.status(403).json({ error: 'User account is deactivated. Cannot checkout.' }); } // Use UTC day boundaries for consistent "today" check const todayStart = new Date(); todayStart.setUTCHours(0, 0, 0, 0); const todayEnd = new Date(); todayEnd.setUTCHours(23, 59, 59, 999); const todayAttendance = await attendance.findOne({ user_id: userId, createdAt: { $gte: todayStart, $lte: todayEnd } }); if (!todayAttendance) { return res.status(404).json({ error: 'No attendance record found for today. Please mark attendance first.' }); } // Check if already checked out if (todayAttendance.checkOut) { return res.status(400).json({ error: 'Already checked out for today.', checkOutTime: todayAttendance.checkOut }); } // AUTO-CLOSE ANY ACTIVE BREAKS BEFORE CHECKOUT let autoClosedBreak = false; if (todayAttendance.breakSessions && todayAttendance.breakSessions.length > 0) { const activeBreakIndex = todayAttendance.breakSessions.findIndex( session => session.endTime === null || session.endTime === undefined ); if (activeBreakIndex !== -1) { const breakStart = new Date(todayAttendance.breakSessions[activeBreakIndex].startTime); const breakDurationMs = checkOut - breakStart; const breakDurationSeconds = Math.floor(breakDurationMs / 1000); todayAttendance.breakSessions[activeBreakIndex].endTime = checkOut; todayAttendance.breakSessions[activeBreakIndex].duration = breakDurationSeconds; // Recalculate total break duration const totalBreakDuration = todayAttendance.breakSessions.reduce((total, session) => { return total + (session.duration || 0); }, 0); todayAttendance.breakDuration = totalBreakDuration; autoClosedBreak = true; console.log(`Auto-closed active break for user ${userId} during checkout`); // Reset UserStatus to neutral await UserStatus.findOneAndUpdate( { user_id: userId }, { currentStatus: 'neutral', lastUpdated: checkOut }, { upsert: true } ); } } let totalWorkingHours = null; if (todayAttendance.createdAt && checkOut) { const checkInTime = new Date(todayAttendance.createdAt); const diffInMs = checkOut - checkInTime; const diffInSeconds = Math.floor(diffInMs / 1000); // Store as total seconds totalWorkingHours = diffInSeconds; } const userObjectId = mongoose.Types.ObjectId.isValid(userId) ? new mongoose.Types.ObjectId(userId) : userId; // Query with BOTH string and ObjectId to handle legacy data const todayActivities = await eventActivity.find({ $or: [ { user_id: userObjectId }, // ObjectId match { user_id: String(userId) } // String match ], createdAt: { $gte: todayStart, $lte: todayEnd } }).lean(); let activityPercentage = 0; if (todayActivities.length > 0) { const activeCount = todayActivities.filter( (a) => a.keystroke === true || a.mouseclick === true ).length; activityPercentage = Math.round((activeCount / todayActivities.length) * 100); } const updateData = { checkOut: checkOut, totalWorkingHours: totalWorkingHours, activityPercentage, updatedAt: new Date() }; // Include break session updates if auto-closed if (autoClosedBreak) { updateData.breakSessions = todayAttendance.breakSessions; updateData.breakDuration = todayAttendance.breakDuration; } const finalUpdatedAttendance = await attendance.findByIdAndUpdate( todayAttendance._id, updateData, { new: true } ); // Invalidate attendance cache for this user await invalidateCache.attendance(userId); res.status(200).json({ message: autoClosedBreak ? 'Checkout successful (active break auto-closed)' : 'Checkout successful', attendance: finalUpdatedAttendance, totalWorkingHours: totalWorkingHours, checkInTime: finalUpdatedAttendance.createdAt, checkOutTime: finalUpdatedAttendance.checkOut, autoClosedBreak: autoClosedBreak }); // Send hierarchical checkout notification - clean one-liner with error handling if (res.statusCode === 200) { const checkoutUser = await user.findById(userId); if (checkoutUser && ['employee', 'department_head', 'admin', 'super_admin'].includes(checkoutUser.role)) { sendCheckoutNotification(checkoutUser, totalWorkingHours) .then(result => { if (result.success) { console.log(`Checkout notification sent for ${checkoutUser.first_name} ${checkoutUser.last_name} to ${result.totalRecipients} recipients`); } else { console.error('Checkout notification failed:', result.error); } }) .catch(error => console.error('Checkout notification error:', error)); } } } catch (err) { console.error('Checkout error:', err); res.status(500).json({ error: 'Internal server error during checkout', details: err.message }); } }; export const sendEmail = async ( receiverEmail, emailTemplate ) => { const mailOptions = { from: process.env.SMTP_FROM, to: receiverEmail, subject: emailTemplate.subject, html: emailTemplate.body, }; try { const info = await transporter.sendMail(mailOptions); console.log("Message sent: %s", info.messageId); } catch (error) { console.error("Error occurred while sending email:", error); } }; // cron changed to run at 16 UTC 4pm, 9pm pst, 7pm ksa cron.schedule("0 16 * * *", async () => { console.log("Running attendance check job at 16:00 UTC"); const now = new Date(); // Use UTC to ensure consistent day checking across timezones const todayUTC = new Date(now); todayUTC.setUTCHours(0, 0, 0, 0); const endOfTodayUTC = new Date(todayUTC); endOfTodayUTC.setUTCHours(23, 59, 59, 999); // Get UTC day name (0 = Sunday, 1 = Monday, etc.) const dayIndex = now.getUTCDay(); const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const currentDayName = days[dayIndex]; console.log(`Today is: ${currentDayName} (UTC)`); try { // Only process active users (isActive: true or undefined for backward compatibility) const users = await user.find({ $or: [ { isActive: true }, { isActive: { $exists: false } } ] }); for (const user of users) { const userId = user._id; const name = user.first_name; const email = user.email; // Safely extract department ID - handle undefined, ObjectId, or populated object const department = user.department?._id || user.department || null; let AttendanceStatus = "Present"; // Default to Present if no conditions match // Check if today is a day off for this user (handle undefined day_off) if (user.day_off && Array.isArray(user.day_off) && user.day_off.includes(currentDayName)) { // Today is a day off for the user console.log(`${name} has a day off today (${currentDayName})`); // Check if attendance is already marked for today const existingAttendance = await attendance.findOne({ user_id: userId, createdAt: { $gte: todayUTC, $lte: endOfTodayUTC }, }); if (existingAttendance) { console.log(`Attendance already marked for user: ${name}`); continue; // Skip to the next user if attendance is already marked } else { // Attendance not yet marked, set as Day off AttendanceStatus = "Day off"; console.log(`Marked attendance as ${AttendanceStatus} for user: ${name}`); // Mark attendance as Day off const newAttendance = new attendance({ _id: new mongoose.Types.ObjectId(), user_id: userId, name: name, attendanceStatus: AttendanceStatus, department: department, app_version: "v1", createdAt: new Date(), // Use current date and time }); await newAttendance.save(); } } else { // Today is not a day off, check if attendance is marked const existingAttendance = await attendance.findOne({ user_id: userId, createdAt: { $gte: todayUTC, $lte: endOfTodayUTC }, }); if (existingAttendance) { console.log(`Attendance already marked for user: ${name}`); continue; // Skip to the next user if attendance is already marked } else { // Attendance not yet marked - Absent uses paid leaves first if available; otherwise marked as Absent (no unpaid leave) console.log(`${name} is absent today (${currentDayName})`); // Validate and convert Absent to Paid Leave or Absent based on balance const validation = await validateLeaveBalance(userId, 'Absent'); AttendanceStatus = validation.modifiedStatus; // Will be 'Paid Leave' or 'Absent' // Mark attendance with the converted status const newAttendance = new attendance({ _id: new mongoose.Types.ObjectId(), user_id: userId, name: name, attendanceStatus: AttendanceStatus, department: department, app_version: "v1", createdAt: new Date(), // Use current date and time }); await newAttendance.save(); // Update leave balance await updateLeaveBalance(userId, AttendanceStatus, newAttendance._id); // Send absent email await sendEmail( email, absentTemplate(name, "Sohaib") ); // await mailer(email, name, "Sohaib", AttendanceStatus); } } } } catch (error) { console.error("Error during the attendance check job:", error); } }, { timezone: "UTC" }); // cron.schedule("* * * * *", async () => { // console.log("Running attendance check job every day at 11:00 pm for testing"); // const now = new Date(); // const today = new Date(now); // today.setHours(0, 0, 0, 0); // Set to midnight // const options = { weekday: "long" }; // const currentDayName = now.toLocaleDateString("en-US", options); // console.log(`Today is: ${currentDayName}`); // try { // const users = await user.find(); // for (const user of users) { // const userId = user._id; // const name = user.first_name; // const email =user.email; // const department = user.department; // let AttendanceStatus; // for (const dayoff of user.day_off) { // // console.log(user.first_name); // // console.log(dayoff); // if (dayoff.toLowerCase() == currentDayName.toLowerCase()) { // AttendanceStatus = "Weekly off"; // console.log(user.first_name); // console.log(AttendanceStatus); // }else{console.log("not a weekly off now checking if employee is already present or not");} // } // const existingAttendance = await attendance.findOne({ // user_id: userId, // createdAt: { $gte: today }, // }); // if (existingAttendance) { // console.log(`Attendance already marked for user: ${name}`); // continue; // Skip to the next user if attendance is already marked // }else { // AttendanceStatus = "Absent"; // console.log(user.first_name); // console.log(AttendanceStatus); // } // const newAttendance = new attendance({ // _id: new mongoose.Types.ObjectId(), // user_id: userId, // name: name, // attendanceStatus: AttendanceStatus, // department: department, // app_version: "v1", // createdAt: new Date(), // Use current date and time // }); // console.log(newAttendance); // await newAttendance.save(); // console.log( // `Marked attendance as ${AttendanceStatus} for user: ${name} on ${today.toDateString()}` // ); // if (AttendanceStatus = "Absent") { // console.log(AttendanceStatus); // await sendEmail( // email, // absentTemplate( name,"Sohaib") // ); // await mailer(email,name,"Sohaib", AttendanceStatus); // }else{ // console.log(AttendanceStatus); // } // } // } catch (error) { // console.error("Error during the attendance check job:", error); // } // }); console.log("Cron job scheduled for attendance check every minute."); // export const markAttendance =async (req, res, next) => { // let email= ""; // const userId = req.body.user_id; // let AttendanceStatus; // const today = new Date(); // const dayOfWeek = today.getDay(); // const days = [ // "Sunday", // "Monday", // "Tuesday", // "Wednesday", // "Thursday", // "Friday", // "Saturday", // ]; // const currentDay = days[dayOfWeek]; // let hours = today.getHours(); // const minutes = today.getMinutes(); // const ampm = hours >= 12 ? "PM" : "AM"; // hours = hours % 12; // hours = hours ? hours : 12; // the hour '0' should be '12' // function convertTimeStringToDate(timeString) { // const [time, modifier] = timeString.split(" "); // let [hours, minutes] = time.split(":"); // hours = parseInt(hours, 10); // minutes = parseInt(minutes, 10); // if (modifier === "PM" && hours !== 12) { // hours += 12; // } // if (modifier === "AM" && hours === 12) { // hours = 0; // } // return new Date( // today.getFullYear(), // today.getMonth(), // today.getDate(), // hours, // minutes // ); // } // const formattedTime = `${hours}:${minutes // .toString() // .padStart(2, "0")} ${ampm}`; // console.log(`Current Time: ${formattedTime}`); // try { // const existingAttendance = await attendance.findOne({ // user_id: userId, // createdAt: { $gte: today.setHours(0, 0, 0, 0) }, // }); // if (existingAttendance) { // return res.status(200).json({ // message: "Attendance already marked for today.", // }); // } else { // const User = await user.findOne({ // _id: userId, // }); // if (req.body.attendanceStatus != " ") { // AttendanceStatus = req.body.attendanceStatus; // } else { // email= User.email; // for (const Days of User.time_table) { // if (currentDay === Days.day) { // console.log("Day: ", Days.day); // console.log("Start Time: ", Days.start_time); // console.log("Current Time: ", formattedTime); // const startTime = convertTimeStringToDate(Days.start_time); // const currentTime = new Date(); // Exact current time // const timeDifferenceInSeconds = (currentTime - startTime) / 1000; // Difference in seconds // console.log( // `Time difference in seconds: ${timeDifferenceInSeconds}` // ); // if (currentTime > startTime) { // // If the current time is after the start time // if (timeDifferenceInSeconds > 600) { // console.log("Late"); // } else { // console.log("Present"); // } // } else { // // If the current time is before the start time // console.log("Present"); // } // if (timeDifferenceInSeconds > 600) { // // More than 10 minutes // AttendanceStatus = "Late"; // console.log("late"); // } else { // AttendanceStatus = "Present"; // console.log("on time"); // } // } // } // } // let attendanceTable = new attendance({ // _id: new mongoose.Types.ObjectId(), // user_id: userId, // name: req.body.name, // attendanceStatus: AttendanceStatus, // department: req.body.department, // app_version: req.body.app_version, // createdAt: req.body.createdAt === "" ? new Date() : req.body.createdAt, // }); // const result = await attendanceTable.save(); // res.status(200).json({ // message: "Attendance marked for today.", // attendanceTable: result, // }); // console.log("asdhajkshdkjsadbsj"); // await mailer(email,req.body.name,"Sohaib", AttendanceStatus); // } // } catch (err) { // console.log(err); // res.status(500).json({ // error: err, // }); // } // }; export const startBreak = async (req, res) => { try { const userId = req.body.user_id; const startTime = new Date(); if (!userId) { return res.status(400).json({ error: 'User ID is required' }); } // Check if user is active const breakUser = await user.findById(userId); if (!breakUser) { return res.status(404).json({ error: 'User not found' }); } if (breakUser.isActive === false) { return res.status(403).json({ error: 'User account is deactivated. Cannot start break.' }); } // Use UTC day boundaries const todayStart = new Date(); todayStart.setUTCHours(0, 0, 0, 0); const todayEnd = new Date(); todayEnd.setUTCHours(23, 59, 59, 999); console.log('startDay', todayStart); console.log('endDay', todayEnd); // Check if attendance record exists for today const todayAttendance = await attendance.findOne({ user_id: userId, createdAt: { $gte: todayStart, $lte: todayEnd } }); console.log('==================') console.log(todayAttendance) if (!todayAttendance) { return res.status(404).json({ error: 'No attendance record found for today. Please mark attendance first.' }); } // Check if already checked out if (todayAttendance.checkOut) { return res.status(400).json({ error: 'Cannot start break after checkout.' }); } // CLEANUP ORPHANED BREAKS (older than 8 hours with no end time) const MAX_BREAK_HOURS = 8; let cleanedOrphanedBreaks = false; if (todayAttendance.breakSessions && todayAttendance.breakSessions.length > 0) { todayAttendance.breakSessions.forEach((session, index) => { if (session.endTime === null || session.endTime === undefined) { const breakAge = (startTime - new Date(session.startTime)) / (1000 * 60 * 60); // hours if (breakAge > MAX_BREAK_HOURS) { // Auto-close orphaned break with max duration cap const maxDuration = MAX_BREAK_HOURS * 60 * 60; // 8 hours in seconds session.endTime = new Date(new Date(session.startTime).getTime() + (maxDuration * 1000)); session.duration = maxDuration; cleanedOrphanedBreaks = true; console.log(`Cleaned orphaned break for user ${userId} from ${session.startTime}`); } } }); } // Check for active break session in attendance record (source of truth) const activeBreak = todayAttendance.breakSessions?.find( session => session.endTime === null || session.endTime === undefined ); if (activeBreak) { return res.status(400).json({ error: 'Break is already in progress. Please stop current break first.' }); } // Add new break session to attendance const newBreakSession = { startTime: startTime, endTime: null, duration: null }; const updateData = { $push: { breakSessions: newBreakSession }, updatedAt: new Date() }; // Include cleaned orphaned breaks if any if (cleanedOrphanedBreaks) { updateData.breakSessions = todayAttendance.breakSessions.concat([newBreakSession]); delete updateData.$push; const recalculatedTotal = todayAttendance.breakSessions.reduce((total, session) => { return total + (session.duration || 0); }, 0); updateData.breakDuration = recalculatedTotal; } const updatedAttendance = await attendance.findByIdAndUpdate( todayAttendance._id, updateData, { new: true } ); // Update UserStatus to 'break' - this will pause the statusUpdater await UserStatus.findOneAndUpdate( { user_id: userId }, { currentStatus: 'break', lastUpdated: new Date() }, { upsert: true, new: true } ); res.status(200).json({ message: 'Break started successfully', attendance: updatedAttendance, breakStartTime: startTime, userStatus: 'break' }); } catch (err) { console.error('Start break error:', err); res.status(500).json({ error: 'Internal server error during break start', details: err.message }); } }; export const stopBreak = async (req, res) => { try { const userId = req.body.user_id; const endTime = new Date(); if (!userId) { return res.status(400).json({ error: 'User ID is required' }); } // Check if user is active const breakUser = await user.findById(userId); if (!breakUser) { return res.status(404).json({ error: 'User not found' }); } if (breakUser.isActive === false) { return res.status(403).json({ error: 'User account is deactivated. Cannot stop break.' }); } // Use UTC day boundaries const todayStart = new Date(); todayStart.setUTCHours(0, 0, 0, 0); const todayEnd = new Date(); todayEnd.setUTCHours(23, 59, 59, 999); const todayAttendance = await attendance.findOne({ user_id: userId, createdAt: { $gte: todayStart, $lte: todayEnd } }); if (!todayAttendance) { return res.status(404).json({ error: 'No attendance record found for today. Please mark attendance first.' }); } // Find the active break session (source of truth) const activeBreakIndex = todayAttendance.breakSessions?.findIndex( session => session.endTime === null || session.endTime === undefined ) ?? -1; if (activeBreakIndex === -1) { return res.status(400).json({ error: 'No active break session found. Please start a break first.' }); } // Calculate break duration in seconds const breakStart = new Date(todayAttendance.breakSessions[activeBreakIndex].startTime); const breakDurationMs = endTime - breakStart; let breakDurationSeconds = Math.floor(breakDurationMs / 1000); // Validate break duration is not negative (clock skew protection) if (breakDurationSeconds < 0) { console.warn(`Negative break duration detected for user ${userId}. Resetting to 0.`); breakDurationSeconds = 0; } // Cap individual break at maximum duration (8 hours) const MAX_SINGLE_BREAK_SECONDS = 8 * 60 * 60; if (breakDurationSeconds > MAX_SINGLE_BREAK_SECONDS) { console.warn(`Break duration exceeds maximum for user ${userId}. Capping at ${MAX_SINGLE_BREAK_SECONDS}s.`); breakDurationSeconds = MAX_SINGLE_BREAK_SECONDS; } // Update the break session todayAttendance.breakSessions[activeBreakIndex].endTime = endTime; todayAttendance.breakSessions[activeBreakIndex].duration = breakDurationSeconds; // Calculate total break duration in seconds const totalBreakDuration = todayAttendance.breakSessions.reduce((total, session) => { return total + (session.duration || 0); }, 0); const updatedAttendance = await attendance.findByIdAndUpdate( todayAttendance._id, { breakSessions: todayAttendance.breakSessions, breakDuration: totalBreakDuration, updatedAt: new Date() }, { new: true } ); // Reset UserStatus to 'neutral' so statusUpdater can resume normal operation await UserStatus.findOneAndUpdate( { user_id: userId }, { currentStatus: 'neutral', lastUpdated: new Date() }, { upsert: true, new: true } ); res.status(200).json({ message: 'Break stopped successfully', attendance: updatedAttendance, breakEndTime: endTime, breakDuration: breakDurationSeconds, totalBreakDuration: totalBreakDuration, userStatus: 'neutral' }); } catch (err) { console.error('Stop break error:', err); res.status(500).json({ error: 'Internal server error during break stop', details: err.message }); } }; // Manual trigger for late arrival check (for testing) export const triggerLateArrivalCheck = async (req, res) => { try { console.log('🧪 Manual late arrival check triggered by admin'); // Import and execute the check function const { checkLateArrivals } = await import('../services/lateArrivalChecker.js'); await checkLateArrivals(); res.status(200).json({ message: 'Late arrival check completed successfully', timestamp: new Date().toISOString() }); } catch (err) { console.error('Manual late arrival check error:', err); res.status(500).json({ error: 'Failed to run late arrival check', details: err.message }); } };