This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const { Context, Vocab } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* Context Controller - Workflow-based status management
|
||||
@@ -11,7 +12,7 @@ class ContextController {
|
||||
*/
|
||||
async createContext(req, res, next) {
|
||||
try {
|
||||
const { title, desc, grade, type, type_image , reference_id } = req.body;
|
||||
const { title, desc, grade, type, type_image, reference_id } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!title || !desc || !grade) {
|
||||
@@ -58,18 +59,96 @@ class ContextController {
|
||||
async getContextsByStatus(req, res, next) {
|
||||
try {
|
||||
const { status } = req.params;
|
||||
const { page = 1, limit = 50, type, grade } = req.query;
|
||||
const { page = 1, limit = 50, type, grade, title, date, from, to, sort } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const where = { status: parseInt(status) };
|
||||
if (type) where.type = type;
|
||||
if (grade) where.grade = parseInt(grade);
|
||||
if (title) where.title = { [Op.like]: `%${title}%` };
|
||||
|
||||
// Hàm helper để chuẩn hóa và parse date cho dải thời gian
|
||||
const parseDateRange = (dStr, isEndDate = false) => {
|
||||
if (!dStr) return null;
|
||||
let s = String(dStr).replace('_', 'T').replace(' ', 'T');
|
||||
if (s.includes(':') && !s.includes('Z') && !s.match(/[+-]\d{2}:?\d{2}$/)) {
|
||||
s += 'Z';
|
||||
}
|
||||
const d = new Date(s);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
if (!s.includes(':') && isEndDate) {
|
||||
d.setHours(23, 59, 59, 999);
|
||||
}
|
||||
return d;
|
||||
};
|
||||
|
||||
if (from || to) {
|
||||
where.updated_at = {};
|
||||
if (from) {
|
||||
const startDate = parseDateRange(from);
|
||||
if (startDate) where.updated_at[Op.gte] = startDate;
|
||||
}
|
||||
if (to) {
|
||||
const endDate = parseDateRange(to, true);
|
||||
if (endDate) where.updated_at[Op.lte] = endDate;
|
||||
}
|
||||
} else if (date) {
|
||||
// Chuẩn hóa chuỗi ngày tháng: thay '_' hoặc khoảng cách bằng 'T' để dễ xử lý
|
||||
let dateString = String(date).replace('_', 'T').replace(' ', 'T');
|
||||
|
||||
// Nếu chuỗi có giờ phút (có dấu :) nhưng chưa có múi giờ (Z hoặc +/-)
|
||||
// Ta mặc định là giờ UTC để khớp chính xác với những gì người dùng thấy trong DB
|
||||
if (dateString.includes(':') && !dateString.includes('Z') && !dateString.match(/[+-]\d{2}:?\d{2}$/)) {
|
||||
dateString += 'Z';
|
||||
}
|
||||
|
||||
const searchDate = new Date(dateString);
|
||||
|
||||
if (!isNaN(searchDate.getTime())) {
|
||||
// Kiểm tra xem có dấu ':' (có giờ phút) không
|
||||
const hasTime = dateString.includes(':');
|
||||
|
||||
if (hasTime) {
|
||||
// Kiểm tra xem có cung cấp đến giây không (vd: 09:08:18 -> 3 phần)
|
||||
const isSecondsProvided = dateString.split(':').length === 3;
|
||||
|
||||
if (isSecondsProvided) {
|
||||
// Tìm chính xác giây đó (dải 1000ms)
|
||||
const startTime = Math.floor(searchDate.getTime() / 1000) * 1000;
|
||||
const startRange = new Date(startTime);
|
||||
const endRange = new Date(startTime + 1000);
|
||||
where.updated_at = { [Op.gte]: startRange, [Op.lt]: endRange };
|
||||
} else {
|
||||
// Chỉ có giờ:phút, tìm trong cả phút đó
|
||||
const startTime = Math.floor(searchDate.getTime() / 60000) * 60000;
|
||||
const startRange = new Date(startTime);
|
||||
const endRange = new Date(startTime + 60000);
|
||||
where.updated_at = { [Op.gte]: startRange, [Op.lt]: endRange };
|
||||
}
|
||||
} else {
|
||||
// Nếu chỉ có ngày (vd: 2026-02-28), tìm cả ngày theo giờ server
|
||||
const startOfDay = new Date(searchDate);
|
||||
startOfDay.setHours(0, 0, 0, 0);
|
||||
const endOfDay = new Date(searchDate);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
where.updated_at = { [Op.gte]: startOfDay, [Op.lte]: endOfDay };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort order
|
||||
let order = [['title', 'ASC']]; // default alphabet
|
||||
if (sort === 'created_at') {
|
||||
order = [['created_at', 'DESC']];
|
||||
} else if (sort === 'updated_at') {
|
||||
order = [['updated_at', 'DESC']];
|
||||
}
|
||||
|
||||
const { count, rows } = await Context.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
order
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -308,8 +387,8 @@ class ContextController {
|
||||
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 || [];
|
||||
} else if (context.type_image === 'normal') {
|
||||
const updatedImagesNormal = currentVocab.image_normal || [];
|
||||
updatedImagesNormal.push(context.image);
|
||||
await currentVocab.update({ image_normal: updatedImagesNormal });
|
||||
}
|
||||
@@ -416,11 +495,11 @@ class ContextController {
|
||||
type,
|
||||
status,
|
||||
grade,
|
||||
reference_id,
|
||||
page = 1,
|
||||
limit = 50
|
||||
} = req.body;
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
const offset = (page - 1) * limit;
|
||||
const where = {};
|
||||
|
||||
@@ -430,7 +509,8 @@ class ContextController {
|
||||
}
|
||||
if (type) where.type = type;
|
||||
if (status !== undefined && status !== null) where.status = parseInt(status);
|
||||
if (grade !== undefined && grade !== null) where.grade = parseInt(grade);
|
||||
if (grade !== undefined && grade !== null) where.grade = parseInt(grade);
|
||||
if (reference_id) where.reference_id = reference_id;
|
||||
|
||||
// ── Text search ──────────────────────────────────────────────────────
|
||||
// `search` → title OR context (cả hai cùng lúc)
|
||||
@@ -440,7 +520,7 @@ class ContextController {
|
||||
|
||||
if (search) {
|
||||
textConditions.push(
|
||||
{ title: { [Op.like]: `%${search}%` } },
|
||||
{ title: { [Op.like]: `%${search}%` } },
|
||||
{ context: { [Op.like]: `%${search}%` } }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,6 +127,8 @@ exports.getAllSentences = async (req, res) => {
|
||||
topic,
|
||||
text,
|
||||
search,
|
||||
status,
|
||||
sort,
|
||||
is_active = true
|
||||
} = req.query;
|
||||
|
||||
@@ -137,6 +139,11 @@ exports.getAllSentences = async (req, res) => {
|
||||
where.is_active = is_active === 'true' || is_active === true;
|
||||
}
|
||||
|
||||
// Status filter (0 = chưa duyệt, 1 = đã duyệt, undefined = all)
|
||||
if (status !== undefined && status !== '' && status !== 'all') {
|
||||
where.status = parseInt(status);
|
||||
}
|
||||
|
||||
if (category) {
|
||||
where.category = category;
|
||||
}
|
||||
@@ -156,11 +163,19 @@ exports.getAllSentences = async (req, res) => {
|
||||
];
|
||||
}
|
||||
|
||||
// Sort order
|
||||
let order = [['created_at', 'DESC']]; // default
|
||||
if (sort === 'updated_at') {
|
||||
order = [['updated_at', 'DESC']];
|
||||
} else if (sort === 'alphabet') {
|
||||
order = [['text', 'ASC']];
|
||||
}
|
||||
|
||||
const { count, rows } = await Sentences.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
order
|
||||
});
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -60,10 +60,10 @@ const { Op } = require('sequelize');
|
||||
*/
|
||||
exports.createVocab = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
const {
|
||||
text,
|
||||
ipa,
|
||||
base_word,
|
||||
base_word,
|
||||
vi,
|
||||
category,
|
||||
topic,
|
||||
@@ -160,16 +160,18 @@ exports.createVocab = async (req, res) => {
|
||||
*/
|
||||
exports.getAllVocabs = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
category,
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
category,
|
||||
topic,
|
||||
base_word,
|
||||
text,
|
||||
search,
|
||||
grade_start,
|
||||
grade_end,
|
||||
status,
|
||||
sort,
|
||||
is_active = true
|
||||
} = req.query;
|
||||
|
||||
@@ -181,6 +183,11 @@ exports.getAllVocabs = async (req, res) => {
|
||||
where.is_active = is_active === 'true' || is_active === true;
|
||||
}
|
||||
|
||||
// Status filter (0 = chưa duyệt, 1 = đã duyệt, undefined = all)
|
||||
if (status !== undefined && status !== '' && status !== 'all') {
|
||||
where.status = parseInt(status);
|
||||
}
|
||||
|
||||
if (category) {
|
||||
where.category = category;
|
||||
}
|
||||
@@ -221,11 +228,19 @@ exports.getAllVocabs = async (req, res) => {
|
||||
where.grade_number = { [Op.lte]: parseInt(grade_end) };
|
||||
}
|
||||
|
||||
// Sort order
|
||||
let order = [['created_at', 'DESC']]; // default
|
||||
if (sort === 'updated_at') {
|
||||
order = [['updated_at', 'DESC']];
|
||||
} else if (sort === 'alphabet') {
|
||||
order = [['text', 'ASC']];
|
||||
}
|
||||
|
||||
const { count, rows } = await Vocab.findAndCountAll({
|
||||
where,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
order
|
||||
});
|
||||
|
||||
res.json({
|
||||
@@ -276,9 +291,9 @@ exports.getVocabById = async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const vocab = await Vocab.findOne({
|
||||
where: {
|
||||
where: {
|
||||
vocab_id: id,
|
||||
is_active: true
|
||||
is_active: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -339,7 +354,7 @@ exports.getVocabsWithoutIpa = async (req, res) => {
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await Vocab.findAndCountAll({
|
||||
where: {
|
||||
where: {
|
||||
is_active: true,
|
||||
[Op.or]: [
|
||||
{ ipa: null },
|
||||
@@ -407,7 +422,7 @@ exports.getVocabsWithoutImages = async (req, res) => {
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await Vocab.findAndCountAll({
|
||||
where: {
|
||||
where: {
|
||||
is_active: true,
|
||||
[Op.or]: [
|
||||
{ image_small: null },
|
||||
@@ -798,7 +813,7 @@ exports.getVocabStats = async (req, res) => {
|
||||
*/
|
||||
exports.searchVocabs = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
const {
|
||||
topic,
|
||||
category,
|
||||
base_word,
|
||||
@@ -876,7 +891,7 @@ exports.searchVocabs = async (req, res) => {
|
||||
// Handle shuffle_pos: find replacement words based on syntax
|
||||
if (shuffle_pos && typeof shuffle_pos === 'object') {
|
||||
const syntaxConditions = [];
|
||||
|
||||
|
||||
if (shuffle_pos.is_subject === true) {
|
||||
syntaxConditions.push({ 'syntax.is_subject': true });
|
||||
}
|
||||
@@ -911,7 +926,7 @@ exports.searchVocabs = async (req, res) => {
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
if (orConditions.length === 1) {
|
||||
where[Op.and] = where[Op.and] || [];
|
||||
where[Op.and].push(orConditions[0]);
|
||||
@@ -975,7 +990,7 @@ exports.searchVocabs = async (req, res) => {
|
||||
exports.getAllCategories = async (req, res) => {
|
||||
try {
|
||||
const categories = await Vocab.findAll({
|
||||
where: {
|
||||
where: {
|
||||
is_active: true,
|
||||
category: { [Op.ne]: null }
|
||||
},
|
||||
@@ -1026,7 +1041,7 @@ exports.getAllCategories = async (req, res) => {
|
||||
exports.getAllTopics = async (req, res) => {
|
||||
try {
|
||||
const topics = await Vocab.findAll({
|
||||
where: {
|
||||
where: {
|
||||
is_active: true,
|
||||
topic: { [Op.ne]: null }
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user