created API For Aplication Absensi
This commit is contained in:
35
app/modules/absensi/controllers/absensi.controller.js
Normal file
35
app/modules/absensi/controllers/absensi.controller.js
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
const services = require('../services/absensi.service')
|
||||
|
||||
const history = async (req, res) => {
|
||||
const response = await services.history(req, res);
|
||||
return response;
|
||||
};
|
||||
|
||||
const create = async (req, res) => {
|
||||
const response = await services.create(req, res);
|
||||
return response;
|
||||
};
|
||||
|
||||
const clockOut = async (req, res) => {
|
||||
const response = await services.clockOut(req, res)
|
||||
return response
|
||||
}
|
||||
|
||||
const update = async (req, res) => {
|
||||
const response = await services.update(req, res);
|
||||
return response
|
||||
}
|
||||
|
||||
const destroy = async (req, res) => {
|
||||
const response = await services.destroy(req, res);
|
||||
return response;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
history,
|
||||
update,
|
||||
destroy,
|
||||
clockOut
|
||||
}
|
||||
0
app/modules/absensi/resources/category.resource.js
Normal file
0
app/modules/absensi/resources/category.resource.js
Normal file
21
app/modules/absensi/routes/absensi.route.js
Normal file
21
app/modules/absensi/routes/absensi.route.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const controller = require('../controllers/absensi.controller')
|
||||
const apiKey = require('../../../middlewares/apiKey')
|
||||
const jwt = require('../../../middlewares/authentication')
|
||||
const upload = require('../../../middlewares/upload')
|
||||
|
||||
router.get('/history', apiKey, jwt, (req, res) => {
|
||||
controller.history(req, res);
|
||||
})
|
||||
|
||||
router.post('/', jwt, apiKey, upload.single('attendances'), (req, res) => {
|
||||
controller.create(req, res)
|
||||
})
|
||||
|
||||
router.post('/clock-out', jwt, apiKey, (req, res) => {
|
||||
controller.clockOut(req, res)
|
||||
})
|
||||
|
||||
|
||||
module.exports = router
|
||||
309
app/modules/absensi/services/absensi.service.js
Normal file
309
app/modules/absensi/services/absensi.service.js
Normal file
@@ -0,0 +1,309 @@
|
||||
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 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 },
|
||||
});
|
||||
|
||||
// === 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');
|
||||
}
|
||||
|
||||
// === 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(),
|
||||
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');
|
||||
}
|
||||
|
||||
// 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(),
|
||||
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 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 categories = await Category.findOne({
|
||||
where: { id },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
const categoriesUpdate = await categories.update({
|
||||
...body,
|
||||
user_id,
|
||||
}, { transaction: t });
|
||||
|
||||
await t.commit();
|
||||
return response.success(res, categoriesUpdate, '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 category = await Category.findOne({
|
||||
where: { id },
|
||||
transaction: t,
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
await t.rollback();
|
||||
return response.failed(res, 404, 'Category tidak ditemukan');
|
||||
}
|
||||
|
||||
await category.destroy({ transaction: t });
|
||||
await t.commit();
|
||||
|
||||
return response.success(res, null, 'Category berhasil dihapus');
|
||||
} catch (error) {
|
||||
await t.rollback();
|
||||
errorHandler(error, req, res);
|
||||
return response.failed(res, 500, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
destroy,
|
||||
history,
|
||||
update,
|
||||
clockOut
|
||||
}
|
||||
Reference in New Issue
Block a user