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

424 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 jam antara 1215 → lunch_in
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');
}
// 🕒 Jika jam >= 15 → otomatis 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');
}
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) {
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')
: '-',
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
}