update new defined
All checks were successful
Deploy to Production / deploy (push) Successful in 19s
All checks were successful
Deploy to Production / deploy (push) Successful in 19s
This commit is contained in:
@@ -206,8 +206,8 @@ class GameTypeController {
|
|||||||
await gameType.update(updateData);
|
await gameType.update(updateData);
|
||||||
|
|
||||||
// Invalidate cache
|
// Invalidate cache
|
||||||
await cacheUtils.del(`game_type:${id}`);
|
await cacheUtils.delete(`game_type:${id}`);
|
||||||
await cacheUtils.del(`game_type:type:${gameType.type}`);
|
await cacheUtils.delete(`game_type:type:${gameType.type}`);
|
||||||
await cacheUtils.deletePattern('game_types:*');
|
await cacheUtils.deletePattern('game_types:*');
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -241,8 +241,8 @@ class GameTypeController {
|
|||||||
await gameType.destroy();
|
await gameType.destroy();
|
||||||
|
|
||||||
// Invalidate cache
|
// Invalidate cache
|
||||||
await cacheUtils.del(`game_type:${id}`);
|
await cacheUtils.delete(`game_type:${id}`);
|
||||||
await cacheUtils.del(`game_type:type:${typeCode}`);
|
await cacheUtils.delete(`game_type:type:${typeCode}`);
|
||||||
await cacheUtils.deletePattern('game_types:*');
|
await cacheUtils.deletePattern('game_types:*');
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -340,6 +340,411 @@ class GameTypeController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get developer guide for game code
|
||||||
|
* GET /api/game-types/guide?gameCode=G2510S1T30
|
||||||
|
*/
|
||||||
|
async getGuide(req, res, next) {
|
||||||
|
try {
|
||||||
|
const { gameCode } = req.query;
|
||||||
|
|
||||||
|
if (!gameCode) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'gameCode is required',
|
||||||
|
example: '/api/game-types/guide?gameCode=G2510S1T30'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||||||
|
const match = gameCode.match(regex);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid gameCode format!',
|
||||||
|
format: 'Gxxxx or GxxxxSxTxx',
|
||||||
|
examples: [
|
||||||
|
'G1400 - Quiz Text to Text (4 options)',
|
||||||
|
'G1510 - Quiz Image to Text (5 options)',
|
||||||
|
'G2410S1T30 - Sort 4 items, shuffled, 30s timer',
|
||||||
|
'G3500 - Memory Card 5 pairs'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = match[1];
|
||||||
|
const count = match[2];
|
||||||
|
const qIdx = match[3];
|
||||||
|
const oIdx = match[4];
|
||||||
|
const shuffle = match[5] !== undefined ? match[5] : '1';
|
||||||
|
const time = match[6] !== undefined ? match[6] : '0';
|
||||||
|
|
||||||
|
const types = { '0': 'Text', '1': 'Image', '2': 'Audio' };
|
||||||
|
|
||||||
|
let gameName = '', instruction = '', displayMode = '';
|
||||||
|
|
||||||
|
switch(category) {
|
||||||
|
case '1':
|
||||||
|
gameName = "Quiz (Trắc nghiệm)";
|
||||||
|
instruction = `Người dùng chọn 1 đáp án đúng trong ${count} options`;
|
||||||
|
displayMode = `Question: ${types[qIdx]} → Options: ${types[oIdx]}`;
|
||||||
|
break;
|
||||||
|
case '2':
|
||||||
|
if (qIdx === '0' && oIdx === '0') {
|
||||||
|
gameName = "Sort Word (Sắp xếp từ)";
|
||||||
|
instruction = `Sắp xếp ${count} từ/ký tự thành chuỗi hoàn chỉnh`;
|
||||||
|
} else {
|
||||||
|
gameName = "Sequences (Sắp xếp chuỗi)";
|
||||||
|
instruction = `Sắp xếp ${count} items theo đúng thứ tự`;
|
||||||
|
}
|
||||||
|
displayMode = `Hint: ${types[qIdx]} → Items: ${types[oIdx]}`;
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
if (qIdx === oIdx) {
|
||||||
|
gameName = "Memory Card (Trí nhớ)";
|
||||||
|
instruction = `Lật và ghép ${count} cặp thẻ giống nhau`;
|
||||||
|
} else {
|
||||||
|
gameName = "Matching (Nối cặp)";
|
||||||
|
instruction = `Nối ${count} items từ 2 nhóm với nhau`;
|
||||||
|
}
|
||||||
|
displayMode = `Group A: ${types[qIdx]} ↔ Group B: ${types[oIdx]}`;
|
||||||
|
break;
|
||||||
|
case '4':
|
||||||
|
gameName = "Lesson Card (Học từ vựng)";
|
||||||
|
instruction = `Hiển thị ${count} thẻ học với context, audio, image, và meaning`;
|
||||||
|
displayMode = `Context: ${types[qIdx]} | Meaning: ${types[oIdx]} (Always with Audio & Image)`;
|
||||||
|
break;
|
||||||
|
case '5':
|
||||||
|
if (qIdx === '0' && oIdx === '0') {
|
||||||
|
gameName = "Sort Sentence (Sắp xếp câu)";
|
||||||
|
instruction = `Sắp xếp ${count} câu theo đúng thứ tự`;
|
||||||
|
} else {
|
||||||
|
gameName = "Sentence Sequences (Sắp xếp câu theo trình tự)";
|
||||||
|
instruction = `Sắp xếp ${count} câu/items theo đúng thứ tự`;
|
||||||
|
}
|
||||||
|
displayMode = `Hint: ${types[qIdx]} → Items: ${types[oIdx]}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate guide text
|
||||||
|
let guide = '';
|
||||||
|
guide += `║SENA SDK - DEVELOPER GUIDE║\n`;
|
||||||
|
guide += `📊 GAME ANALYSIS\n`;
|
||||||
|
guide += `Game Type : ${gameName}\n`;
|
||||||
|
guide += `Objective : ${instruction}\n`;
|
||||||
|
guide += `Display Mode : ${displayMode}\n`;
|
||||||
|
guide += `Items Count : ${count}\n`;
|
||||||
|
guide += `Shuffle : ${shuffle === '1' ? 'YES (call sdk.start() to shuffle)' : 'NO (S0)'}\n`;
|
||||||
|
guide += `Time Limit : ${time === '0' ? 'Unlimited' : time + ' seconds'}\n\n`;
|
||||||
|
|
||||||
|
// Generate sample data based on game type
|
||||||
|
const sampleData = GameTypeController.prototype.generateSampleData(category, count, qIdx, oIdx, shuffle, time, gameCode);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
gameCode,
|
||||||
|
analysis: {
|
||||||
|
category: parseInt(category),
|
||||||
|
gameName,
|
||||||
|
instruction,
|
||||||
|
displayMode,
|
||||||
|
itemsCount: parseInt(count),
|
||||||
|
shuffle: shuffle === '1',
|
||||||
|
timeLimit: parseInt(time),
|
||||||
|
questionType: types[qIdx],
|
||||||
|
optionType: types[oIdx]
|
||||||
|
},
|
||||||
|
guide,
|
||||||
|
sampleData
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate sample data based on game type
|
||||||
|
*/
|
||||||
|
generateSampleData(category, count, qIdx, oIdx, shuffle, time, gameCode) {
|
||||||
|
const types = { '0': 'text', '1': 'image', '2': 'audio' };
|
||||||
|
const questionType = types[qIdx];
|
||||||
|
const optionType = types[oIdx];
|
||||||
|
|
||||||
|
let sample = {
|
||||||
|
gameCode: gameCode,
|
||||||
|
config: {
|
||||||
|
time: {
|
||||||
|
value: parseInt(time),
|
||||||
|
description: time === '0' ? 'Không giới hạn thời gian' : `Giới hạn thời gian trả lời ${time} giây`,
|
||||||
|
options: [
|
||||||
|
{ value: 0, label: 'Không giới hạn thời gian' },
|
||||||
|
{ value: 30, label: 'Giới hạn 30 giây' },
|
||||||
|
{ value: 60, label: 'Giới hạn 60 giây' },
|
||||||
|
{ value: 90, label: 'Giới hạn 90 giây' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
shuffle: {
|
||||||
|
value: shuffle === '1',
|
||||||
|
description: shuffle === '1' ? 'Có xáo trộn các đáp án' : 'Không xáo trộn các đáp án',
|
||||||
|
options: [
|
||||||
|
{ value: 0, label: 'Không xáo trộn các đáp án' },
|
||||||
|
{ value: 1, label: 'Có xáo trộn các đáp án' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
question: {},
|
||||||
|
options: [],
|
||||||
|
answer: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate question
|
||||||
|
if (category === '1') {
|
||||||
|
// Quiz
|
||||||
|
if (questionType === 'text') {
|
||||||
|
sample.question = { text: 'What is the capital of France?' };
|
||||||
|
} else if (questionType === 'image') {
|
||||||
|
sample.question = { image: 'https://example.com/question.jpg' };
|
||||||
|
} else {
|
||||||
|
sample.question = { audio: 'https://example.com/question.mp3' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate options
|
||||||
|
const answers = ['Paris', 'London', 'Berlin', 'Madrid', 'Rome', 'Amsterdam', 'Vienna', 'Brussels', 'Oslo'];
|
||||||
|
for (let i = 0; i < parseInt(count); i++) {
|
||||||
|
if (optionType === 'text') {
|
||||||
|
sample.options.push({ text: answers[i], isCorrect: i === 0 });
|
||||||
|
} else if (optionType === 'image') {
|
||||||
|
sample.options.push({
|
||||||
|
text: answers[i],
|
||||||
|
image: `https://example.com/option${i+1}.jpg`,
|
||||||
|
isCorrect: i === 0
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sample.options.push({
|
||||||
|
text: answers[i],
|
||||||
|
audio: `https://example.com/option${i+1}.mp3`,
|
||||||
|
isCorrect: i === 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sample.answer = answers[0]; // Always return text for correct answer
|
||||||
|
|
||||||
|
} else if (category === '2') {
|
||||||
|
// Sort/Sequences - for letters/characters with blanks
|
||||||
|
// G2xyz: x=number of blanks, y=question type (0=text,1=image,2=audio), z=always text (ignore)
|
||||||
|
const numBlanks = parseInt(count); // Number of blanks and options
|
||||||
|
|
||||||
|
// Question based on type (qIdx)
|
||||||
|
if (questionType === 'text') {
|
||||||
|
sample.question = 'Sắp xếp các chữ cái để tạo thành tên con vật';
|
||||||
|
} else if (questionType === 'image') {
|
||||||
|
sample.question = 'https://example.com/question_hint.jpg';
|
||||||
|
} else {
|
||||||
|
sample.question = 'https://example.com/question_hint.mp3';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full answer array
|
||||||
|
const fullWords = [
|
||||||
|
['C', 'H', 'I', 'C', 'K', 'E', 'N'],
|
||||||
|
['T', 'I', 'G', 'E', 'R'],
|
||||||
|
['L', 'I', 'O', 'N'],
|
||||||
|
['E', 'A', 'G', 'L', 'E'],
|
||||||
|
['D', 'O', 'L', 'P', 'H', 'I', 'N'],
|
||||||
|
['M', 'O', 'N', 'K', 'E', 'Y'],
|
||||||
|
['E', 'L', 'E', 'P', 'H', 'A', 'N', 'T']
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedWord = fullWords[0]; // Use CHICKEN as default
|
||||||
|
|
||||||
|
// Create hint with exactly 'count' number of blanks
|
||||||
|
// Distribute blanks evenly across the word
|
||||||
|
const blankPositions = [];
|
||||||
|
if (numBlanks >= selectedWord.length) {
|
||||||
|
// If blanks >= word length, make all positions blank
|
||||||
|
for (let i = 0; i < selectedWord.length; i++) {
|
||||||
|
blankPositions.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Distribute blanks evenly
|
||||||
|
const step = Math.floor(selectedWord.length / numBlanks);
|
||||||
|
for (let i = 0; i < numBlanks; i++) {
|
||||||
|
blankPositions.push(Math.min(i * step + Math.floor(step / 2), selectedWord.length - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hintValue = selectedWord.map((letter, idx) => {
|
||||||
|
return blankPositions.includes(idx) ? '_' : letter;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hint is a separate object, not inside question (always text)
|
||||||
|
sample.hint = {
|
||||||
|
type: 'display',
|
||||||
|
value: hintValue
|
||||||
|
};
|
||||||
|
|
||||||
|
// Options are the letters that need to be filled in blanks (always text)
|
||||||
|
sample.options = blankPositions.map(pos => selectedWord[pos]);
|
||||||
|
|
||||||
|
sample.answer = selectedWord;
|
||||||
|
|
||||||
|
} else if (category === '3') {
|
||||||
|
// Instruction/hint is text
|
||||||
|
sample.question = questionType === optionType ?
|
||||||
|
{ instruction: 'Flip and match pairs of cards' } :
|
||||||
|
{ instruction: 'Match items from two groups' };
|
||||||
|
|
||||||
|
// Memory/Matching
|
||||||
|
for (let i = 0; i < parseInt(count); i++) {
|
||||||
|
if (questionType === optionType) {
|
||||||
|
// Memory Card - same type pairs (need 2 cards per pair)
|
||||||
|
if (optionType === 'text') {
|
||||||
|
const numbers = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'];
|
||||||
|
sample.options.push({ text: numbers[i], pairId: i });
|
||||||
|
sample.options.push({ text: `${i+1}`, pairId: i });
|
||||||
|
} else if (optionType === 'image') {
|
||||||
|
sample.options.push({
|
||||||
|
image: `https://example.com/card${i+1}a.jpg`,
|
||||||
|
pairId: i
|
||||||
|
});
|
||||||
|
sample.options.push({
|
||||||
|
image: `https://example.com/card${i+1}b.jpg`,
|
||||||
|
pairId: i
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sample.options.push({
|
||||||
|
audio: `https://example.com/card${i+1}a.mp3`,
|
||||||
|
pairId: i
|
||||||
|
});
|
||||||
|
sample.options.push({
|
||||||
|
audio: `https://example.com/card${i+1}b.mp3`,
|
||||||
|
pairId: i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Matching - different types
|
||||||
|
const left = questionType === 'text' ?
|
||||||
|
{ text: `Item ${i+1}` } :
|
||||||
|
{ image: `https://example.com/left${i+1}.jpg` };
|
||||||
|
const right = optionType === 'text' ?
|
||||||
|
{ text: `Match ${i+1}` } :
|
||||||
|
{ image: `https://example.com/right${i+1}.jpg` };
|
||||||
|
|
||||||
|
sample.options.push({
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
pairId: i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Answer is array of pairIds in order: [0,0,1,1] for 2 pairs
|
||||||
|
sample.answer = sample.options.map(o => o.pairId);
|
||||||
|
|
||||||
|
} else if (category === '4') {
|
||||||
|
// Lesson Card - Always return all 4 fields (context, audio, image, meaning)
|
||||||
|
sample.question = { instruction: 'Learn vocabulary with context, audio, image, and meaning' };
|
||||||
|
sample.options = [];
|
||||||
|
|
||||||
|
const contexts = [
|
||||||
|
'The cat is sleeping on the sofa',
|
||||||
|
'I drink water every day',
|
||||||
|
'She loves to read books',
|
||||||
|
'They play football in the park',
|
||||||
|
'We eat breakfast at 7 AM',
|
||||||
|
'He drives a red car',
|
||||||
|
'The sun rises in the east',
|
||||||
|
'Birds fly in the sky',
|
||||||
|
'Children study at school'
|
||||||
|
];
|
||||||
|
|
||||||
|
const meanings = [
|
||||||
|
'Con mèo đang ngủ trên ghế sofa',
|
||||||
|
'Tôi uống nước mỗi ngày',
|
||||||
|
'Cô ấy thích đọc sách',
|
||||||
|
'Họ chơi bóng đá ở công viên',
|
||||||
|
'Chúng tôi ăn sáng lúc 7 giờ',
|
||||||
|
'Anh ấy lái một chiếc xe màu đỏ',
|
||||||
|
'Mặt trời mọc ở phía đông',
|
||||||
|
'Chim bay trên bầu trời',
|
||||||
|
'Trẻ em học ở trường'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < parseInt(count); i++) {
|
||||||
|
let card = {
|
||||||
|
context: contexts[i],
|
||||||
|
audio: `https://example.com/audio${i+1}.mp3`,
|
||||||
|
image: `https://example.com/image${i+1}.jpg`,
|
||||||
|
meaning: meanings[i]
|
||||||
|
};
|
||||||
|
sample.options.push(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
sample.answer = 'User completes all lesson cards';
|
||||||
|
|
||||||
|
} else if (category === '5') {
|
||||||
|
// Sort/Sequences for words (similar to G2 but with words instead of letters)
|
||||||
|
// G5xyz: x=number of blanks, y=question type (0=text,1=image,2=audio), z=always text (ignore)
|
||||||
|
const numBlanks = parseInt(count); // Number of blanks and options
|
||||||
|
|
||||||
|
// Question based on type (qIdx)
|
||||||
|
if (questionType === 'text') {
|
||||||
|
sample.question = 'Sắp xếp các từ để tạo thành câu hoàn chỉnh';
|
||||||
|
} else if (questionType === 'image') {
|
||||||
|
sample.question = 'https://example.com/sentence_question.jpg';
|
||||||
|
} else {
|
||||||
|
sample.question = 'https://example.com/sentence_question.mp3';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full answer array (words instead of letters)
|
||||||
|
const sentences = [
|
||||||
|
['I', 'wake', 'up', 'early', 'in', 'the', 'morning'],
|
||||||
|
['She', 'goes', 'to', 'school', 'by', 'bus'],
|
||||||
|
['They', 'are', 'playing', 'in', 'the', 'park'],
|
||||||
|
['We', 'will', 'visit', 'the', 'museum', 'tomorrow'],
|
||||||
|
['He', 'has', 'finished', 'his', 'homework'],
|
||||||
|
['The', 'weather', 'is', 'nice', 'today'],
|
||||||
|
['My', 'family', 'likes', 'to', 'travel']
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedSentence = sentences[0]; // Use first sentence as default
|
||||||
|
|
||||||
|
// Create hint with exactly 'count' number of blanks
|
||||||
|
// Distribute blanks evenly across the sentence
|
||||||
|
const blankPositions = [];
|
||||||
|
if (numBlanks >= selectedSentence.length) {
|
||||||
|
// If blanks >= sentence length, make all positions blank
|
||||||
|
for (let i = 0; i < selectedSentence.length; i++) {
|
||||||
|
blankPositions.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Distribute blanks evenly
|
||||||
|
const step = Math.floor(selectedSentence.length / numBlanks);
|
||||||
|
for (let i = 0; i < numBlanks; i++) {
|
||||||
|
blankPositions.push(Math.min(i * step + Math.floor(step / 2), selectedSentence.length - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hintValue = selectedSentence.map((word, idx) => {
|
||||||
|
return blankPositions.includes(idx) ? '_' : word;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hint is a separate object, not inside question (always text words)
|
||||||
|
sample.hint = {
|
||||||
|
type: 'display',
|
||||||
|
value: hintValue
|
||||||
|
};
|
||||||
|
|
||||||
|
// Options are the words that need to be filled in blanks (always text)
|
||||||
|
sample.options = blankPositions.map(pos => selectedSentence[pos]);
|
||||||
|
|
||||||
|
sample.answer = selectedSentence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new GameTypeController();
|
module.exports = new GameTypeController();
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ const GameType = sequelize.define('game_types', {
|
|||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
comment: 'Mô tả loại game (tiếng Việt)'
|
comment: 'Mô tả loại game (tiếng Việt)'
|
||||||
},
|
},
|
||||||
|
category : {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
comment: 'Danh mục loại game: quiz, puzzle, adventure, etc.'
|
||||||
|
},
|
||||||
type: {
|
type: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING(100),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|||||||
@@ -436,6 +436,10 @@
|
|||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<div class="game-card">
|
<div class="game-card">
|
||||||
|
<button class="btn btn-secondary" onclick="loadAllGames()" style="margin-bottom: 20px;">
|
||||||
|
← Quay lại danh sách
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="game-info">
|
<div class="game-info">
|
||||||
<img src="${game.thumbnail}" alt="${game.title}" class="game-thumbnail" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22200%22%3E%3Crect width=%22200%22 height=%22200%22 fill=%22%23ddd%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22%3ENo Image%3C/text%3E%3C/svg%3E'">
|
<img src="${game.thumbnail}" alt="${game.title}" class="game-thumbnail" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22200%22%3E%3Crect width=%22200%22 height=%22200%22 fill=%22%23ddd%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22%3ENo Image%3C/text%3E%3C/svg%3E'">
|
||||||
<div class="game-details">
|
<div class="game-details">
|
||||||
|
|||||||
560
public/gametype-manager.html
Normal file
560
public/gametype-manager.html
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Game Type Manager - SENA</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 1.1em;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-active {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-inactive {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-premium {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-view.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🎮 Game Type Manager</h1>
|
||||||
|
<p>Quản lý các loại trò chơi giáo dục</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<!-- List View -->
|
||||||
|
<div id="listView" class="list-view">
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-primary" onclick="showCreateForm()">➕ Tạo mới Game Type</button>
|
||||||
|
<button class="btn btn-secondary" onclick="loadGameTypes()">🔄 Làm mới</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message"></div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table id="gameTypesTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Type Code</th>
|
||||||
|
<th>EN Title</th>
|
||||||
|
<th>VI Title</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Thumb</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="gameTypesBody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="loading">Đang tải dữ liệu...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<div id="formView" class="form-view">
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn btn-secondary" onclick="showListView()">← Quay lại danh sách</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="formTitle">Tạo mới Game Type</h2>
|
||||||
|
|
||||||
|
<div id="formMessage"></div>
|
||||||
|
|
||||||
|
<form id="gameTypeForm" onsubmit="saveGameType(event)">
|
||||||
|
<input type="hidden" id="gameTypeId" />
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Type Code * (Mã định danh)</label>
|
||||||
|
<input type="text" id="type" required placeholder="VD: G1400, counting_quiz">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Category</label>
|
||||||
|
<select id="category">
|
||||||
|
<option value="">-- Chọn category --</option>
|
||||||
|
<option value="Quiz">Quiz</option>
|
||||||
|
<option value="Sort">Sort</option>
|
||||||
|
<option value="Memory">Memory</option>
|
||||||
|
<option value="Lesson">Lesson</option>
|
||||||
|
<option value="Sentence">Sentence</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>English Title *</label>
|
||||||
|
<input type="text" id="en_title" required placeholder="English name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Vietnamese Title *</label>
|
||||||
|
<input type="text" id="vi_title" required placeholder="Tên tiếng Việt">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>English Description</label>
|
||||||
|
<textarea id="en_desc" placeholder="English description"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Vietnamese Description</label>
|
||||||
|
<textarea id="vi_desc" placeholder="Mô tả tiếng Việt"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Thumbnail URL</label>
|
||||||
|
<input type="text" id="thumb" placeholder="https://example.com/thumb.jpg">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Data (JSON)</label>
|
||||||
|
<textarea id="data" placeholder='{"sample": "data"}' style="font-family: monospace;"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Config (JSON)</label>
|
||||||
|
<textarea id="config" placeholder='{"setting": "value"}' style="font-family: monospace;"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit" class="btn btn-success">💾 Lưu</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="showListView()">Hủy</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Auto-detect API base URL
|
||||||
|
const API_BASE = window.location.origin + '/api/game-types';
|
||||||
|
|
||||||
|
// Load all game types
|
||||||
|
async function loadGameTypes() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}?limit=100`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
renderGameTypes(data.data.gameTypes);
|
||||||
|
} else {
|
||||||
|
showMessage('error', 'Không thể tải dữ liệu');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('error', 'Lỗi kết nối: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render game types table
|
||||||
|
function renderGameTypes(gameTypes) {
|
||||||
|
const tbody = document.getElementById('gameTypesBody');
|
||||||
|
|
||||||
|
if (gameTypes.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;">Chưa có game type nào</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = gameTypes.map(gt => `
|
||||||
|
<tr>
|
||||||
|
<td>${gt.id.substring(0, 8)}...</td>
|
||||||
|
<td><strong>${gt.type}</strong></td>
|
||||||
|
<td>${gt.en_title}</td>
|
||||||
|
<td>${gt.vi_title}</td>
|
||||||
|
<td>${gt.category || '-'}</td>
|
||||||
|
<td>${gt.thumb ? '🖼️' : '-'}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary" style="padding:8px 12px; font-size:14px;" onclick="editGameType('${gt.id}')">✏️ Sửa</button>
|
||||||
|
<button class="btn btn-danger" style="padding:8px 12px; font-size:14px;" onclick="deleteGameType('${gt.id}')">🗑️ Xóa</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show create form
|
||||||
|
function showCreateForm() {
|
||||||
|
document.getElementById('formTitle').textContent = 'Tạo mới Game Type';
|
||||||
|
document.getElementById('gameTypeForm').reset();
|
||||||
|
document.getElementById('gameTypeId').value = '';
|
||||||
|
showFormView();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit game type
|
||||||
|
async function editGameType(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/${id}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const gt = data.data;
|
||||||
|
document.getElementById('formTitle').textContent = 'Cập nhật Game Type';
|
||||||
|
document.getElementById('gameTypeId').value = gt.id;
|
||||||
|
document.getElementById('type').value = gt.type;
|
||||||
|
document.getElementById('en_title').value = gt.en_title;
|
||||||
|
document.getElementById('vi_title').value = gt.vi_title;
|
||||||
|
document.getElementById('en_desc').value = gt.en_desc || '';
|
||||||
|
document.getElementById('vi_desc').value = gt.vi_desc || '';
|
||||||
|
document.getElementById('category').value = gt.category || '';
|
||||||
|
document.getElementById('thumb').value = gt.thumb || '';
|
||||||
|
document.getElementById('data').value = gt.data ? JSON.stringify(gt.data, null, 2) : '';
|
||||||
|
document.getElementById('config').value = gt.config ? JSON.stringify(gt.config, null, 2) : '';
|
||||||
|
showFormView();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('error', 'Lỗi tải dữ liệu: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save game type (create or update)
|
||||||
|
async function saveGameType(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const id = document.getElementById('gameTypeId').value;
|
||||||
|
|
||||||
|
// Helper function to convert empty string to null
|
||||||
|
const nullIfEmpty = (value) => value === '' ? null : value;
|
||||||
|
|
||||||
|
// Parse JSON fields
|
||||||
|
let dataJson = null;
|
||||||
|
let configJson = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataText = document.getElementById('data').value.trim();
|
||||||
|
if (dataText) dataJson = JSON.parse(dataText);
|
||||||
|
} catch (e) {
|
||||||
|
showFormMessage('error', 'Data JSON không hợp lệ: ' + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configText = document.getElementById('config').value.trim();
|
||||||
|
if (configText) configJson = JSON.parse(configText);
|
||||||
|
} catch (e) {
|
||||||
|
showFormMessage('error', 'Config JSON không hợp lệ: ' + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
type: document.getElementById('type').value,
|
||||||
|
en_title: document.getElementById('en_title').value,
|
||||||
|
vi_title: document.getElementById('vi_title').value,
|
||||||
|
en_desc: nullIfEmpty(document.getElementById('en_desc').value),
|
||||||
|
vi_desc: nullIfEmpty(document.getElementById('vi_desc').value),
|
||||||
|
category: nullIfEmpty(document.getElementById('category').value),
|
||||||
|
thumb: nullIfEmpty(document.getElementById('thumb').value),
|
||||||
|
data: dataJson,
|
||||||
|
config: configJson
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = id ? `${API_BASE}/${id}` : API_BASE;
|
||||||
|
const method = id ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('Save result:', result); // Debug log
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showMessage('success', id ? 'Cập nhật thành công!' : 'Tạo mới thành công!');
|
||||||
|
setTimeout(() => {
|
||||||
|
showListView();
|
||||||
|
loadGameTypes();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
const errorMsg = result.error || result.message || JSON.stringify(result);
|
||||||
|
showFormMessage('error', errorMsg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save error:', error);
|
||||||
|
showFormMessage('error', 'Lỗi kết nối: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete game type
|
||||||
|
async function deleteGameType(id) {
|
||||||
|
if (!confirm('Bạn có chắc chắn muốn xóa game type này?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
showMessage('success', 'Xóa thành công!');
|
||||||
|
loadGameTypes();
|
||||||
|
} else {
|
||||||
|
showMessage('error', result.error || 'Không thể xóa');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('error', 'Lỗi kết nối: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide views
|
||||||
|
function showListView() {
|
||||||
|
document.getElementById('listView').style.display = 'block';
|
||||||
|
document.getElementById('formView').classList.remove('active');
|
||||||
|
document.getElementById('formMessage').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFormView() {
|
||||||
|
document.getElementById('listView').style.display = 'none';
|
||||||
|
document.getElementById('formView').classList.add('active');
|
||||||
|
document.getElementById('message').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show messages
|
||||||
|
function showMessage(type, text) {
|
||||||
|
const messageDiv = document.getElementById('message');
|
||||||
|
messageDiv.innerHTML = `<div class="${type}">${text}</div>`;
|
||||||
|
setTimeout(() => { messageDiv.innerHTML = ''; }, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFormMessage(type, text) {
|
||||||
|
const messageDiv = document.getElementById('formMessage');
|
||||||
|
messageDiv.innerHTML = `<div class="${type}">${text}</div>`;
|
||||||
|
setTimeout(() => { messageDiv.innerHTML = ''; }, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
loadGameTypes();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -7,6 +7,9 @@ const gameTypeController = require('../controllers/gameTypeController');
|
|||||||
* Base path: /api/game-types
|
* Base path: /api/game-types
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Get developer guide for game code
|
||||||
|
router.get('/guide', gameTypeController.getGuide);
|
||||||
|
|
||||||
// Get all game types
|
// Get all game types
|
||||||
router.get('/', gameTypeController.getAllGameTypes);
|
router.get('/', gameTypeController.getAllGameTypes);
|
||||||
|
|
||||||
|
|||||||
26
scripts/sync-gametype.js
Normal file
26
scripts/sync-gametype.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
const GameType = require('../models/GameType');
|
||||||
|
|
||||||
|
async function syncGameType() {
|
||||||
|
try {
|
||||||
|
console.log('Starting GameType model synchronization...');
|
||||||
|
|
||||||
|
// Sync with alter: true to update existing table structure
|
||||||
|
await GameType.sync({ alter: true });
|
||||||
|
|
||||||
|
console.log('✅ GameType model synchronized successfully!');
|
||||||
|
console.log('Table structure has been updated to match the model definition.');
|
||||||
|
|
||||||
|
// Test query to verify
|
||||||
|
const count = await GameType.count();
|
||||||
|
console.log(`Current GameType records: ${count}`);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error syncing GameType model:', error.message);
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncGameType();
|
||||||
Reference in New Issue
Block a user