Files
absens-api/app/modules/absensi/services/absensi.service.js
2025-10-22 13:37:57 +07:00

471 lines
15 KiB
JavaScript

const response = require('../../../helpers/responses');
const db = require('../../../../models/migration');
const errorHandler = require('../../../middlewares/errorHandler')
const { sequelize, Op } = require('../../../../models/migration');
const { getDistance } = require('../../../helpers/distance');
const moment = require('moment-timezone')
const path = require("path");
const fs = require("fs");
const axios = require("axios");
const User = db.User
const Attedances = db.Attedances
const Branch = db.Branch
const saveUploadedFile = (file, folder = "public/uploads") => {
const dir = path.join(process.cwd(), folder);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const fileName = `${Date.now()}-${file.originalname}`;
const filePath = path.join(dir, fileName);
// file.buffer karena kita pakai memoryStorage
fs.writeFileSync(filePath, file.buffer);
// return URL yang bisa diakses dari frontend
return `/${folder.replace("public/", "")}/${fileName}`.replace(/\\/g, "/");
};
const create = async (req, res) => {
const t = await sequelize.transaction();
try {
const { type, lat, lng, reason } = req.body;
const user_id = req.user.id;
const now = moment().tz('Asia/Jakarta');
const today = now.format('YYYY-MM-DD');
const currentHour = parseInt(now.format('HH'));
const user = await User.findOne({ where: { id: user_id } });
if (!user) return response.failed(res, 404, 'User tidak ditemukan');
const branch = await Branch.findOne({ where: { id: user.branch_id } });
if (!branch) return response.failed(res, 404, 'Branch kantor tidak ditemukan');
let attendance = await Attedances.findOne({
where: { user_id, date: today },
});
if (attendance && ['sick', 'permission'].includes(attendance.type)) {
return response.failed(res, 400, `Hari ini Anda sudah absen ${attendance.type}`);
}
// === Jika izin (sakit / izin) ===
if (['sick', 'permission'].includes(type)) {
if (attendance) return response.failed(res, 400, 'Sudah ada absensi hari ini');
attendance = await Attedances.create({
user_id,
branch_id: user.branch_id,
name: user.name,
type,
reason,
date: today,
}, { transaction: t });
await t.commit();
return response.success(res, attendance, 'Izin berhasil disimpan');
}
if (attendance && attendance.clock_in) {
// 🕛 Jika branch tidak mengaktifkan absen siang
if (!branch.lunch_attendance) {
// Langsung lewati logika absen siang dan lanjut ke clock_out
if (currentHour >= 15 && !attendance.clock_out) {
attendance.clock_out = now.toDate();
const durationMs = moment(attendance.clock_out).diff(moment(attendance.clock_in));
const hours = Math.floor(durationMs / (1000 * 60 * 60));
const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
attendance.work_duration = `${hours} jam ${minutes} menit`;
await attendance.save({ transaction: t });
await t.commit();
return response.success(
res,
{
...attendance.toJSON(),
branch_name: branch.name,
clock_in: moment(attendance.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
clock_out: moment(attendance.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
work_duration: attendance.work_duration,
},
'Absen pulang berhasil'
);
}
if (currentHour < 15 && !attendance.clock_out) {
await t.rollback();
return response.failed(res, 400, 'Belum waktunya absen pulang');
}
} else {
// 🕛 Jika branch pakai absen siang
if (currentHour >= 12 && currentHour < 15 && !attendance.lunch_in) {
attendance.lunch_in = now.toDate();
await attendance.save({ transaction: t });
await t.commit();
return response.success(res, attendance, 'Absen masuk setelah makan siang berhasil');
}
if (currentHour < 12 && !attendance.lunch_in) {
await t.rollback();
return response.failed(res, 400, 'Belum waktunya absen setelah makan siang');
}
if (currentHour >= 15 && !attendance.clock_out) {
attendance.clock_out = now.toDate();
const durationMs = moment(attendance.clock_out).diff(moment(attendance.clock_in));
const hours = Math.floor(durationMs / (1000 * 60 * 60));
const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
attendance.work_duration = `${hours} jam ${minutes} menit`;
await attendance.save({ transaction: t });
await t.commit();
return response.success(
res,
{
...attendance.toJSON(),
branch_name: branch.name,
clock_in: moment(attendance.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
clock_out: moment(attendance.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
work_duration: attendance.work_duration,
},
'Absen pulang berhasil'
);
}
if (currentHour < 15 && attendance.lunch_in && !attendance.clock_out) {
await t.rollback();
return response.failed(res, 400, 'Belum waktunya absen pulang');
}
}
await t.rollback();
return response.failed(res, 400, 'Sudah melakukan absen hari ini');
}
// === Jika sudah absen masuk tapi belum pulang ===
// if (attendance && attendance.clock_in && !attendance.clock_out) {
// attendance.clock_out = now.toDate(); // waktu WIB
// const durationMs = moment(attendance.clock_out).diff(moment(attendance.clock_in));
// const hours = Math.floor(durationMs / (1000 * 60 * 60));
// const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
// attendance.work_duration = `${hours} jam ${minutes} menit`;
// await attendance.save({ transaction: t });
// await t.commit();
// return response.success(res, {
// ...attendance.toJSON(),
// clock_in: moment(attendance.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
// clock_out: attendance.clock_out
// ? moment(attendance.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss')
// : null
// }, 'Absen pulang berhasil');
// }
// === Jika belum absen sama sekali, cek lokasi ===
const distance = getDistance(branch.lat, branch.lng, lat, lng);
const allowedRadius = parseFloat(process.env.ABSENCE_RADIUS) || 100;
if (distance > allowedRadius) {
await t.rollback();
return response.failed(res, 400, `Lokasi di luar area kantor (${distance.toFixed(2)} meter)`);
}
let finalPhotoUrl = null;
if (req.file) {
// Jika upload file langsung
finalPhotoUrl = saveUploadedFile(req.file, "public/uploads/attendance_photos");
} else if (req.body.photo) {
// Jika kirim URL dari FE (misal kamera web base64 atau URL publik)
const imageUrl = req.body.photo;
const fileName = `${Date.now()}-${Math.random()
.toString(36)
.substring(7)}.jpg`;
const filePath = path.join("public/uploads/attendance_photos", fileName);
const responseImg = await axios({
url: imageUrl,
responseType: "arraybuffer",
});
fs.writeFileSync(filePath, responseImg.data);
finalPhotoUrl = filePath.replace("public", "").replace(/\\/g, "/");
}
// === Absen masuk ===
attendance = await Attedances.create({
user_id,
name: user.name,
photo: finalPhotoUrl,
branch_id: user.branch_id,
type: 'present',
date: today,
clock_in: now.toDate(),
lat,
lng,
}, { transaction: t });
await t.commit();
return response.success(res, {
...attendance.toJSON(),
branch_name: branch.name,
clock_in: moment(attendance.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
clock_out: attendance.clock_out
? moment(attendance.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss')
: null
}, 'Absen masuk berhasil');
} catch (error) {
await t.rollback();
errorHandler(error, res, req);
return response.failed(res, 500, error.message);
}
};
const clockOut = async (req, res) => {
const t = await sequelize.transaction();
try {
const user_id = req.user.id;
const now = moment().tz('Asia/Jakarta');
const today = now.format('YYYY-MM-DD');
const user = await User.findOne({ where: { id: user_id } });
if (!user) return response.failed(res, 404, 'User tidak ditemukan');
const attendance = await Attedances.findOne({
where: { user_id, date: today, type: 'present' },
});
if (!attendance) {
await t.rollback();
return response.failed(res, 400, 'Belum absen masuk hari ini');
}
if (attendance.clock_out) {
await t.rollback();
return response.failed(res, 400, 'Sudah absen pulang hari ini');
}
const branch = await Branch.findOne({ where: { id: user.branch_id } });
// Set jam pulang (tanpa cek lokasi)
attendance.clock_out = now.toDate();
// Hitung durasi kerja
const durationMs = moment(attendance.clock_out).diff(moment(attendance.clock_in));
const hours = Math.floor(durationMs / (1000 * 60 * 60));
const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
attendance.work_duration = `${hours} jam ${minutes} menit`;
await attendance.save({ transaction: t });
await t.commit();
return response.success(res, {
...attendance.toJSON(),
branch_name: branch.name,
clock_in: moment(attendance.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
clock_out: moment(attendance.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss'),
work_duration: attendance.work_duration,
}, 'Absen pulang berhasil');
} catch (error) {
await t.rollback();
errorHandler(error, res, req);
return response.failed(res, 500, error.message);
}
};
const history = async (req, res) => {
try {
const user_id = req.user?.id;
if (!user_id) return response.failed(res, 401, "User tidak terautentikasi");
const { type } = req.query;
const where = { user_id };
if (type === 'today') {
const today = new Date(Date.now() + 7 * 60 * 60 * 1000).toISOString().split('T')[0];
where.date = today; // pakai equality, bukan LIKE
}
const attendances = await Attedances.findAll({
where,
order: [['date', 'DESC']],
});
if (!attendances.length)
return response.success(res, [], "Tidak ada data absensi");
const result = attendances.map(a => {
let duration = null;
if (a.clock_in && a.clock_out && a.lunch_in) {
const diffMs = new Date(a.clock_out) - new Date(a.clock_in);
const totalMinutes = Math.floor(diffMs / 60000);
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
duration = `${hours} jam ${minutes} menit`;
}
return {
name: a.name,
date: moment(a.date).tz('Asia/Jakarta').format('YYYY-MM-DD'),
clock_in: a.clock_in
? moment(a.clock_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss')
: '-',
clock_out: a.clock_out
? moment(a.clock_out).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss')
: '-',
lunch_in: a.lunch_in
? moment(a.lunch_in).tz('Asia/Jakarta').format('YYYY-MM-DD HH:mm:ss')
: '-',
duration: duration || '-',
type: a.type,
};
});
return response.success(res, result, "Riwayat absensi berhasil dimuat");
} catch (error) {
errorHandler(error, req, res);
return response.failed(res, 500, error.message);
}
};
const getAll = async (req, res) => {
try {
const today = moment().tz('Asia/Jakarta').format('YYYY-MM-DD');
const attendances = await Attedances.findAll({
where: {
date: today
},
include: [
{
model: User,
as: 'user',
attributes: ['id', 'name', 'email']
}
],
order: [['created_at', 'DESC']]
});
return response.success(res, attendances, 'List kehadiran hari ini');
} catch (error) {
console.error(error);
return response.error(res, 'Gagal mengambil data absensi hari ini');
}
};
const update = async (req, res) => {
const t = await sequelize.transaction();
try {
const id = req.params.id;
const user_id = req.user.id;
const body = req.body;
const attedances = await Attedances.findOne({
where: { id },
transaction: t
});
const attedancesUpdate = await attedances.update({
...body,
user_id,
}, { transaction: t });
await t.commit();
return response.success(res, attedancesUpdate, 'Category Berhasil Di update');
} catch (error) {
await t.rollback();
errorHandler(error, req, res);
return response.failed(res, 500, error.message);
}
}
const destroy = async (req, res) => {
const t = await sequelize.transaction();
try {
const id = req.params.id;
const attendance = await Attedances.findOne({
where: { id },
transaction: t,
});
if (!attendance) {
await t.rollback();
return response.failed(res, 404, 'Attendance tidak ditemukan');
}
await attendance.destroy({ transaction: t });
await t.commit();
return response.success(res, null, 'Attendance berhasil dihapus');
} catch (error) {
await t.rollback();
errorHandler(error, req, res);
return response.failed(res, 500, error.message);
}
};
const checkLocation = async (req, res) => {
try {
const user_id = req.user.id;
const { lat, lng } = req.query;
if (!lat || !lng) {
return response.failed(res, 400, 'Latitude dan longitude wajib dikirim');
}
const user = await User.findOne({ where: { id: user_id } });
if (!user) return response.failed(res, 404, 'User tidak ditemukan');
const branch = await Branch.findOne({ where: { id: user.branch_id } });
if (!branch) return response.failed(res, 404, 'Data kantor tidak ditemukan');
// Hitung jarak user dengan kantor (dalam meter)
const distanceMeters = getDistance(branch.lat, branch.lng, lat, lng);
// Otomatis ubah ke km jika lebih dari 1000 meter
const isKm = distanceMeters >= 1000;
const distance = isKm ? distanceMeters / 1000 : distanceMeters;
const unit = isKm ? 'km' : 'm';
// Radius tetap pakai meter (biar konsisten)
const allowedRadius = parseFloat(process.env.ABSENCE_RADIUS) || 100;
if (distanceMeters <= allowedRadius) {
return response.success(res, {
inOffice: true,
branch_name: branch.name,
distance: distance.toFixed(2),
unit,
message: `Anda sedang berada di lokasi ${branch.name}, silakan absen.`,
});
}
return response.success(res, {
inOffice: false,
branch_name: branch.name,
distance: distance.toFixed(2),
unit,
message: `Anda berada di luar area kantor ${distance.toFixed(2)} ${unit} dari ${branch.name}.`,
});
} catch (error) {
errorHandler(error, req, res);
return response.failed(res, 500, error.message);
}
};
module.exports = {
create,
destroy,
history,
update,
clockOut,
getAll,
checkLocation
}