Files
pwa/backend/src/modules/sessions/homework.service.ts
T
ar.badieiandCursor c6243caa35 Add read-only Noya admin panel with assignments and UX polish.
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>
2026-09-21 08:00:24 +03:30

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.
}