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>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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.
|
||||
}
|
||||
Reference in New Issue
Block a user