新增文本题库导入功能,题目新增“解析”字段
This commit is contained in:
@@ -187,6 +187,55 @@ export class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
static async getAllTaskStats(req: Request, res: Response) {
|
||||
try {
|
||||
const page = toPositiveInt(req.query.page, 1);
|
||||
const limit = toPositiveInt(req.query.limit, 5);
|
||||
|
||||
const statusRaw = typeof req.query.status === 'string' ? req.query.status : undefined;
|
||||
const status =
|
||||
statusRaw === 'completed' || statusRaw === 'ongoing' || statusRaw === 'notStarted'
|
||||
? statusRaw
|
||||
: undefined;
|
||||
|
||||
const endAtStart = typeof req.query.endAtStart === 'string' ? req.query.endAtStart : undefined;
|
||||
const endAtEnd = typeof req.query.endAtEnd === 'string' ? req.query.endAtEnd : undefined;
|
||||
|
||||
if (endAtStart && !Number.isFinite(Date.parse(endAtStart))) {
|
||||
return res.status(400).json({ success: false, message: 'endAtStart 参数无效' });
|
||||
}
|
||||
if (endAtEnd && !Number.isFinite(Date.parse(endAtEnd))) {
|
||||
return res.status(400).json({ success: false, message: 'endAtEnd 参数无效' });
|
||||
}
|
||||
|
||||
const { ExamTaskModel } = await import('../models/examTask');
|
||||
const result = await ExamTaskModel.getAllTasksWithStatsPaged({
|
||||
page,
|
||||
limit,
|
||||
status,
|
||||
endAtStart,
|
||||
endAtEnd,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: result.total,
|
||||
pages: Math.ceil(result.total / limit),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('获取任务统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取任务统计失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static async getDashboardOverview(req: Request, res: Response) {
|
||||
try {
|
||||
const { QuizModel } = await import('../models');
|
||||
|
||||
@@ -66,7 +66,7 @@ export class QuestionController {
|
||||
// 创建题目
|
||||
static async createQuestion(req: Request, res: Response) {
|
||||
try {
|
||||
const { content, type, category, options, answer, score } = req.body;
|
||||
const { content, type, category, options, answer, analysis, score } = req.body;
|
||||
|
||||
const questionData: CreateQuestionData = {
|
||||
content,
|
||||
@@ -74,6 +74,7 @@ export class QuestionController {
|
||||
category,
|
||||
options,
|
||||
answer,
|
||||
analysis,
|
||||
score
|
||||
};
|
||||
|
||||
@@ -106,7 +107,7 @@ export class QuestionController {
|
||||
static async updateQuestion(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { content, type, category, options, answer, score } = req.body;
|
||||
const { content, type, category, options, answer, analysis, score } = req.body;
|
||||
|
||||
const updateData: Partial<CreateQuestionData> = {};
|
||||
if (content !== undefined) updateData.content = content;
|
||||
@@ -114,6 +115,7 @@ export class QuestionController {
|
||||
if (category !== undefined) updateData.category = category;
|
||||
if (options !== undefined) updateData.options = options;
|
||||
if (answer !== undefined) updateData.answer = answer;
|
||||
if (analysis !== undefined) updateData.analysis = analysis;
|
||||
if (score !== undefined) updateData.score = score;
|
||||
|
||||
const question = await QuestionModel.update(id, updateData);
|
||||
@@ -181,6 +183,7 @@ export class QuestionController {
|
||||
type: QuestionController.mapQuestionType(row['题型'] || row['type']),
|
||||
category: row['题目类别'] || row['category'] || '通用',
|
||||
answer: row['标准答案'] || row['answer'],
|
||||
analysis: row['解析'] || row['analysis'] || '',
|
||||
score: parseInt(row['分值'] || row['score']) || 0,
|
||||
options: QuestionController.parseOptions(row['选项'] || row['options'])
|
||||
}));
|
||||
@@ -215,6 +218,120 @@ export class QuestionController {
|
||||
}
|
||||
}
|
||||
|
||||
static async importTextQuestions(req: Request, res: Response) {
|
||||
try {
|
||||
const mode = req.body?.mode as 'overwrite' | 'incremental';
|
||||
const rawQuestions = req.body?.questions as any[];
|
||||
|
||||
if (mode !== 'overwrite' && mode !== 'incremental') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '导入模式不合法',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(rawQuestions) || rawQuestions.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '题目列表不能为空',
|
||||
});
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
const normalized = new Map<string, CreateQuestionData>();
|
||||
|
||||
const normalizeAnswer = (type: string, answer: unknown): string | string[] => {
|
||||
if (type === 'multiple') {
|
||||
if (Array.isArray(answer)) {
|
||||
return answer.map((a) => String(a).trim()).filter(Boolean);
|
||||
}
|
||||
return String(answer || '')
|
||||
.split(/[|,,、\s]+/g)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(answer)) {
|
||||
return String(answer[0] ?? '').trim();
|
||||
}
|
||||
return String(answer ?? '').trim();
|
||||
};
|
||||
|
||||
const normalizeJudgment = (answer: string) => {
|
||||
const v = String(answer || '').trim();
|
||||
const yes = new Set(['正确', '对', '是', 'true', 'True', 'TRUE', '1', 'Y', 'y', 'yes', 'YES']);
|
||||
const no = new Set(['错误', '错', '否', '不是', 'false', 'False', 'FALSE', '0', 'N', 'n', 'no', 'NO']);
|
||||
if (yes.has(v)) return '正确';
|
||||
if (no.has(v)) return '错误';
|
||||
return v;
|
||||
};
|
||||
|
||||
for (let i = 0; i < rawQuestions.length; i++) {
|
||||
const q = rawQuestions[i] || {};
|
||||
const content = String(q.content ?? '').trim();
|
||||
if (!content) {
|
||||
errors.push(`第${i + 1}题:题目内容不能为空`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = QuestionController.mapQuestionType(String(q.type ?? '').trim());
|
||||
const category = String(q.category ?? '通用').trim() || '通用';
|
||||
const score = Number(q.score);
|
||||
const options = Array.isArray(q.options) ? q.options.map((o: any) => String(o).trim()).filter(Boolean) : undefined;
|
||||
let answer = normalizeAnswer(type, q.answer);
|
||||
const analysis = String(q.analysis ?? '').trim();
|
||||
|
||||
if (type === 'judgment' && typeof answer === 'string') {
|
||||
answer = normalizeJudgment(answer);
|
||||
}
|
||||
|
||||
const questionData: CreateQuestionData = {
|
||||
content,
|
||||
type: type as any,
|
||||
category,
|
||||
options: type === 'single' || type === 'multiple' ? options : undefined,
|
||||
answer: answer as any,
|
||||
analysis,
|
||||
score,
|
||||
};
|
||||
|
||||
const validationErrors = QuestionModel.validateQuestionData(questionData);
|
||||
if (validationErrors.length > 0) {
|
||||
errors.push(`第${i + 1}题:${validationErrors.join(';')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized.set(content, questionData);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '数据验证失败',
|
||||
errors,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await QuestionModel.importFromText(mode, Array.from(normalized.values()));
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
mode,
|
||||
total: normalized.size,
|
||||
inserted: result.inserted,
|
||||
updated: result.updated,
|
||||
errors: result.errors,
|
||||
cleared: result.cleared ?? undefined,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('文本导入失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '文本导入失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 映射题型
|
||||
private static mapQuestionType(type: string): string {
|
||||
const typeMap: { [key: string]: string } = {
|
||||
@@ -270,6 +387,7 @@ export class QuestionController {
|
||||
'题目类别': question.category || '通用',
|
||||
'选项': question.options ? question.options.join('|') : '',
|
||||
'标准答案': question.answer,
|
||||
'解析': question.analysis || '',
|
||||
'分值': question.score,
|
||||
'创建时间': new Date(question.createdAt).toLocaleString()
|
||||
}));
|
||||
|
||||
@@ -118,6 +118,7 @@ export const initDatabase = async () => {
|
||||
console.log('数据库初始化成功');
|
||||
} else {
|
||||
console.log('数据库表已存在,跳过初始化');
|
||||
await ensureColumn('questions', "analysis TEXT NOT NULL DEFAULT ''", 'analysis');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('数据库初始化失败:', error);
|
||||
|
||||
@@ -18,6 +18,7 @@ CREATE TABLE questions (
|
||||
type TEXT NOT NULL CHECK(type IN ('single', 'multiple', 'judgment', 'text')),
|
||||
options TEXT, -- JSON格式存储选项
|
||||
answer TEXT NOT NULL,
|
||||
analysis TEXT NOT NULL DEFAULT '',
|
||||
score INTEGER NOT NULL CHECK(score > 0),
|
||||
category TEXT NOT NULL DEFAULT '通用',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
|
||||
@@ -318,6 +318,89 @@ export class ExamTaskModel {
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
static async getAllTasksWithStatsPaged(
|
||||
input: {
|
||||
page: number;
|
||||
limit: number;
|
||||
status?: 'completed' | 'ongoing' | 'notStarted';
|
||||
endAtStart?: string;
|
||||
endAtEnd?: string;
|
||||
},
|
||||
): Promise<{ data: Array<ActiveTaskStat & { status: '已完成' | '进行中' | '未开始' }>; total: number }> {
|
||||
const nowIso = new Date().toISOString();
|
||||
const nowMs = Date.now();
|
||||
const offset = (input.page - 1) * input.limit;
|
||||
|
||||
const whereParts: string[] = [];
|
||||
const params: any[] = [];
|
||||
|
||||
if (input.status === 'completed') {
|
||||
whereParts.push('t.end_at < ?');
|
||||
params.push(nowIso);
|
||||
} else if (input.status === 'ongoing') {
|
||||
whereParts.push('t.start_at <= ? AND t.end_at >= ?');
|
||||
params.push(nowIso, nowIso);
|
||||
} else if (input.status === 'notStarted') {
|
||||
whereParts.push('t.start_at > ?');
|
||||
params.push(nowIso);
|
||||
}
|
||||
|
||||
if (input.endAtStart) {
|
||||
whereParts.push('t.end_at >= ?');
|
||||
params.push(input.endAtStart);
|
||||
}
|
||||
if (input.endAtEnd) {
|
||||
whereParts.push('t.end_at <= ?');
|
||||
params.push(input.endAtEnd);
|
||||
}
|
||||
|
||||
const whereClause = whereParts.length > 0 ? `WHERE ${whereParts.join(' AND ')}` : '';
|
||||
|
||||
const totalRow = await get(`SELECT COUNT(*) as total FROM exam_tasks t ${whereClause}`, params);
|
||||
const total = Number(totalRow?.total || 0);
|
||||
|
||||
const tasks = await all(
|
||||
`
|
||||
SELECT
|
||||
t.id, t.name as taskName, s.name as subjectName, s.total_score as totalScore,
|
||||
t.start_at as startAt, t.end_at as endAt
|
||||
FROM exam_tasks t
|
||||
JOIN exam_subjects s ON t.subject_id = s.id
|
||||
${whereClause}
|
||||
ORDER BY t.end_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`,
|
||||
[...params, input.limit, offset],
|
||||
);
|
||||
|
||||
const data: Array<ActiveTaskStat & { status: '已完成' | '进行中' | '未开始' }> = [];
|
||||
for (const task of tasks) {
|
||||
const report = await this.getReport(task.id);
|
||||
const stat = this.buildActiveTaskStat({
|
||||
taskId: task.id,
|
||||
taskName: task.taskName,
|
||||
subjectName: task.subjectName,
|
||||
totalScore: Number(task.totalScore) || 0,
|
||||
startAt: task.startAt,
|
||||
endAt: task.endAt,
|
||||
report,
|
||||
});
|
||||
|
||||
const startMs = new Date(task.startAt).getTime();
|
||||
const endMs = new Date(task.endAt).getTime();
|
||||
const status: '已完成' | '进行中' | '未开始' =
|
||||
Number.isFinite(endMs) && endMs < nowMs
|
||||
? '已完成'
|
||||
: Number.isFinite(startMs) && startMs > nowMs
|
||||
? '未开始'
|
||||
: '进行中';
|
||||
|
||||
data.push({ ...stat, status });
|
||||
}
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
static async findById(id: string): Promise<ExamTask | null> {
|
||||
const sql = `SELECT id, name, subject_id as subjectId, start_at as startAt, end_at as endAt, created_at as createdAt, selection_config as selectionConfig FROM exam_tasks WHERE id = ?`;
|
||||
const row = await get(sql, [id]);
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Question {
|
||||
category: string;
|
||||
options?: string[];
|
||||
answer: string | string[];
|
||||
analysis: string;
|
||||
score: number;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -18,6 +19,7 @@ export interface CreateQuestionData {
|
||||
category?: string;
|
||||
options?: string[];
|
||||
answer: string | string[];
|
||||
analysis?: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
@@ -26,6 +28,7 @@ export interface ExcelQuestionData {
|
||||
type: string;
|
||||
category?: string;
|
||||
answer: string;
|
||||
analysis?: string;
|
||||
score: number;
|
||||
options?: string[];
|
||||
}
|
||||
@@ -37,13 +40,14 @@ export class QuestionModel {
|
||||
const optionsStr = data.options ? JSON.stringify(data.options) : null;
|
||||
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
|
||||
const category = data.category && data.category.trim() ? data.category.trim() : '通用';
|
||||
const analysis = String(data.analysis ?? '').trim().slice(0, 255);
|
||||
|
||||
const sql = `
|
||||
INSERT INTO questions (id, content, type, options, answer, score, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
await run(sql, [id, data.content, data.type, optionsStr, answerStr, data.score, category]);
|
||||
await run(sql, [id, data.content, data.type, optionsStr, answerStr, analysis, data.score, category]);
|
||||
return this.findById(id) as Promise<Question>;
|
||||
}
|
||||
|
||||
@@ -58,8 +62,8 @@ export class QuestionModel {
|
||||
await run('BEGIN TRANSACTION');
|
||||
|
||||
const sql = `
|
||||
INSERT INTO questions (id, content, type, options, answer, score, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
@@ -69,9 +73,10 @@ export class QuestionModel {
|
||||
const optionsStr = question.options ? JSON.stringify(question.options) : null;
|
||||
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
|
||||
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
|
||||
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
|
||||
|
||||
// 直接执行插入,不调用单个create方法
|
||||
await run(sql, [id, question.content, question.type, optionsStr, answerStr, question.score, category]);
|
||||
await run(sql, [id, question.content, question.type, optionsStr, answerStr, analysis, question.score, category]);
|
||||
success++;
|
||||
} catch (error: any) {
|
||||
errors.push(`第${i + 1}题: ${error.message}`);
|
||||
@@ -89,6 +94,77 @@ export class QuestionModel {
|
||||
return { success, errors };
|
||||
}
|
||||
|
||||
static async importFromText(
|
||||
mode: 'overwrite' | 'incremental',
|
||||
questions: CreateQuestionData[],
|
||||
): Promise<{
|
||||
inserted: number;
|
||||
updated: number;
|
||||
errors: string[];
|
||||
cleared?: { questions: number; quizRecords: number; quizAnswers: number };
|
||||
}> {
|
||||
const errors: string[] = [];
|
||||
let inserted = 0;
|
||||
let updated = 0;
|
||||
let cleared: { questions: number; quizRecords: number; quizAnswers: number } | undefined;
|
||||
|
||||
const insertSql = `
|
||||
INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
await run('BEGIN TRANSACTION');
|
||||
try {
|
||||
if (mode === 'overwrite') {
|
||||
const [qCount, rCount, aCount] = await Promise.all([
|
||||
get(`SELECT COUNT(*) as total FROM questions`),
|
||||
get(`SELECT COUNT(*) as total FROM quiz_records`),
|
||||
get(`SELECT COUNT(*) as total FROM quiz_answers`),
|
||||
]);
|
||||
cleared = { questions: qCount.total, quizRecords: rCount.total, quizAnswers: aCount.total };
|
||||
|
||||
await run(`DELETE FROM quiz_answers`);
|
||||
await run(`DELETE FROM quiz_records`);
|
||||
await run(`DELETE FROM questions`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const question = questions[i];
|
||||
try {
|
||||
const optionsStr = question.options ? JSON.stringify(question.options) : null;
|
||||
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
|
||||
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
|
||||
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
|
||||
|
||||
if (mode === 'incremental') {
|
||||
const existing = await get(`SELECT id FROM questions WHERE content = ?`, [question.content]);
|
||||
if (existing?.id) {
|
||||
await run(
|
||||
`UPDATE questions SET content = ?, type = ?, options = ?, answer = ?, analysis = ?, score = ?, category = ? WHERE id = ?`,
|
||||
[question.content, question.type, optionsStr, answerStr, analysis, question.score, category, existing.id],
|
||||
);
|
||||
updated++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
await run(insertSql, [id, question.content, question.type, optionsStr, answerStr, analysis, question.score, category]);
|
||||
inserted++;
|
||||
} catch (error: any) {
|
||||
errors.push(`第${i + 1}题: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await run('COMMIT');
|
||||
} catch (error) {
|
||||
await run('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { inserted, updated, errors, cleared };
|
||||
}
|
||||
|
||||
// 根据ID查找题目
|
||||
static async findById(id: string): Promise<Question | null> {
|
||||
const sql = `SELECT * FROM questions WHERE id = ?`;
|
||||
@@ -219,6 +295,11 @@ export class QuestionModel {
|
||||
fields.push('answer = ?');
|
||||
values.push(answerStr);
|
||||
}
|
||||
|
||||
if (data.analysis !== undefined) {
|
||||
fields.push('analysis = ?');
|
||||
values.push(String(data.analysis ?? '').trim().slice(0, 255));
|
||||
}
|
||||
|
||||
if (data.score !== undefined) {
|
||||
fields.push('score = ?');
|
||||
@@ -257,6 +338,7 @@ export class QuestionModel {
|
||||
category: row.category || '通用',
|
||||
options: row.options ? JSON.parse(row.options) : undefined,
|
||||
answer: this.parseAnswer(row.answer, row.type),
|
||||
analysis: String(row.analysis ?? ''),
|
||||
score: row.score,
|
||||
createdAt: row.created_at
|
||||
};
|
||||
@@ -309,6 +391,10 @@ export class QuestionModel {
|
||||
if (data.category !== undefined && data.category.trim().length === 0) {
|
||||
errors.push('题目类别不能为空');
|
||||
}
|
||||
|
||||
if (data.analysis !== undefined && String(data.analysis).length > 255) {
|
||||
errors.push('解析长度不能超过255个字符');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface QuizAnswer {
|
||||
questionType?: string;
|
||||
correctAnswer?: string | string[];
|
||||
questionScore?: number;
|
||||
questionAnalysis?: string;
|
||||
}
|
||||
|
||||
export interface SubmitAnswerData {
|
||||
@@ -195,7 +196,7 @@ export class QuizModel {
|
||||
SELECT a.id, a.record_id as recordId, a.question_id as questionId,
|
||||
a.user_answer as userAnswer, a.score, a.is_correct as isCorrect,
|
||||
a.created_at as createdAt,
|
||||
q.content as questionContent, q.type as questionType, q.answer as correctAnswer, q.score as questionScore
|
||||
q.content as questionContent, q.type as questionType, q.answer as correctAnswer, q.score as questionScore, q.analysis as questionAnalysis
|
||||
FROM quiz_answers a
|
||||
JOIN questions q ON a.question_id = q.id
|
||||
WHERE a.record_id = ?
|
||||
@@ -215,7 +216,8 @@ export class QuizModel {
|
||||
questionContent: row.questionContent,
|
||||
questionType: row.questionType,
|
||||
correctAnswer: this.parseAnswer(row.correctAnswer, row.questionType),
|
||||
questionScore: row.questionScore
|
||||
questionScore: row.questionScore,
|
||||
questionAnalysis: row.questionAnalysis ?? ''
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ apiRouter.post('/questions', adminAuth, QuestionController.createQuestion);
|
||||
apiRouter.put('/questions/:id', adminAuth, QuestionController.updateQuestion);
|
||||
apiRouter.delete('/questions/:id', adminAuth, QuestionController.deleteQuestion);
|
||||
apiRouter.post('/questions/import', adminAuth, upload.single('file'), QuestionController.importQuestions);
|
||||
apiRouter.post('/questions/import-text', adminAuth, QuestionController.importTextQuestions);
|
||||
apiRouter.get('/questions/export', adminAuth, QuestionController.exportQuestions);
|
||||
|
||||
// 为了兼容前端可能的错误请求,添加一个不包含 /api 前缀的路由
|
||||
@@ -110,6 +111,7 @@ apiRouter.get('/admin/active-tasks', adminAuth, AdminController.getActiveTasksSt
|
||||
apiRouter.get('/admin/dashboard/overview', adminAuth, AdminController.getDashboardOverview);
|
||||
apiRouter.get('/admin/tasks/history-stats', adminAuth, AdminController.getHistoryTaskStats);
|
||||
apiRouter.get('/admin/tasks/upcoming-stats', adminAuth, AdminController.getUpcomingTaskStats);
|
||||
apiRouter.get('/admin/tasks/all-stats', adminAuth, AdminController.getAllTaskStats);
|
||||
apiRouter.put('/admin/config', adminAuth, AdminController.updateQuizConfig);
|
||||
apiRouter.get('/admin/config', adminAuth, AdminController.getQuizConfig);
|
||||
apiRouter.put('/admin/password', adminAuth, AdminController.updatePassword);
|
||||
|
||||
Reference in New Issue
Block a user