const { Context, Vocab } = require('../models'); /** * Context Controller - Workflow-based status management * Status flow: 0 (Draft) -> 1 (Enriched) -> 2 (Prompt Ready) -> 3 (Generating) -> 4 (Image Ready) -> 5 (Approved) */ class ContextController { /** * Create new context - Status 0 (Draft) * Required fields: title, desc, grade */ async createContext(req, res, next) { try { const { title, desc, grade, type, type_image , reference_id } = req.body; // Validate required fields if (!title || !desc || !grade) { return res.status(400).json({ success: false, message: 'Title, desc, and grade are required' }); } // Validate grade format (gradeX100 + unitX10 + lesson) const gradeNum = parseInt(grade); if (isNaN(gradeNum) || gradeNum < 100) { return res.status(400).json({ success: false, message: 'Grade must be in format: gradeX100 + unitX10 + lesson (e.g., 123 for Grade 1 Unit 2 Lesson 3)' }); } const context = await Context.create({ title, desc, grade: gradeNum, type: type || 'general', type_image: type_image || null, status: 0, // Draft context: '', knowledge: '', reference_id: reference_id || null }); res.status(201).json({ success: true, message: 'Context created successfully', data: context }); } catch (error) { next(error); } } /** * Get contexts by status */ async getContextsByStatus(req, res, next) { try { const { status } = req.params; const { page = 1, limit = 50, type, grade } = req.query; const offset = (page - 1) * limit; const where = { status: parseInt(status) }; if (type) where.type = type; if (grade) where.grade = parseInt(grade); const { count, rows } = await Context.findAndCountAll({ where, limit: parseInt(limit), offset: parseInt(offset), order: [['created_at', 'DESC']] }); res.json({ success: true, data: { contexts: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit) } } }); } catch (error) { next(error); } } /** * Update knowledge - Status 0 -> 1 (Enriched) */ async enrichContext(req, res, next) { try { const { id } = req.params; const { knowledge } = req.body; if (!knowledge) { return res.status(400).json({ success: false, message: 'Knowledge is required' }); } const context = await Context.findByPk(id); if (!context) { return res.status(404).json({ success: false, message: 'Context not found' }); } if (context.status !== 0) { return res.status(400).json({ success: false, message: 'Context must be in Draft status (0) to enrich' }); } await context.update({ knowledge, status: 1 }); res.json({ success: true, message: 'Context enriched successfully', data: context }); } catch (error) { next(error); } } /** * Update context and img_prompt - Status 1 -> 2 (Prompt Ready) */ async preparePrompt(req, res, next) { try { const { id } = req.params; const { context, img_prompt } = req.body; if (!context || !img_prompt) { return res.status(400).json({ success: false, message: 'Context and img_prompt are required' }); } const contextRecord = await Context.findByPk(id); if (!contextRecord) { return res.status(404).json({ success: false, message: 'Context not found' }); } if (contextRecord.status !== 1) { return res.status(400).json({ success: false, message: 'Context must be in Enriched status (1) to prepare prompt' }); } await contextRecord.update({ context, img_prompt, status: 2 }); res.json({ success: true, message: 'Prompt prepared successfully', data: contextRecord }); } catch (error) { next(error); } } /** * Update status to 3 (Generating) or back to 1 (Enriched) */ async updateStatusFromPromptReady(req, res, next) { try { const { id } = req.params; const { status } = req.body; const context = await Context.findByPk(id); if (!context) { return res.status(404).json({ success: false, message: 'Context not found' }); } await context.update({ status: parseInt(status) }); res.json({ success: true, message: `Status updated to ${status}`, data: context }); } catch (error) { next(error); } } /** * Bulk update all status 2 to status 3 (Prompt Ready -> Generating) */ async bulkUpdateStatus2To3(req, res, next) { try { const [affectedCount] = await Context.update( { status: 3 }, { where: { status: 2 } } ); res.json({ success: true, message: `Updated ${affectedCount} context(s) from status 2 to status 3`, data: { affectedCount } }); } catch (error) { next(error); } } /** * Add images - Status 3 -> 4 (Image Ready) */ async addImages(req, res, next) { try { const { id } = req.params; const { image } = req.body; if (!image || typeof image !== 'string' || image.trim().length === 0) { return res.status(400).json({ success: false, message: 'Image must be a non-empty string (URL)' }); } const context = await Context.findByPk(id); if (!context) { return res.status(404).json({ success: false, message: 'Context not found' }); } if (context.status !== 3) { return res.status(400).json({ success: false, message: 'Context must be in Generating status (3) to add images' }); } await context.update({ image, status: 4 }); res.json({ success: true, message: 'Images added successfully', data: context }); } catch (error) { next(error); } } /** * Approve context - Status 4 -> 5 (Approved) */ async approveContext(req, res, next) { try { const { id } = req.params; const context = await Context.findByPk(id); if (!context) { return res.status(404).json({ success: false, message: 'Context not found' }); } if (context.status !== 4) { return res.status(400).json({ success: false, message: 'Context must be in Image Ready status (4) to approve' }); } // add image to Vocab Image const currentVocab = await Vocab.findOne({ where: { vocab_id: context.reference_id } }); console.log('Current Vocab:', currentVocab); if (currentVocab) { if (context.type_image === 'small') { const updatedImagesSmall = currentVocab.image_small || []; updatedImagesSmall.push(context.image); await currentVocab.update({ image_small: updatedImagesSmall }); } else if (context.type_image === 'square') { const updatedImagesSquare = currentVocab.image_square || []; updatedImagesSquare.push(context.image); await currentVocab.update({ image_square: updatedImagesSquare }); } else if (context.type_image === 'normal') { const updatedImagesNormal = currentVocab.image_normal || []; updatedImagesNormal.push(context.image); await currentVocab.update({ image_normal: updatedImagesNormal }); } await currentVocab.save(); } await context.update({ status: 5 }); res.json({ success: true, message: 'Context approved successfully', data: context }); } catch (error) { next(error); } } /** * Get context by UUID (for viewing details) */ async getContextById(req, res, next) { try { const { id } = req.params; const context = await Context.findByPk(id); if (!context) { return res.status(404).json({ success: false, message: 'Context not found' }); } res.json({ success: true, data: context }); } catch (error) { next(error); } } /** * Get all contexts with pagination and filters */ async getAllContexts(req, res, next) { try { const { page = 1, limit = 50, type, grade, status } = req.query; const offset = (page - 1) * limit; const where = {}; if (type) where.type = type; if (grade) where.grade = parseInt(grade); if (status !== undefined) where.status = parseInt(status); const { count, rows } = await Context.findAndCountAll({ where, limit: parseInt(limit), offset: parseInt(offset), order: [['created_at', 'DESC']] }); res.json({ success: true, data: { contexts: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit) } } }); } catch (error) { next(error); } } /** * Search contexts with partial match on title/context + filter by type_image * * POST /api/contexts/search * Body: * { * search : String - tìm kiếm một phần trong title HOẶC context (OR) * title : String - tìm riêng trong title (LIKE) * context_text : String - tìm riêng trong context (LIKE) * type_image : String - exact match (e.g., 'small', 'square', 'normal') * type : String - exact match * status : Number - exact match * grade : Number - exact match * page : Number (default: 1) * limit : Number (default: 50) * } */ async searchContexts(req, res, next) { try { const { search, title, context_text, type_image, type, status, grade, page = 1, limit = 50 } = req.body; const { Op } = require('sequelize'); const offset = (page - 1) * limit; const where = {}; // ── Exact-match filters ────────────────────────────────────────────── if (type_image !== undefined && type_image !== null && type_image !== '') { where.type_image = type_image; } if (type) where.type = type; if (status !== undefined && status !== null) where.status = parseInt(status); if (grade !== undefined && grade !== null) where.grade = parseInt(grade); // ── Text search ────────────────────────────────────────────────────── // `search` → title OR context (cả hai cùng lúc) // `title` → chỉ title // `context_text` → chỉ context const textConditions = []; if (search) { textConditions.push( { title: { [Op.like]: `%${search}%` } }, { context: { [Op.like]: `%${search}%` } } ); } if (title) { textConditions.push({ title: { [Op.like]: `%${title}%` } }); } if (context_text) { textConditions.push({ context: { [Op.like]: `%${context_text}%` } }); } if (textConditions.length > 0) { where[Op.or] = textConditions; } const { count, rows } = await Context.findAndCountAll({ where, limit: parseInt(limit), offset: parseInt(offset), order: [['created_at', 'DESC']] }); res.json({ success: true, message: 'Search completed successfully', data: { contexts: rows, pagination: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / limit) } } }); } catch (error) { next(error); } } /** * Update context (general update - use with caution) */ async updateContext(req, res, next) { try { const { id } = req.params; const updates = req.body; const context = await Context.findByPk(id); if (!context) { return res.status(404).json({ success: false, message: 'Context not found' }); } await context.update(updates); res.json({ success: true, message: 'Context updated successfully', data: context }); } catch (error) { next(error); } } /** * Delete context */ async deleteContext(req, res, next) { try { const { id } = req.params; const context = await Context.findByPk(id); if (!context) { return res.status(404).json({ success: false, message: 'Context not found' }); } await context.destroy(); res.json({ success: true, message: 'Context deleted successfully' }); } catch (error) { next(error); } } } module.exports = new ContextController();