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

View File

@@ -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({

View File

@@ -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 }
},