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 }