263 lines
7.0 KiB
JavaScript
263 lines
7.0 KiB
JavaScript
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
|
|
}
|