created API For Aplication Absensi
This commit is contained in:
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