This commit is contained in:
4
app.js
4
app.js
@@ -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);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
@@ -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: {
|
||||||
|
|||||||
@@ -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']
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user