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

This commit is contained in:
vuongps38770
2026-02-28 20:00:38 +07:00
parent f96833a7e4
commit 72283443ab
15 changed files with 972 additions and 318 deletions

View File

@@ -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}%` } }
);
}