const { Sentences } = require('../models'); const { Op } = require('sequelize'); /** * ============================================ * CREATE SENTENCE * ============================================ * Tạo mới một sentence entry * * @route POST /api/sentences * @access Private (requires authentication) * * INPUT: * { * text: String (required) - nội dung câu * ipa: String - phiên âm IPA * vi: String - nghĩa tiếng Việt * category: String - category của câu (e.g., Action Verbs, Nouns) * topic: String - chủ đề (e.g., Food, Travel, Education) * image_small: JSON Array - mảng URLs của hình ảnh nhỏ * image_square: JSON Array - mảng URLs của hình ảnh vuông * image_normal: JSON Array - mảng URLs của hình ảnh bình thường * audio: JSON Array - mảng URLs của audio files * tags: JSON Array - các tags để phân loại * usage_note: String - lưu ý về ngữ cảnh sử dụng * etc: String - các thông tin khác * } * * OUTPUT: * { * success: Boolean, * message: String, * data: Sentence object đã tạo * } */ exports.createSentence = async (req, res) => { try { const { text, ipa, vi, category, topic, image_small, image_square, image_normal, audio, tags, usage_note, etc } = req.body; // Validate required fields if (!text) { return res.status(400).json({ success: false, message: 'text is a required field' }); } const sentence = await Sentences.create({ text, ipa: ipa || null, vi: vi || '', category: category || null, topic: topic || null, image_small: image_small || [], image_square: image_square || [], image_normal: image_normal || [], audio: audio || null, tags: tags || [], usage_note: usage_note || '', etc: etc || '', is_active: true }); res.status(201).json({ success: true, message: 'Sentence created successfully', data: sentence }); } catch (error) { console.error('Error creating sentence:', error); res.status(500).json({ success: false, message: 'Error creating sentence', error: error.message }); } }; /** * ============================================ * GET ALL SENTENCES * ============================================ * Lấy danh sách sentences với phân trang và filter * * @route GET /api/sentences * @access Private * * INPUT (Query Parameters): * { * page: Number - trang hiện tại (mặc định: 1) * limit: Number - số items mỗi trang (mặc định: 20) * category: String - lọc theo category * topic: String - lọc theo topic * text: String - lọc theo text chính xác * search: String - tìm kiếm trong text và vi * is_active: Boolean - lọc theo trạng thái active (mặc định: true) * } * * OUTPUT: * { * success: Boolean, * message: String, * data: Array of Sentence objects, * pagination: { total, page, limit, totalPages } * } */ exports.getAllSentences = async (req, res) => { try { const { page = 1, limit = 20, category, topic, text, search, is_active = true } = req.query; const offset = (page - 1) * limit; const where = {}; if (is_active !== undefined) { where.is_active = is_active === 'true' || is_active === true; } if (category) { where.category = category; } if (topic) { where.topic = topic; } if (text) { where.text = text; } if (search) { where[Op.or] = [ { text: { [Op.like]: `%${search}%` } }, { vi: { [Op.like]: `%${search}%` } } ]; } const { count, rows } = await Sentences.findAndCountAll({ where, limit: parseInt(limit), offset: parseInt(offset), order: [['created_at', 'DESC']] }); res.json({ success: true, message: 'Sentences retrieved successfully', data: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit) } }); } catch (error) { console.error('Error getting sentences:', error); res.status(500).json({ success: false, message: 'Error retrieving sentences', error: error.message }); } }; /** * ============================================ * GET SENTENCE BY ID * ============================================ * Lấy chi tiết một sentence theo id * * @route GET /api/sentences/:id * @access Private * * INPUT (URL Parameter): * { * id: UUID - id của sentence cần lấy * } * * OUTPUT: * { * success: Boolean, * message: String, * data: Sentence object với đầy đủ thông tin * } */ exports.getSentenceById = async (req, res) => { try { const { id } = req.params; const sentence = await Sentences.findOne({ where: { id, is_active: true } }); if (!sentence) { return res.status(404).json({ success: false, message: 'Sentence not found' }); } res.json({ success: true, message: 'Sentence retrieved successfully', data: sentence }); } catch (error) { console.error('Error getting sentence:', error); res.status(500).json({ success: false, message: 'Error retrieving sentence', error: error.message }); } }; /** * ============================================ * GET SENTENCES WITHOUT IPA * ============================================ * Lấy tất cả các sentences chưa có IPA * * @route GET /api/sentences/missing/ipa * @access Private * * INPUT (Query Parameters): * { * page: Number (mặc định: 1) * limit: Number (mặc định: 50) * } * * OUTPUT: * { * success: Boolean, * message: String, * data: Array of Sentence objects, * pagination: { total, page, limit, totalPages } * } */ exports.getSentencesWithoutIpa = async (req, res) => { try { const { page = 1, limit = 50 } = req.query; const offset = (page - 1) * limit; const { count, rows } = await Sentences.findAndCountAll({ where: { is_active: true, [Op.or]: [ { ipa: null }, { ipa: '' } ] }, limit: parseInt(limit), offset: parseInt(offset), order: [['text', 'ASC']] }); res.json({ success: true, message: 'Sentences without IPA retrieved successfully', data: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit) } }); } catch (error) { console.error('Error getting sentences without IPA:', error); res.status(500).json({ success: false, message: 'Error retrieving sentences without IPA', error: error.message }); } }; /** * ============================================ * GET SENTENCES WITHOUT IMAGES * ============================================ * Lấy tất cả các sentences chưa đủ hình ảnh * * @route GET /api/sentences/missing/images * @access Private * * INPUT (Query Parameters): * { * page: Number (mặc định: 1) * limit: Number (mặc định: 50) * } * * OUTPUT: * { * success: Boolean, * message: String, * data: Array of Sentence objects, * pagination: { total, page, limit, totalPages } * } */ exports.getSentencesWithoutImages = async (req, res) => { try { const { page = 1, limit = 50 } = req.query; const offset = (page - 1) * limit; const { count, rows } = await Sentences.findAndCountAll({ where: { is_active: true, [Op.or]: [ { image_small: null }, { image_square: null }, { image_normal: null } ] }, limit: parseInt(limit), offset: parseInt(offset), order: [['text', 'ASC']] }); res.json({ success: true, message: 'Sentences without images retrieved successfully', data: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit) } }); } catch (error) { console.error('Error getting sentences without images:', error); res.status(500).json({ success: false, message: 'Error retrieving sentences without images', error: error.message }); } }; /** * ============================================ * UPDATE SENTENCE * ============================================ * Cập nhật thông tin sentence * * @route PUT /api/sentences/:id * @access Private * * INPUT (URL Parameter + Body): * { * id: UUID - id cần update * Body: Object - các trường cần update * } * * OUTPUT: * { * success: Boolean, * message: String, * data: Updated Sentence object * } */ exports.updateSentence = async (req, res) => { try { const { id } = req.params; const updateData = req.body; const sentence = await Sentences.findOne({ where: { id } }); if (!sentence) { return res.status(404).json({ success: false, message: 'Sentence not found' }); } await sentence.update(updateData); res.json({ success: true, message: 'Sentence updated successfully', data: sentence }); } catch (error) { console.error('Error updating sentence:', error); res.status(500).json({ success: false, message: 'Error updating sentence', error: error.message }); } }; /** * ============================================ * DELETE SENTENCE (SOFT DELETE) * ============================================ * Xóa mềm sentence (set is_active = false) * * @route DELETE /api/sentences/:id * @access Private * * INPUT (URL Parameter): * { * id: UUID - id cần xóa * } * * OUTPUT: * { * success: Boolean, * message: String * } */ exports.deleteSentence = async (req, res) => { try { const { id } = req.params; const sentence = await Sentences.findOne({ where: { id } }); if (!sentence) { return res.status(404).json({ success: false, message: 'Sentence not found' }); } await sentence.update({ is_active: false }); res.json({ success: true, message: 'Sentence deleted successfully' }); } catch (error) { console.error('Error deleting sentence:', error); res.status(500).json({ success: false, message: 'Error deleting sentence', error: error.message }); } }; /** * ============================================ * BULK CREATE SENTENCES * ============================================ * Tạo nhiều sentences cùng lúc * * @route POST /api/sentences/bulk * @access Private * * INPUT (Body): * { * sentences: Array of Sentence objects - mỗi object phải có text * } * * OUTPUT: * { * success: Boolean, * message: String, * data: Array of created Sentence objects, * count: Number - số lượng đã tạo * } */ exports.bulkCreateSentences = async (req, res) => { try { const { sentences } = req.body; if (!Array.isArray(sentences) || sentences.length === 0) { return res.status(400).json({ success: false, message: 'sentences must be a non-empty array' }); } // Validate each sentence has required fields for (let i = 0; i < sentences.length; i++) { if (!sentences[i].text) { return res.status(400).json({ success: false, message: `Sentence at index ${i} is missing required field (text)` }); } } const createdSentences = await Sentences.bulkCreate(sentences); res.status(201).json({ success: true, message: `${createdSentences.length} sentences created successfully`, data: createdSentences, count: createdSentences.length }); } catch (error) { console.error('Error bulk creating sentences:', error); res.status(500).json({ success: false, message: 'Error creating sentences', error: error.message }); } }; /** * ============================================ * GET SENTENCE STATISTICS * ============================================ * Lấy thống kê về sentences * * @route GET /api/sentences/stats/overview * @access Private * * OUTPUT: * { * success: Boolean, * message: String, * data: { * total: { active, inactive, all }, * by_category: Array [{category, count}], * by_topic: Array [{topic, count}] * } * } */ exports.getSentenceStats = async (req, res) => { try { const { sequelize } = Sentences; const totalActive = await Sentences.count({ where: { is_active: true } }); const totalInactive = await Sentences.count({ where: { is_active: false } }); const byCategory = await Sentences.findAll({ where: { is_active: true }, attributes: [ 'category', [sequelize.fn('COUNT', sequelize.col('id')), 'count'] ], group: ['category'], order: [[sequelize.fn('COUNT', sequelize.col('id')), 'DESC']], raw: true }); const byTopic = await Sentences.findAll({ where: { is_active: true }, attributes: [ 'topic', [sequelize.fn('COUNT', sequelize.col('id')), 'count'] ], group: ['topic'], order: [[sequelize.fn('COUNT', sequelize.col('id')), 'DESC']], raw: true }); res.json({ success: true, message: 'Statistics retrieved successfully', data: { total: { active: totalActive, inactive: totalInactive, all: totalActive + totalInactive }, by_category: byCategory, by_topic: byTopic } }); } catch (error) { console.error('Error getting sentence stats:', error); res.status(500).json({ success: false, message: 'Error retrieving statistics', error: error.message }); } }; /** * ============================================ * SEARCH SENTENCES * ============================================ * Tìm kiếm sentences nâng cao với nhiều filter * * @route POST /api/sentences/search * @access Private * * INPUT (Body): * { * topic: String (optional) * category: String (optional) * text: String (optional) - partial match * vi: String (optional) - partial match * tags: Array (optional) - lọc theo tags * page: Number (mặc định: 1) * limit: Number (mặc định: 100) * } * * OUTPUT: * { * success: Boolean, * message: String, * data: Array of Sentence objects, * pagination: { total, page, limit, totalPages } * } */ exports.searchSentences = async (req, res) => { try { const { topic, category, text, vi, page = 1, limit = 100 } = req.body; const offset = (page - 1) * limit; const where = { is_active: true }; if (topic) { where.topic = topic; } if (category) { where.category = category; } if (text) { where.text = { [Op.like]: `%${text}%` }; } if (vi) { where.vi = { [Op.like]: `%${vi}%` }; } const { count, rows } = await Sentences.findAndCountAll({ where, limit: parseInt(limit), offset: parseInt(offset), order: [['text', 'ASC']] }); res.json({ success: true, message: 'Search completed successfully', data: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit) } }); } catch (error) { console.error('Error searching sentences:', error); res.status(500).json({ success: false, message: 'Error searching sentences', error: error.message }); } }; /** * ============================================ * GET ALL CATEGORIES * ============================================ * Lấy danh sách tất cả categories có trong database * * @route GET /api/sentences/meta/categories * @access Private * * OUTPUT: * { * success: Boolean, * message: String, * data: Array of String, * count: Number * } */ exports.getAllCategories = async (req, res) => { try { const categories = await Sentences.findAll({ where: { is_active: true, category: { [Op.ne]: null } }, attributes: ['category'], group: ['category'], order: [['category', 'ASC']], raw: true }); const categoryList = categories.map(c => c.category); res.json({ success: true, message: 'Categories retrieved successfully', data: categoryList, count: categoryList.length }); } catch (error) { console.error('Error getting categories:', error); res.status(500).json({ success: false, message: 'Error retrieving categories', error: error.message }); } }; /** * ============================================ * GET ALL TOPICS * ============================================ * Lấy danh sách tất cả topics có trong database * * @route GET /api/sentences/meta/topics * @access Private * * OUTPUT: * { * success: Boolean, * message: String, * data: Array of String, * count: Number * } */ exports.getAllTopics = async (req, res) => { try { const topics = await Sentences.findAll({ where: { is_active: true, topic: { [Op.ne]: null } }, attributes: ['topic'], group: ['topic'], order: [['topic', 'ASC']], raw: true }); const topicList = topics.map(t => t.topic); res.json({ success: true, message: 'Topics retrieved successfully', data: topicList, count: topicList.length }); } catch (error) { console.error('Error getting topics:', error); res.status(500).json({ success: false, message: 'Error retrieving topics', error: error.message }); } };