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
|
||||
}
|
||||
28
app/modules/branch/controllers/branch.controller.js
Normal file
28
app/modules/branch/controllers/branch.controller.js
Normal 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
|
||||
}
|
||||
23
app/modules/branch/routes/branch.route.js
Normal file
23
app/modules/branch/routes/branch.route.js
Normal 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
|
||||
96
app/modules/branch/services/branch.service.js
Normal file
96
app/modules/branch/services/branch.service.js
Normal 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
|
||||
}
|
||||
22
app/modules/profile/controllers/profile.controller.js
Normal file
22
app/modules/profile/controllers/profile.controller.js
Normal 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
|
||||
}
|
||||
0
app/modules/profile/resources/profile.resource.js
Normal file
0
app/modules/profile/resources/profile.resource.js
Normal file
20
app/modules/profile/routes/profile.route.js
Normal file
20
app/modules/profile/routes/profile.route.js
Normal 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
|
||||
262
app/modules/profile/services/profile.service.js
Normal file
262
app/modules/profile/services/profile.service.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user