update
All checks were successful
Deploy to Production / deploy (push) Successful in 20s

This commit is contained in:
silverpro89
2026-02-18 18:01:45 +07:00
parent 09e72e37e7
commit b7ba1d02b3
10 changed files with 1207 additions and 1248 deletions

4
app.js
View File

@@ -166,7 +166,7 @@ app.get('/api', (req, res) => {
lessons: '/api/lessons', lessons: '/api/lessons',
games: '/api/games', games: '/api/games',
gameTypes: '/api/game-types', gameTypes: '/api/game-types',
vocab: '/api/vocab', vocabs: '/api/vocabs',
contexts: '/api/contexts', contexts: '/api/contexts',
contextGuides: '/api/context-guides', contextGuides: '/api/context-guides',
upload: '/api/upload', upload: '/api/upload',
@@ -223,7 +223,7 @@ app.use('/api/chapters', chapterLessonRoutes); // Nested route: /api/chapters/:i
app.use('/api/games', gameRoutes); app.use('/api/games', gameRoutes);
app.use('/api/game-types', gameTypeRoutes); app.use('/api/game-types', gameTypeRoutes);
app.use('/api/lessons', lessonRoutes); app.use('/api/lessons', lessonRoutes);
app.use('/api/vocab', vocabRoutes); app.use('/api/vocabs', vocabRoutes);
app.use('/api/grammar', grammarRoutes); app.use('/api/grammar', grammarRoutes);
app.use('/api/stories', storyRoutes); app.use('/api/stories', storyRoutes);
app.use('/api/learning-content', learningContentRoutes); app.use('/api/learning-content', learningContentRoutes);

View File

@@ -1,4 +1,4 @@
const { Context } = require('../models'); const { Context, Vocab } = require('../models');
/** /**
* Context Controller - Workflow-based status management * Context Controller - Workflow-based status management
@@ -220,6 +220,28 @@ class ContextController {
} }
} }
/**
* 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) * Add images - Status 3 -> 4 (Image Ready)
*/ */
@@ -286,9 +308,28 @@ class ContextController {
message: 'Context must be in Image Ready status (4) to approve' message: 'Context must be in Image Ready status (4) to approve'
}); });
} }
// add image to Vocab Image
await context.update({ status: 5 }); 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({ res.json({
success: true, success: true,
message: 'Context approved successfully', message: 'Context approved successfully',

View File

@@ -96,8 +96,7 @@ exports.getAllStories = async (req, res) => {
where, where,
limit: parseInt(limit), limit: parseInt(limit),
offset, offset,
order: [[sort_by, sort_order.toUpperCase()]], order: [[sort_by, sort_order.toUpperCase()]]
attributes: ['id', 'name', 'logo', 'grade', 'tag', 'created_at', 'updated_at']
}); });
res.json({ res.json({

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database'); const { sequelize } = require('../config/database');
const { ref } = require('joi');
const Context = sequelize.define('Context', { const Context = sequelize.define('Context', {
uuid: { uuid: {

View File

@@ -24,16 +24,9 @@ const Vocab = sequelize.define('Vocab', {
allowNull: false, allowNull: false,
index: true index: true
}, },
// Đã xuất hiện trong khối nào, bài học nào, lesson nào
// Ví dụ 111 là grade 1, unit 1, lesson 1
grade: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: 'It is number of gradeX100 + unitX10 + lesson (e.g., Grade 1 Unit 2 Lesson 3 = 123)'
},
// Loại biến thể (V1, V2, V3, V_ing, Noun_Form...) // Loại biến thể (V1, V2, V3, V_ing, Noun_Form...)
form_key: { form_key: {
type: DataTypes.JSON, type: DataTypes.TEXT,
defaultValue: 'base', defaultValue: 'base',
comment: 'Form key indicating the type of word form (e.g., base, V1, V2, V3, V_ing, Noun_Form)' comment: 'Form key indicating the type of word form (e.g., base, V1, V2, V3, V_ing, Noun_Form)'
}, },
@@ -47,10 +40,6 @@ const Vocab = sequelize.define('Vocab', {
type: DataTypes.STRING(100), type: DataTypes.STRING(100),
comment: 'Category of the word (e.g., Action Verbs, Nouns)' comment: 'Category of the word (e.g., Action Verbs, Nouns)'
}, },
etc : {
type: DataTypes.TEXT,
comment: 'Book or additional reference'
},
topic: { topic: {
type: DataTypes.STRING(100), type: DataTypes.STRING(100),
comment: 'Topic of the word (e.g., Food, Travel, Education)' comment: 'Topic of the word (e.g., Food, Travel, Education)'
@@ -121,10 +110,6 @@ const Vocab = sequelize.define('Vocab', {
{ {
name: 'idx_category', name: 'idx_category',
fields: ['category'] fields: ['category']
},
{
name: 'idx_grade',
fields: ['grade']
} }
] ]
}); });

View File

@@ -359,6 +359,16 @@
<div class="card"> <div class="card">
<h3>📋 Get Contexts by Status <span class="status-indicator status-2">Status: 2</span></h3> <h3>📋 Get Contexts by Status <span class="status-indicator status-2">Status: 2</span></h3>
<button onclick="getContextsByStatus(2)">Get Status 2 (Prompt Ready)</button> <button onclick="getContextsByStatus(2)">Get Status 2 (Prompt Ready)</button>
<hr style="margin: 15px 0; border: 1px solid #e0e0e0;">
<button onclick="bulkUpdateStatus2To3()" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); margin-top: 10px;">
🚀 Quick Approve: All Status 2 → 3
</button>
<div class="result" id="bulkUpdateResultTop"></div>
<hr style="margin: 15px 0; border: 1px solid #e0e0e0;">
<div class="result" id="status2Result"></div> <div class="result" id="status2Result"></div>
<div class="context-list" id="status2List"></div> <div class="context-list" id="status2List"></div>
</div> </div>
@@ -382,6 +392,14 @@
</div> </div>
<button onclick="updateStatus()">Update Status</button> <button onclick="updateStatus()">Update Status</button>
<div class="result" id="updateStatusResult"></div> <div class="result" id="updateStatusResult"></div>
<hr style="margin: 20px 0; border: 1px solid #e0e0e0;">
<h4 style="color: #667eea; margin-bottom: 10px;">🚀 Bulk Update</h4>
<button onclick="bulkUpdateStatus2To3()" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
Update All Status 2 → 3
</button>
<div class="result" id="bulkUpdateResult"></div>
</div> </div>
<div class="card"> <div class="card">
@@ -643,6 +661,31 @@
} }
} }
async function bulkUpdateStatus2To3() {
const headers = getHeaders();
if (!confirm('Are you sure you want to update ALL contexts from status 2 to status 3?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/bulk/status-2-to-3`, {
method: 'POST',
headers: headers
});
const data = await response.json();
showResult('bulkUpdateResult', data, response.ok);
showResult('bulkUpdateResultTop', data, response.ok);
if (response.ok) {
getContextsByStatus(3);
getContextsByStatus(2);
}
} catch (error) {
showResult('bulkUpdateResult', { error: error.message }, false);
showResult('bulkUpdateResultTop', { error: error.message }, false);
}
}
async function addImages() { async function addImages() {
const headers = getHeaders(); const headers = getHeaders();

View File

@@ -23,6 +23,9 @@ router.post('/:id/prepare-prompt', contextController.preparePrompt);
// Status 2 -> 3 or 1: Update status // Status 2 -> 3 or 1: Update status
router.post('/:id/update-status', contextController.updateStatusFromPromptReady); router.post('/:id/update-status', contextController.updateStatusFromPromptReady);
// Bulk update all status 2 to status 3
router.post('/bulk/status-2-to-3', contextController.bulkUpdateStatus2To3);
// Status 3 -> 4: Add images // Status 3 -> 4: Add images
router.post('/:id/add-images', contextController.addImages); router.post('/:id/add-images', contextController.addImages);

View File

@@ -78,7 +78,7 @@ const { authenticateToken } = require('../middleware/auth');
* 500: * 500:
* description: Server error * description: Server error
*/ */
router.post('/', authenticateToken, storyController.createStory); router.post('/', storyController.createStory);
/** /**
* @swagger * @swagger
@@ -135,7 +135,7 @@ router.post('/', authenticateToken, storyController.createStory);
* 500: * 500:
* description: Server error * description: Server error
*/ */
router.get('/', authenticateToken, storyController.getAllStories); router.get('/', storyController.getAllStories);
/** /**
* @swagger * @swagger
@@ -160,7 +160,7 @@ router.get('/', authenticateToken, storyController.getAllStories);
* 500: * 500:
* description: Server error * description: Server error
*/ */
router.get('/grade', authenticateToken, storyController.getStoriesByGrade); router.get('/grade', storyController.getStoriesByGrade);
/** /**
* @swagger * @swagger
@@ -185,7 +185,7 @@ router.get('/grade', authenticateToken, storyController.getStoriesByGrade);
* 500: * 500:
* description: Server error * description: Server error
*/ */
router.get('/tag', authenticateToken, storyController.getStoriesByTag); router.get('/tag', storyController.getStoriesByTag);
/** /**
* @swagger * @swagger
@@ -228,7 +228,7 @@ router.get('/tag', authenticateToken, storyController.getStoriesByTag);
* 500: * 500:
* description: Server error * description: Server error
*/ */
router.get('/guide', authenticateToken, storyController.getStoryGuide); router.get('/guide', storyController.getStoryGuide);
/** /**
* @swagger * @swagger
@@ -244,7 +244,7 @@ router.get('/guide', authenticateToken, storyController.getStoryGuide);
* 500: * 500:
* description: Server error * description: Server error
*/ */
router.get('/stats', authenticateToken, storyController.getStoryStats); router.get('/stats', storyController.getStoryStats);
/** /**
* @swagger * @swagger
@@ -269,7 +269,7 @@ router.get('/stats', authenticateToken, storyController.getStoryStats);
* 500: * 500:
* description: Server error * description: Server error
*/ */
router.get('/:id', authenticateToken, storyController.getStoryById); router.get('/:id', storyController.getStoryById);
/** /**
* @swagger * @swagger
@@ -303,7 +303,7 @@ router.get('/:id', authenticateToken, storyController.getStoryById);
* 500: * 500:
* description: Server error * description: Server error
*/ */
router.put('/:id', authenticateToken, storyController.updateStory); router.put('/:id', storyController.updateStory);
/** /**
* @swagger * @swagger
@@ -328,6 +328,6 @@ router.put('/:id', authenticateToken, storyController.updateStory);
* 500: * 500:
* description: Server error * description: Server error
*/ */
router.delete('/:id', authenticateToken, storyController.deleteStory); router.delete('/:id', storyController.deleteStory);
module.exports = router; module.exports = router;

View File

@@ -4,334 +4,347 @@ const vocabController = require('../controllers/vocabController');
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
/** /**
* @swagger * ============================================
* tags: * POST /api/vocabs
* name: Vocabulary * ============================================
* description: Vocabulary management system for curriculum-based language learning * Tạo một vocab entry mới
*/ *
* INPUT:
* {
* text: String (required) - từ thực tế (wash, washes, washing, ate, eaten...)
* ipa: String - phiên âm IPA (ví dụ: /wɒʃ/)
* base_word: String (required) - từ gốc để nhóm lại (wash, eat...)
* form_key: JSON - loại biến thể (V1, V2, V3, V_ing, Noun_Form...), mặc định 'base'
* vi: String - nghĩa tiếng Việt
* category: String - category của từ (Action Verbs, Nouns, etc.)
* topic: String - chủ đề (Food, Travel, Education, etc.)
* 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
* example_sentences: JSON - các câu ví dụ
* tags: JSON Array - các tags để phân loại
* syntax: JSON - vai trò cú pháp
* semantics: JSON - ràng buộc ngữ nghĩa
* constraints: JSON - ràng buộc ngữ pháp
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Vocab object đã tạo (bao gồm vocab_id, created_at, updated_at)
* }
**/
router.post('/', vocabController.createVocab);
/** /**
* @swagger * ============================================
* /api/vocab: * POST /api/vocabs/bulk
* post: * ============================================
* summary: Create a new vocabulary entry * Tạo nhiều vocab entries cùng lúc
* tags: [Vocabulary] *
* security: * INPUT:
* - bearerAuth: [] * {
* requestBody: * vocabs: Array of Vocab objects - mỗi object phải có text và base_word
* required: true * [
* content: * {
* application/json: * text: String (required),
* schema: * base_word: String (required),
* $ref: '#/components/schemas/VocabComplete' * ipa: String,
* example: * vi: String,
* vocab_code: "vocab-001-eat" * ...
* base_word: "eat" * },
* translation: "ăn" * ...
* attributes: * ]
* difficulty_score: 1 * }
* category: "Action Verbs" *
* images: * OUTPUT:
* - "https://cdn.sena.tech/img/eat-main.png" * {
* - "https://cdn.sena.tech/img/eat-context.jpg" * success: Boolean,
* tags: ["daily-routine", "verb"] * message: String,
* mappings: * data: Array of created Vocab objects,
* - book_id: "global-success-1" * count: Number - số lượng vocab đã tạo
* grade: 1 * }
* unit: 2 **/
* lesson: 3 router.post('/bulk', vocabController.bulkCreateVocabs);
* form_key: "v1"
* - book_id: "global-success-2"
* grade: 2
* unit: 5
* lesson: 1
* form_key: "v_ing"
* forms:
* v1:
* text: "eat"
* phonetic: "/iːt/"
* audio: "https://cdn.sena.tech/audio/eat_v1.mp3"
* min_grade: 1
* v_s_es:
* text: "eats"
* phonetic: "/iːts/"
* audio: "https://cdn.sena.tech/audio/eats_s.mp3"
* min_grade: 2
* v_ing:
* text: "eating"
* phonetic: "/ˈiː.tɪŋ/"
* audio: "https://cdn.sena.tech/audio/eating_ing.mp3"
* min_grade: 2
* v2:
* text: "ate"
* phonetic: "/et/"
* audio: "https://cdn.sena.tech/audio/ate_v2.mp3"
* min_grade: 3
* relations:
* synonyms: ["consume", "dine"]
* antonyms: ["fast", "starve"]
* syntax:
* is_subject: false
* is_verb: true
* is_object: false
* is_be: false
* is_adj: false
* verb_type: "transitive"
* semantics:
* can_be_subject_type: ["human", "animal"]
* can_take_object_type: ["food", "plant"]
* word_type: "action"
* constraints:
* requires_object: true
* semantic_object_types: ["food", "plant"]
* responses:
* 201:
* description: Vocabulary created successfully
* 400:
* description: Invalid input
* 500:
* description: Server error
*/
router.post('/', authenticateToken, vocabController.createVocab);
/** /**
* @swagger * ============================================
* /api/vocab: * POST /api/vocabs/search
* get: * ============================================
* summary: Get all vocabulary entries with pagination and filters * Tìm kiếm vocab nâng cao với nhiều filter
* tags: [Vocabulary] *
* security: * INPUT:
* - bearerAuth: [] * {
* parameters: * topic: String (optional) - chủ đề (exact match)
* - in: query * category: String (optional) - loại từ (exact match)
* name: page * base_word: String (optional) - từ gốc (partial match với LIKE)
* schema: * form_key: JSON (optional) - loại biến thể (V1, V2, V3, V_ing, Noun_Form, etc.)
* type: integer * text: String (optional) - từ thực tế (partial match với LIKE)
* default: 1 * vi: String (optional) - nghĩa tiếng Việt (partial match với LIKE)
* description: Page number *
* - in: query * v_type: Boolean (optional) - tìm các biến thể khác của cùng một base_word
* name: limit * base_word_filter: String (optional) - base_word cụ thể (dùng khi v_type=true)
* schema: *
* type: integer * shuffle_pos: Object (optional) - tìm từ thay thế dựa trên syntax
* default: 20 * {
* description: Items per page * is_subject: Boolean,
* - in: query * is_verb: Boolean,
* name: category * is_object: Boolean,
* schema: * is_be: Boolean,
* type: string * is_adj: Boolean,
* description: Filter by category (e.g., "Action Verbs") * is_adv: Boolean,
* - in: query * is_article: Boolean
* name: grade * }
* schema: *
* type: integer * page: Number - trang hiện tại (mặc định: 1)
* description: Filter by grade level * limit: Number - số items mỗi trang (mặc định: 100)
* - in: query * }
* name: book_id *
* schema: * OUTPUT:
* type: string * {
* description: Filter by book ID (e.g., "global-success-1") * success: Boolean,
* - in: query * message: String,
* name: difficulty_min * data: Array of Vocab objects,
* schema: * pagination: {
* type: integer * total: Number,
* description: Minimum difficulty score * page: Number,
* - in: query * limit: Number,
* name: difficulty_max * totalPages: Number
* schema: * }
* type: integer * }
* description: Maximum difficulty score **/
* - in: query router.post('/search', vocabController.searchVocabs);
* name: search
* schema: /**
* type: string * ============================================
* description: Search in base_word, translation, or vocab_code * GET /api/vocabs
* - in: query * ============================================
* name: include_relations * Lấy danh sách tất cả vocab với phân trang và filter
* schema: *
* type: string * INPUT (Query Parameters):
* enum: ['true', 'false'] * {
* default: 'false' * page: Number - trang hiện tại (mặc định: 1)
* description: Include synonyms/antonyms in response * limit: Number - số items mỗi trang (mặc định: 20)
* responses: * category: String - lọc theo category
* 200: * topic: String - lọc theo topic
* description: List of vocabularies * base_word: String - lọc theo base_word chính xác
* 500: * text: String - lọc theo text chính xác
* description: Server error * search: String - tìm kiếm trong text, base_word 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 Vocab objects,
* pagination: {
* total: Number,
* page: Number,
* limit: Number,
* totalPages: Number
* }
* }
*
**/
router.get('/', vocabController.getAllVocabs); router.get('/', vocabController.getAllVocabs);
/** /**
* @swagger * ============================================
* /api/vocab/curriculum: * GET /api/vocabs/stats/overview
* get: * ============================================
* summary: Get vocabularies by curriculum mapping * Lấy thống kê tổng quan về vocab
* tags: [Vocabulary] *
* security: * INPUT: Không có
* - bearerAuth: [] *
* parameters: * OUTPUT:
* - in: query * {
* name: book_id * success: Boolean,
* required: false * message: String,
* schema: * data: {
* type: string * total: {
* description: Book ID (e.g., "global-success-1") * active: Number,
* - in: query * inactive: Number,
* name: grade * all: Number
* required: false * },
* schema: * unique_base_words: Number,
* type: integer * by_category: Array [{category: String, count: Number}],
* description: Grade level * by_topic: Array [{topic: String, count: Number}]
* - in: query * }
* name: unit * }
* schema: **/
* type: integer router.get('/stats/overview', vocabController.getVocabStats);
* description: Unit number
* - in: query
* name: lesson
* schema:
* type: integer
* description: Lesson number
* responses:
* 200:
* description: List of vocabularies for the specified curriculum
* 400:
* description: Invalid parameters
* 500:
* description: Server error
*/
router.get('/curriculum', authenticateToken, vocabController.getVocabsByCurriculum);
/** /**
* @swagger * ============================================
* /api/vocab/guide: * GET /api/vocabs/meta/categories
* get: * ============================================
* summary: Get comprehensive guide for AI to create vocabulary entries * Lấy danh sách tất cả categories
* tags: [Vocabulary] *
* security: * INPUT: Không có
* - bearerAuth: [] *
* responses: * OUTPUT:
* 200: * {
* description: Complete guide with rules, examples, and data structures * success: Boolean,
* content: * message: String,
* application/json: * data: Array of String - danh sách categories,
* schema: * count: Number - số lượng categories
* type: object * }
* properties: **/
* guide_version: router.get('/meta/categories', vocabController.getAllCategories);
* type: string
* last_updated:
* type: string
* data_structure:
* type: object
* rules:
* type: object
* examples:
* type: object
* 500:
* description: Server error
*/
router.get('/guide', authenticateToken, vocabController.getVocabGuide);
/** /**
* @swagger * ============================================
* /api/vocab/stats: * GET /api/vocabs/meta/topics
* get: * ============================================
* summary: Get vocabulary statistics * Lấy danh sách tất cả topics
* tags: [Vocabulary] *
* security: * INPUT: Không có
* - bearerAuth: [] *
* responses: * OUTPUT:
* 200: * {
* description: Vocabulary statistics * success: Boolean,
* 500: * message: String,
* description: Server error * data: Array of String - danh sách topics,
*/ * count: Number - số lượng topics
router.get('/stats', authenticateToken, vocabController.getVocabStats); * }
**/
router.get('/meta/topics', vocabController.getAllTopics);
/** /**
* @swagger * ============================================
* /api/vocab/{id}: * GET /api/vocabs/missing/ipa
* get: * ============================================
* summary: Get vocabulary by ID or code * Lấy tất cả các vocab chưa có IPA
* tags: [Vocabulary] *
* security: * INPUT (Query Parameters):
* - bearerAuth: [] * {
* parameters: * page: Number - trang hiện tại (mặc định: 1),
* - in: path * limit: Number - số items mỗi trang (mặc định: 50)
* name: id * }
* required: true *
* schema: * OUTPUT:
* type: string * {
* description: Vocabulary ID (numeric) or vocab_code (string) * success: Boolean,
* responses: * message: String,
* 200: * data: Array of Vocab objects - các vocab chưa có IPA,
* description: Vocabulary details * pagination: {
* 404: * total: Number,
* description: Vocabulary not found * page: Number,
* 500: * limit: Number,
* description: Server error * totalPages: Number
*/ * }
router.get('/:id', authenticateToken, vocabController.getVocabById); * }
**/
router.get('/missing/ipa', vocabController.getVocabsWithoutIpa);
/** /**
* @swagger * ============================================
* /api/vocab/{id}: * GET /api/vocabs/missing/images
* put: * ============================================
* summary: Update vocabulary entry * Lấy tất cả các vocab chưa đủ hình ảnh
* tags: [Vocabulary] *
* security: * INPUT (Query Parameters):
* - bearerAuth: [] * {
* parameters: * page: Number - trang hiện tại (mặc định: 1),
* - in: path * limit: Number - số items mỗi trang (mặc định: 50)
* name: id * }
* required: true *
* schema: * OUTPUT:
* type: string * {
* description: Vocabulary ID (numeric) or vocab_code (string) * success: Boolean,
* requestBody: * message: String,
* required: true * data: Array of Vocab objects - các vocab chưa đủ hình ảnh,
* content: * pagination: {
* application/json: * total: Number,
* schema: * page: Number,
* $ref: '#/components/schemas/VocabComplete' * limit: Number,
* example: * totalPages: Number
* translation: "ăn uống" * }
* attributes: * }
* difficulty_score: 2 **/
* tags: ["daily-routine", "verb", "food"] router.get('/missing/images', vocabController.getVocabsWithoutImages);
* responses:
* 200:
* description: Vocabulary updated successfully
* 404:
* description: Vocabulary not found
* 500:
* description: Server error
*/
router.put('/:id', authenticateToken, vocabController.updateVocab);
/** /**
* @swagger * ============================================
* /api/vocab/{id}: * GET /api/vocabs/:id
* delete: * ============================================
* summary: Delete vocabulary (soft delete) * Lấy chi tiết một vocab theo ID
* tags: [Vocabulary] *
* security: * INPUT (URL Parameter):
* - bearerAuth: [] * {
* parameters: * id: UUID - vocab_id của vocab cần lấy
* - in: path * }
* name: id *
* required: true * OUTPUT:
* schema: * {
* type: string * success: Boolean,
* description: Vocabulary ID (numeric) or vocab_code (string) * message: String,
* responses: * data: Vocab object với đầy đủ thông tin
* 200: * }
* description: Vocabulary deleted successfully **/
* 404: router.get('/:id', vocabController.getVocabById);
* description: Vocabulary not found
* 500: /**
* description: Server error * ============================================
*/ * PUT /api/vocabs/:id
router.delete('/:id', authenticateToken, vocabController.deleteVocab); * ============================================
* Cập nhật thông tin vocab
*
* INPUT (URL Parameter + Body):
* {
* id: UUID - vocab_id cần update
* Body: Object - các trường cần update (có thể update một hoặc nhiều trường)
* {
* text: String,
* ipa: String,
* base_word: String,
* form_key: JSON,
* vi: String,
* category: String,
* topic: String,
* image_small: JSON Array,
* image_square: JSON Array,
* image_normal: JSON Array,
* audio: JSON Array,
* example_sentences: JSON,
* tags: JSON Array,
* syntax: JSON,
* semantics: JSON,
* constraints: JSON,
* is_active: Boolean
* }
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String,
* data: Updated Vocab object
* }
* **/
router.put('/:id', vocabController.updateVocab);
/**
* ============================================
* DELETE /api/vocabs/:id
* ============================================
* Xóa mềm vocab (set is_active = false)
*
* INPUT (URL Parameter):
* {
* id: UUID - vocab_id cần xóa
* }
*
* OUTPUT:
* {
* success: Boolean,
* message: String
* }
**/
router.delete('/:id', vocabController.deleteVocab);
module.exports = router; module.exports = router;