created API For Aplication Absensi

This commit is contained in:
2025-10-14 14:08:11 +07:00
commit 96d206d892
56 changed files with 6533 additions and 0 deletions

View 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
}

View 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

View 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
}

View File

@@ -0,0 +1,28 @@
const services = require('../services/branch.service')
const getAll = async (req, res) => {
const response = await services.getAll(req, res)
return response
}
const create = async (req, res) => {
const response = await services.create(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 = {
getAll,
create,
update,
destroy
}

View File

@@ -0,0 +1,23 @@
const express = require('express')
const router = express.Router()
const controller = require('../controllers/branch.controller')
const apiKey = require('../../../middlewares/apiKey')
const jwt = require('../../../middlewares/authentication')
router.get('/', apiKey, (req, res) => {
controller.getAll(req, res)
})
router.post('/', apiKey, jwt, (req, res) => {
controller.create(req, res)
} )
router.put('/:id', apiKey, jwt, (req, res) => {
controller.update(req, res)
})
router.put('/:id', apiKey, jwt, (req, res) => {
controller.destroy(req, res)
})
module.exports = router

View File

@@ -0,0 +1,96 @@
const response = require('../../../helpers/responses')
const db = require('../../../../models/migration')
const errorHandler = require('../../../middlewares/errorHandler')
const {sequelize} = require('../../../../models/migration')
const { where } = require('sequelize')
const Branch = db.Branch
const getAll = async (req, res) => {
try {
const branch = await Branch.findAll({
order: [['created_at', 'DESC']]
})
return response.success(res, branch, 'successfully loaded')
} catch (error) {
errorHandler(error, req, res)
return response.failed(res, 500, error.message)
}
}
const create = async (req, res) => {
const t = await sequelize.transaction()
try {
const user_id = req.user.id
const body = req.body
const branch = await Branch.create({
...body,
user_id
})
await t.commit()
return response.success(res, branch, 'create successfuly')
} 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 body = req.body
const user_id = req.user.id
const branch = await Branch.findOne({
where: {id},
transaction: t
})
if (!branch) {
await t.rollback()
return response.failed(res, 404, 'Data Not Found')
}
const branchUpdate = await branch.update({
...body,
user_id
})
await t.commit()
return response.success(res, branchUpdate, 'Updated Successfuly')
} catch (error) {
await t.rollback()
errorHandler(error, req, res)
return response.failed(res, 500, error.message)
}
}
const destroy = async (req, res) => {
try {
const id = req.params.id
const branch = await Branch.findOne({
where: { id },
})
if (!branch) {
return response.failed(res, 404, 'Data Not Found')
}
await branch.destroy();
return response.success(res, null, 'Deleted Successfuly')
} catch (error) {
errorHandler(error, req, res)
return response.failed(res, 500, error.message)
}
}
module.exports = {
getAll,
create,
update,
destroy
}

View File

@@ -0,0 +1,22 @@
const service = require('../services/profile.service')
const update = async (req, res) => {
const response = await service.update(req, res)
return response
}
const getProfile = async (req, res) => {
const response = await service.getProfile(req, res)
return response
}
const getOverview = async (req, res) => {
const response = await service.getOverview(req, res)
return response
}
module.exports = {
update,
getProfile,
getOverview
}

View File

@@ -0,0 +1,20 @@
const express = require('express')
const router = express.Router()
const controller = require('../controllers/profile.controller')
const apiKey = require('../../../middlewares/apiKey')
const jwt = require('../../../middlewares/authentication')
const upload = require('../../../middlewares/upload')
router.get('/', apiKey, jwt, (req, res) => {
controller.getProfile(req, res)
})
router.put('/', apiKey, jwt,upload.single("avatar"), (req, res) => {
controller.update(req, res)
})
router.get('/overview', apiKey, jwt, (req, res) => {
controller.getOverview(req, res)
})
module.exports = router

View File

@@ -0,0 +1,262 @@
const response = require('../../../helpers/responses')
const db = require('../../../../models/migration')
const errorHandler = require('../../../middlewares/errorHandler')
const { sequelize, Op } = require('../../../../models/migration')
const User = db.User
const path = require("path");
const fs = require("fs");
const Story = db.Story;
const StoryReader = db.StoryReader
const Serial = db.Serial;
const StoryRating = db.StoryRating
const StoryLike = db.StoryLike
const Category = db.Category;
const DifficultyLevel = db.DifficultyLevel
const AgeTarget = db.AgeTarget
// UPDATE
const update = async (req, res) => {
const t = await sequelize.transaction();
try {
const id = req.user.id;
const users = await User.findOne({
where: { id },
transaction: t,
});
if (!users) {
await t.rollback();
return response.failed(res, 404, "User tidak ditemukan atau bukan milik Anda");
}
const body = req.body;
let avatarUrl = users.avatar_url; // default tetap avatar lama
// 🔹 Kalau ada file diupload
if (req.file) {
const uploadDir = path.join(process.cwd(), "public", "uploads", "avatars");
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// Simpan file
const fileName = `${Date.now()}-${req.file.originalname}`;
const filePath = path.join(uploadDir, fileName);
fs.writeFileSync(filePath, req.file.buffer);
// Bisa pakai URL public (misal /uploads/avatars/xxx.jpg)
avatarUrl = `/uploads/avatars/${fileName}`;
}
const updatedUser = await users.update(
{
...body,
avatar_url: avatarUrl,
},
{ transaction: t }
);
await t.commit();
return response.success(res, updatedUser, "Profil berhasil diperbarui");
} catch (error) {
await t.rollback();
errorHandler(error, req, res);
return response.failed(res, 500, error.message);
}
};
// GET ALL
const getProfile = async (req, res) => {
try {
const user_id = req.user.id
const user = await User.findOne({
where: { id: user_id }
})
if (!user) {
return response.failed(res, 404, 'User tidak ditemukan')
}
// Mapper agar sesuai response yang kamu mau
const result = {
id: user.id,
email: user.email,
display_name: user.name, // atau field display_name jika ada
role: user.role,
avatar_url: user.avatar_url || null,
bio: user.bio || null,
birth: user.birth || null,
created_at: user.created_at,
updated_at: user.updated_at
}
return response.success(res, result, 'Profile berhasil dimuat')
} catch (error) {
errorHandler(error, req, res)
return response.failed(res, 500, error.message)
}
}
const getOverview = async (req, res) => {
try {
const user_id = req.user.id;
// Total cerita
const totalStories = await Story.count({ where: { user_id } });
const totalPublished = await Story.count({ where: { user_id, is_published: true } });
// Total pembaca unik
const totalReaders = await StoryReader.count({
include: [{ model: Story, where: { user_id }, attributes: [] }],
distinct: true,
col: "user_id"
});
// Rating rata-rata (FIX: kualifikasi kolom supaya tidak ambiguous)
const ratingResult = await StoryRating.findOne({
attributes: [
[sequelize.fn("AVG", sequelize.col("StoryRating.rating")), "avgRating"]
],
include: [{ model: Story, where: { user_id }, attributes: [] }],
raw: true
});
const avgRating = ratingResult && ratingResult.avgRating
? Number(parseFloat(ratingResult.avgRating).toFixed(1))
: 0;
// === Ambil semua serial user ===
const serialsRaw = await Serial.findAll({
where: { user_id },
attributes: [
"id",
"title",
"description",
"reading_time",
"rating",
"cover_image_url",
"createdAt",
"is_active"
],
include: [{ model: Category, attributes: ["id", "title", "emoji"] }],
order: [["createdAt", "DESC"]]
});
// Ambil semua story user (sekalian untuk lookup cover)
const stories = await Story.findAll({
where: { user_id },
attributes: [
"id",
"title",
"synopsis",
"is_published",
"cover_image_url",
"createdAt",
"series_id"
],
include: [
{ model: Category, attributes: ["id", "title", "emoji"] },
{ model: DifficultyLevel, attributes: ["id", "title", "emoji"] },
{ model: AgeTarget, attributes: ["id", "title", "emoji"] }
],
order: [["createdAt", "DESC"]]
});
// Buat map serial_id → serial
const serialMap = {};
serialsRaw.forEach(serial => {
serialMap[serial.id] = serial.toJSON();
});
// Buat map serial_id → story pertama (untuk fallback cover)
const storiesBySerial = {};
for (const story of stories) {
if (story.series_id && !storiesBySerial[story.series_id]) {
storiesBySerial[story.series_id] = story.cover_image_url;
}
}
// === Gabungkan serial dengan fallback cover ===
const serials = serialsRaw.map(serial => {
let coverImage = serial.cover_image_url;
if (!coverImage && storiesBySerial[serial.id]) {
coverImage = storiesBySerial[serial.id];
}
const storiesOfSerial = stories.filter(s => s.series_id === serial.id);
return {
id: serial.id,
title: serial.title,
description: serial.description,
reading_time: serial.reading_time,
rating: serial.rating,
cover_image_url: coverImage,
is_active: serial.is_active,
createdAt: serial.createdAt,
category: serial.Category
? { id: serial.Category.id, title: serial.Category.title, emoji: serial.Category.emoji }
: null,
story: storiesOfSerial.map(s => ({
id: s.id,
title: s.title,
cover_image_url: s.cover_image_url,
is_published: s.is_published
})),
total_episodes: storiesOfSerial.length
};
});
// === Gabungkan story dengan serial data ===
const storiesWithSerial = stories.map(story => {
const serial = story.series_id ? serialMap[story.series_id] || null : null;
let coverImage = story.cover_image_url;
// Fallback cover dari serial
if (!coverImage && serial) {
coverImage = serial.cover_image_url || storiesBySerial[serial.id] || null;
}
return {
...story.toJSON(),
cover_image_url: coverImage,
Serial: serial
};
});
// === Response ===
return response.success(
res,
{
total_stories: totalStories,
total_published: totalPublished,
total_readers: totalReaders,
average_rating: avgRating,
stories: storiesWithSerial,
serials
},
"Overview berhasil dimuat"
);
} catch (error) {
errorHandler(error, req, res);
return response.failed(res, 500, error.message);
}
};
module.exports = {
update,
getProfile,
getOverview
}