Ship admin auth, user overview, assessments, chats, assignment filters/export, security hardening, and a separate desktop admin shell with clearer assessment answers. Co-authored-by: Cursor <cursoragent@cursor.com>
171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
import { SessionProgressStatus, AssignmentReportStatus, UserAssignmentStatus } from '@prisma/client';
|
|
import { prisma } from '../../lib/prisma.js';
|
|
import { ForbiddenError, NotFoundError, ValidationError } from '../../shared/errors/index.js';
|
|
import {
|
|
getHomeworkForSession,
|
|
getHomeworkReportRequiredFor,
|
|
type SessionHomework,
|
|
} from './sessionHomework.js';
|
|
import {
|
|
activateAssignmentsForSession,
|
|
seedAssignmentDefinitions,
|
|
} from '../assignments/assignments.service.js';
|
|
|
|
async function getCompletedOrders(userId: bigint): Promise<number[]> {
|
|
const rows = await prisma.sessionProgress.findMany({
|
|
where: { userId, status: SessionProgressStatus.completed },
|
|
select: { sessionOrder: true },
|
|
});
|
|
return rows.map((row) => row.sessionOrder).sort((a, b) => a - b);
|
|
}
|
|
|
|
function serializeReport(row: {
|
|
sessionOrder: number;
|
|
reportText: string;
|
|
submittedAt: Date;
|
|
}) {
|
|
return {
|
|
sessionOrder: row.sessionOrder,
|
|
reportText: row.reportText,
|
|
submittedAt: row.submittedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
export async function hasHomeworkReport(userId: bigint, sessionOrder: number): Promise<boolean> {
|
|
const row = await prisma.homeworkReport.findUnique({
|
|
where: { userId_sessionOrder: { userId, sessionOrder } },
|
|
select: { id: true },
|
|
});
|
|
return Boolean(row);
|
|
}
|
|
|
|
export async function getHomeworkReport(userId: bigint, sessionOrder: number) {
|
|
const homework = getHomeworkForSession(sessionOrder);
|
|
if (!homework) {
|
|
throw new NotFoundError(`Session ${sessionOrder} has no homework`);
|
|
}
|
|
|
|
const row = await prisma.homeworkReport.findUnique({
|
|
where: { userId_sessionOrder: { userId, sessionOrder } },
|
|
});
|
|
|
|
return {
|
|
sessionOrder,
|
|
homework,
|
|
report: row ? serializeReport(row) : null,
|
|
};
|
|
}
|
|
|
|
export async function submitHomeworkReport(userId: bigint, sessionOrder: number, reportText: string) {
|
|
const homework = getHomeworkForSession(sessionOrder);
|
|
if (!homework) {
|
|
throw new ValidationError(`Session ${sessionOrder} has no homework to report`);
|
|
}
|
|
|
|
const trimmed = reportText.trim();
|
|
if (trimmed.length < 10) {
|
|
throw new ValidationError('گزارش تکلیف باید حداقل چند جمله باشد');
|
|
}
|
|
|
|
const completedOrders = await getCompletedOrders(userId);
|
|
if (!completedOrders.includes(sessionOrder)) {
|
|
throw new ForbiddenError('Homework can only be reported after the session is completed');
|
|
}
|
|
|
|
const row = await prisma.homeworkReport.upsert({
|
|
where: { userId_sessionOrder: { userId, sessionOrder } },
|
|
create: {
|
|
userId,
|
|
sessionOrder,
|
|
reportText: trimmed,
|
|
},
|
|
update: {
|
|
reportText: trimmed,
|
|
submittedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
// Bridge into cumulative assignment system (idempotent activation + append report).
|
|
await seedAssignmentDefinitions();
|
|
await activateAssignmentsForSession(userId, sessionOrder);
|
|
const userAssignment = await prisma.userAssignment.findFirst({
|
|
where: { userId, definition: { sessionOrder } },
|
|
orderBy: { activatedAt: 'asc' },
|
|
});
|
|
if (userAssignment && userAssignment.status !== UserAssignmentStatus.COMPLETED) {
|
|
const now = new Date();
|
|
await prisma.assignmentReport.create({
|
|
data: {
|
|
userAssignmentId: userAssignment.id,
|
|
userId,
|
|
reportText: trimmed,
|
|
reportStatus: AssignmentReportStatus.completed,
|
|
},
|
|
});
|
|
await prisma.userAssignment.update({
|
|
where: { id: userAssignment.id },
|
|
data: {
|
|
status: UserAssignmentStatus.REPORTED,
|
|
startedAt: userAssignment.startedAt ?? now,
|
|
lastActivityAt: now,
|
|
},
|
|
});
|
|
}
|
|
|
|
return {
|
|
sessionOrder,
|
|
homework,
|
|
report: serializeReport(row),
|
|
};
|
|
}
|
|
|
|
export type PendingHomeworkReport = {
|
|
forSessionOrder: number;
|
|
homeworkSessionOrder: number;
|
|
homework: SessionHomework;
|
|
submitted: boolean;
|
|
};
|
|
|
|
/**
|
|
* If the learner's next educational session requires a prior homework report,
|
|
* return the pending homework info. Null when nothing is required or already submitted.
|
|
*/
|
|
export async function getPendingHomeworkReport(
|
|
userId: bigint,
|
|
nextSessionOrder: number,
|
|
): Promise<PendingHomeworkReport | null> {
|
|
const homeworkSessionOrder = getHomeworkReportRequiredFor(nextSessionOrder);
|
|
if (!homeworkSessionOrder) {
|
|
return null;
|
|
}
|
|
|
|
const homework = getHomeworkForSession(homeworkSessionOrder);
|
|
if (!homework) {
|
|
return null;
|
|
}
|
|
|
|
const completedOrders = await getCompletedOrders(userId);
|
|
if (!completedOrders.includes(homeworkSessionOrder)) {
|
|
return null;
|
|
}
|
|
|
|
const submitted = await hasHomeworkReport(userId, homeworkSessionOrder);
|
|
return {
|
|
forSessionOrder: nextSessionOrder,
|
|
homeworkSessionOrder,
|
|
homework,
|
|
submitted,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Soft check-in only: assignments never block the next session
|
|
* (`blocksNextSession` is false). Kept for call-site compatibility.
|
|
*/
|
|
export async function assertHomeworkReportForSessionStart(
|
|
_userId: bigint,
|
|
_sessionOrder: number,
|
|
): Promise<void> {
|
|
// Intentionally no-op — cumulative homework must not gate session unlock.
|
|
}
|