// /components/employeeTimings/employeeTimingService.js 'use strict'; const EmployeeTiming = require('./employeeTimingModel'); const User = require('../users/userModel'); const AppError = require('../../utils/AppError'); const { calculateMeta, escapeRegex, getSearchTerm } = require('../../utils/pagination'); const normalizeTime = (timeStr) => { if (timeStr === undefined || timeStr === null) return ''; const trimmed = String(timeStr).trim(); if (!trimmed) return ''; // Check HH:mm format const match = /^([01]?[0-9]|2[0-3]):([0-5][0-9])$/.exec(trimmed); if (match) { const hh = match[1].padStart(2, '0'); const mm = match[2]; return `${hh}:${mm}`; } return trimmed; }; const normalizeDate = (rawDate) => { if (!rawDate) return new Date(); const d = new Date(rawDate); if (Number.isNaN(d.getTime())) { throw new AppError('VALIDATION_FAILED', { field: 'date' }, 'تاریخ نامعتبر است'); } return d; }; const sanitizeNote = (note) => { if (!note) return ''; return String(note).trim().slice(0, 1000); }; const computeStatus = (entryTime, exitTime) => { const hasEntry = Boolean(entryTime && String(entryTime).trim()); const hasExit = Boolean(exitTime && String(exitTime).trim()); if (hasEntry && hasExit) return 'complete'; if (hasEntry && !hasExit) return 'missing_exit'; if (!hasEntry && hasExit) return 'missing_entry'; return 'incomplete'; }; const computeDurationMinutes = (entryTime, exitTime) => { if (!entryTime || !exitTime) return null; const [inH, inM] = String(entryTime).split(':').map(Number); const [outH, outM] = String(exitTime).split(':').map(Number); if (Number.isNaN(inH) || Number.isNaN(inM) || Number.isNaN(outH) || Number.isNaN(outM)) { return null; } const startMinutes = inH * 60 + inM; let endMinutes = outH * 60 + outM; if (endMinutes < startMinutes) { endMinutes += 24 * 60; } return endMinutes - startMinutes; }; const computeDurationFormatted = (minutes) => { if (minutes === null || minutes === undefined) return '—'; const h = Math.floor(minutes / 60); const m = minutes % 60; if (h === 0) return `${m} دقیقه`; if (m === 0) return `${h} ساعت`; return `${h} ساعت و ${m} دقیقه`; }; const enrichRecord = (record) => { const obj = record.toObject ? record.toObject({ virtuals: true }) : { ...record }; const entryTime = obj.entryTime || ''; const exitTime = obj.exitTime || ''; const status = computeStatus(entryTime, exitTime); const hasWarning = status !== 'complete'; const durationMinutes = computeDurationMinutes(entryTime, exitTime); const durationFormatted = computeDurationFormatted(durationMinutes); let warningMessage = null; if (status === 'missing_exit') warningMessage = 'ورود ثبت شده ولی خروج ثبت نشده است'; else if (status === 'missing_entry') warningMessage = 'خروج ثبت شده ولی ورود ثبت نشده است'; else if (status === 'incomplete') warningMessage = 'زمان ورود و خروج هیچ‌کدام ثبت نشده است'; return { ...obj, status, hasWarning, warningMessage, durationMinutes, durationFormatted }; }; const getAll = async (query = {}) => { const page = parseInt(query.page, 10) || 1; const limit = Math.min(parseInt(query.limit, 10) || 20, 200); const skip = (page - 1) * limit; const filter = {}; if (query.user || query.userId) { filter.user = query.user || query.userId; } // Date range filter if (query.startDate || query.endDate) { filter.date = {}; if (query.startDate) { const start = new Date(query.startDate); start.setHours(0, 0, 0, 0); filter.date.$gte = start; } if (query.endDate) { const end = new Date(query.endDate); end.setHours(23, 59, 59, 999); filter.date.$lte = end; } } // Search by user name / username / nationalIdCode const searchTerm = getSearchTerm(query); if (searchTerm) { const escaped = escapeRegex(searchTerm); const matchingUsers = await User.find({ $or: [ { name: new RegExp(escaped, 'i') }, { username: new RegExp(escaped, 'i') }, { nationalIdCode: new RegExp(escaped, 'i') }, { phoneNumber: new RegExp(escaped, 'i') } ] }).select('_id').lean(); const userIds = matchingUsers.map(u => u._id); if (filter.user) { if (!userIds.some(id => String(id) === String(filter.user))) { return { items: [], meta: calculateMeta(0, page, limit) }; } } else { filter.user = { $in: userIds }; } } // Filter by status (complete, incomplete, missing_exit, missing_entry) if (query.status) { const s = String(query.status).trim(); if (s === 'complete') { filter.entryTime = { $nin: ['', null] }; filter.exitTime = { $nin: ['', null] }; } else if (s === 'missing_exit') { filter.entryTime = { $nin: ['', null] }; filter.$or = [{ exitTime: '' }, { exitTime: null }, { exitTime: { $exists: false } }]; } else if (s === 'missing_entry') { filter.exitTime = { $nin: ['', null] }; filter.$or = [{ entryTime: '' }, { entryTime: null }, { entryTime: { $exists: false } }]; } else if (s === 'incomplete') { filter.$or = [ { entryTime: '' }, { entryTime: null }, { entryTime: { $exists: false } }, { exitTime: '' }, { exitTime: null }, { exitTime: { $exists: false } } ]; } } const [records, total] = await Promise.all([ EmployeeTiming.find(filter) .populate('user', 'name nationalIdCode phoneNumber username role uniqueCode') .populate('createdBy', 'name username') .populate('updatedBy', 'name username') .sort({ date: -1, createdAt: -1 }) .skip(skip) .limit(limit), EmployeeTiming.countDocuments(filter) ]); return { items: records.map(enrichRecord), meta: calculateMeta(total, page, limit) }; }; const getSummary = async (query = {}) => { const baseFilter = {}; if (query.user || query.userId) { baseFilter.user = query.user || query.userId; } if (query.startDate || query.endDate) { baseFilter.date = {}; if (query.startDate) { const start = new Date(query.startDate); start.setHours(0, 0, 0, 0); baseFilter.date.$gte = start; } if (query.endDate) { const end = new Date(query.endDate); end.setHours(23, 59, 59, 999); baseFilter.date.$lte = end; } } const [total, complete, missingExit, missingEntry] = await Promise.all([ EmployeeTiming.countDocuments(baseFilter), EmployeeTiming.countDocuments({ ...baseFilter, entryTime: { $nin: ['', null] }, exitTime: { $nin: ['', null] } }), EmployeeTiming.countDocuments({ ...baseFilter, entryTime: { $nin: ['', null] }, $or: [{ exitTime: '' }, { exitTime: null }, { exitTime: { $exists: false } }] }), EmployeeTiming.countDocuments({ ...baseFilter, exitTime: { $nin: ['', null] }, $or: [{ entryTime: '' }, { entryTime: null }, { entryTime: { $exists: false } }] }) ]); const incomplete = total - complete; return { total, complete, incomplete, missingExit, missingEntry }; }; const getOne = async (id) => { const record = await EmployeeTiming.findById(id) .populate('user', 'name nationalIdCode phoneNumber username role uniqueCode') .populate('createdBy', 'name username') .populate('updatedBy', 'name username'); if (!record) { throw new AppError('NOT_FOUND', { resource: 'EmployeeTiming' }, 'رکورد تردد یافت نشد'); } return enrichRecord(record); }; const create = async (body, authorId) => { if (!body.user && !body.userId) { throw new AppError('VALIDATION_FAILED', { field: 'user' }, 'انتخاب کارمند / کاربر الزامی است'); } const userId = body.user || body.userId; const userExists = await User.findById(userId).select('_id name').lean(); if (!userExists) { throw new AppError('NOT_FOUND', { resource: 'User' }, 'کاربر انتخاب‌شده یافت نشد'); } const record = await EmployeeTiming.create({ user: userId, date: normalizeDate(body.date), entryTime: normalizeTime(body.entryTime), exitTime: normalizeTime(body.exitTime), note: sanitizeNote(body.note), createdBy: authorId || undefined, updatedBy: authorId || undefined }); return getOne(record._id); }; const update = async (id, body, authorId) => { const record = await EmployeeTiming.findById(id); if (!record) { throw new AppError('NOT_FOUND', { resource: 'EmployeeTiming' }, 'رکورد تردد یافت نشد'); } if (body.user !== undefined || body.userId !== undefined) { const targetUserId = body.user || body.userId; const userExists = await User.findById(targetUserId).select('_id').lean(); if (!userExists) { throw new AppError('NOT_FOUND', { resource: 'User' }, 'کاربر انتخاب‌شده یافت نشد'); } record.user = targetUserId; } if (body.date !== undefined) { record.date = normalizeDate(body.date); } if (body.entryTime !== undefined) { record.entryTime = normalizeTime(body.entryTime); } if (body.exitTime !== undefined) { record.exitTime = normalizeTime(body.exitTime); } if (body.note !== undefined) { record.note = sanitizeNote(body.note); } if (authorId) { record.updatedBy = authorId; } await record.save(); return getOne(record._id); }; const remove = async (id) => { const record = await EmployeeTiming.findByIdAndDelete(id); if (!record) { throw new AppError('NOT_FOUND', { resource: 'EmployeeTiming' }, 'رکورد تردد یافت نشد'); } return { success: true }; }; module.exports = { getAll, getSummary, getOne, create, update, remove, computeStatus, computeDurationMinutes, computeDurationFormatted };