feat: add employee timing system, notification templates, and professor class plan sms
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// /components/employeeTimings/employeeTiming.test.js
|
||||
'use strict';
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
computeStatus,
|
||||
computeDurationMinutes,
|
||||
computeDurationFormatted
|
||||
} = require('./employeeTimingService');
|
||||
|
||||
describe('EmployeeTiming status and warning computations', () => {
|
||||
it('returns "complete" when both entry and exit times are present', () => {
|
||||
assert.equal(computeStatus('08:30', '17:00'), 'complete');
|
||||
assert.equal(computeStatus('09:00', '14:30'), 'complete');
|
||||
});
|
||||
|
||||
it('returns "missing_exit" when entry is present but exit is missing', () => {
|
||||
assert.equal(computeStatus('08:30', ''), 'missing_exit');
|
||||
assert.equal(computeStatus('08:30', null), 'missing_exit');
|
||||
assert.equal(computeStatus('08:30', undefined), 'missing_exit');
|
||||
assert.equal(computeStatus('08:30', ' '), 'missing_exit');
|
||||
});
|
||||
|
||||
it('returns "missing_entry" when exit is present but entry is missing', () => {
|
||||
assert.equal(computeStatus('', '17:00'), 'missing_entry');
|
||||
assert.equal(computeStatus(null, '17:00'), 'missing_entry');
|
||||
assert.equal(computeStatus(undefined, '17:00'), 'missing_entry');
|
||||
assert.equal(computeStatus(' ', '17:00'), 'missing_entry');
|
||||
});
|
||||
|
||||
it('returns "incomplete" when neither entry nor exit is present', () => {
|
||||
assert.equal(computeStatus('', ''), 'incomplete');
|
||||
assert.equal(computeStatus(null, null), 'incomplete');
|
||||
assert.equal(computeStatus(undefined, undefined), 'incomplete');
|
||||
});
|
||||
});
|
||||
|
||||
describe('EmployeeTiming duration computations', () => {
|
||||
it('calculates duration in minutes correctly for standard day hours', () => {
|
||||
assert.equal(computeDurationMinutes('08:00', '16:30'), 510);
|
||||
assert.equal(computeDurationMinutes('09:15', '10:45'), 90);
|
||||
});
|
||||
|
||||
it('handles shift crossing midnight', () => {
|
||||
assert.equal(computeDurationMinutes('22:00', '02:00'), 240);
|
||||
});
|
||||
|
||||
it('returns null for missing times', () => {
|
||||
assert.equal(computeDurationMinutes('', '17:00'), null);
|
||||
assert.equal(computeDurationMinutes('08:00', ''), null);
|
||||
assert.equal(computeDurationMinutes(null, null), null);
|
||||
});
|
||||
|
||||
it('formats duration in Persian string', () => {
|
||||
assert.equal(computeDurationFormatted(510), '8 ساعت و 30 دقیقه');
|
||||
assert.equal(computeDurationFormatted(120), '2 ساعت');
|
||||
assert.equal(computeDurationFormatted(45), '45 دقیقه');
|
||||
assert.equal(computeDurationFormatted(null), '—');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
// /components/employeeTimings/employeeTimingController.js
|
||||
'use strict';
|
||||
|
||||
const catchAsync = require('../../utils/catchAsync');
|
||||
const employeeTimingService = require('./employeeTimingService');
|
||||
const { successResponse } = require('../../utils/apiResponse');
|
||||
|
||||
exports.getAll = catchAsync(async (req, res) => {
|
||||
const result = await employeeTimingService.getAll(req.query);
|
||||
return successResponse(res, 200, 'Employee timing records retrieved', result.items, result.meta);
|
||||
});
|
||||
|
||||
exports.getSummary = catchAsync(async (req, res) => {
|
||||
const summary = await employeeTimingService.getSummary(req.query);
|
||||
return successResponse(res, 200, 'Employee timing summary retrieved', summary);
|
||||
});
|
||||
|
||||
exports.getOne = catchAsync(async (req, res) => {
|
||||
const record = await employeeTimingService.getOne(req.params.id);
|
||||
return successResponse(res, 200, 'Employee timing record retrieved', record);
|
||||
});
|
||||
|
||||
exports.create = catchAsync(async (req, res) => {
|
||||
const record = await employeeTimingService.create(req.body, req.user?._id);
|
||||
return successResponse(res, 201, 'Employee timing record created', record);
|
||||
});
|
||||
|
||||
exports.update = catchAsync(async (req, res) => {
|
||||
const record = await employeeTimingService.update(req.params.id, req.body, req.user?._id);
|
||||
return successResponse(res, 200, 'Employee timing record updated', record);
|
||||
});
|
||||
|
||||
exports.delete = catchAsync(async (req, res) => {
|
||||
const result = await employeeTimingService.remove(req.params.id);
|
||||
return successResponse(res, 200, 'Employee timing record deleted', result);
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
// /components/employeeTimings/employeeTimingModel.js
|
||||
'use strict';
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const employeeTimingSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
date: {
|
||||
type: Date,
|
||||
required: true,
|
||||
default: Date.now,
|
||||
index: true
|
||||
},
|
||||
entryTime: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
exitTime: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
note: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
createdBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
updatedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
}
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
});
|
||||
|
||||
/**
|
||||
* Status virtual property:
|
||||
* - 'complete': both entryTime and exitTime are provided
|
||||
* - 'missing_exit': entryTime provided, but exitTime missing (Warning)
|
||||
* - 'missing_entry': exitTime provided, but entryTime missing (Warning)
|
||||
* - 'incomplete': both entryTime and exitTime are missing (Warning)
|
||||
*/
|
||||
employeeTimingSchema.virtual('status').get(function () {
|
||||
const hasEntry = Boolean(this.entryTime && String(this.entryTime).trim());
|
||||
const hasExit = Boolean(this.exitTime && String(this.exitTime).trim());
|
||||
|
||||
if (hasEntry && hasExit) return 'complete';
|
||||
if (hasEntry && !hasExit) return 'missing_exit';
|
||||
if (!hasEntry && hasExit) return 'missing_entry';
|
||||
return 'incomplete';
|
||||
});
|
||||
|
||||
/**
|
||||
* Warning flag virtual property:
|
||||
* True if record is incomplete (missing entry, missing exit, or both)
|
||||
*/
|
||||
employeeTimingSchema.virtual('hasWarning').get(function () {
|
||||
const hasEntry = Boolean(this.entryTime && String(this.entryTime).trim());
|
||||
const hasExit = Boolean(this.exitTime && String(this.exitTime).trim());
|
||||
return !(hasEntry && hasExit);
|
||||
});
|
||||
|
||||
/**
|
||||
* Warning message helper
|
||||
*/
|
||||
employeeTimingSchema.virtual('warningMessage').get(function () {
|
||||
const hasEntry = Boolean(this.entryTime && String(this.entryTime).trim());
|
||||
const hasExit = Boolean(this.exitTime && String(this.exitTime).trim());
|
||||
|
||||
if (hasEntry && !hasExit) return 'ورود ثبت شده ولی خروج ثبت نشده است';
|
||||
if (!hasEntry && hasExit) return 'خروج ثبت شده ولی ورود ثبت نشده است';
|
||||
if (!hasEntry && !hasExit) return 'زمان ورود و خروج هیچکدام ثبت نشده است';
|
||||
return null;
|
||||
});
|
||||
|
||||
/**
|
||||
* Duration in minutes calculation (if both entry and exit times are valid HH:mm)
|
||||
*/
|
||||
employeeTimingSchema.virtual('durationMinutes').get(function () {
|
||||
if (!this.entryTime || !this.exitTime) return null;
|
||||
const [inH, inM] = String(this.entryTime).split(':').map(Number);
|
||||
const [outH, outM] = String(this.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) {
|
||||
// Crosses midnight
|
||||
endMinutes += 24 * 60;
|
||||
}
|
||||
return endMinutes - startMinutes;
|
||||
});
|
||||
|
||||
/**
|
||||
* Formatted duration in Persian/hours
|
||||
*/
|
||||
employeeTimingSchema.virtual('durationFormatted').get(function () {
|
||||
const minutes = this.durationMinutes;
|
||||
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} دقیقه`;
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('EmployeeTiming', employeeTimingSchema);
|
||||
@@ -0,0 +1,21 @@
|
||||
// /components/employeeTimings/employeeTimingRoutes.js
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const employeeTimingController = require('./employeeTimingController');
|
||||
const authMiddleware = require('../../middlewares/authMiddleware');
|
||||
const perm = require('../../middlewares/permissionMiddleware');
|
||||
const { PERMISSIONS } = require('../../constants/permissions');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/admin/get-all', perm.requires(PERMISSIONS.EMPLOYEE_TIMINGS_READ), employeeTimingController.getAll);
|
||||
router.get('/admin/summary', perm.requires(PERMISSIONS.EMPLOYEE_TIMINGS_READ), employeeTimingController.getSummary);
|
||||
router.get('/admin/get-one/:id', perm.requires(PERMISSIONS.EMPLOYEE_TIMINGS_READ), employeeTimingController.getOne);
|
||||
router.post('/admin/create', perm.requires(PERMISSIONS.EMPLOYEE_TIMINGS_CREATE), employeeTimingController.create);
|
||||
router.put('/admin/update/:id', perm.requires(PERMISSIONS.EMPLOYEE_TIMINGS_UPDATE), employeeTimingController.update);
|
||||
router.delete('/admin/delete/:id', perm.requires(PERMISSIONS.EMPLOYEE_TIMINGS_DELETE), employeeTimingController.delete);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,327 @@
|
||||
// /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
|
||||
};
|
||||
Reference in New Issue
Block a user