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 }