|
|
@@ -31,11 +31,34 @@ function lessonHistoryFilters(input,args){const f={...(input.filters||{}),...inp
|
|
|
for(const key of ['dateFrom','dateTo'])if(f[key]&&!/^\d{4}-\d{2}-\d{2}$/.test(f[key]))fail(400,'日期格式应为 YYYY-MM-DD');
|
|
|
add('"actualAt">=($?::date::timestamp AT TIME ZONE \'Asia/Shanghai\')',f.dateFrom);add('"actualAt"<(($?::date+1)::timestamp AT TIME ZONE \'Asia/Shanghai\')',f.dateTo);
|
|
|
return conditions.length?' WHERE '+conditions.join(' AND '):'';}
|
|
|
+// Resolve only the displayed page; preserve the original completed-lesson snapshot when present.
|
|
|
+async function lessonHistoryTypeRows(rows) {
|
|
|
+ const wanted=rows.map(row=>({company:row.company,appointmentId:text(row.yyds),studentId:text(row.xymz)})).filter(row=>row.company&&/^\d+$/.test(row.appointmentId)&&row.studentId);
|
|
|
+ if(!wanted.length)return rows;
|
|
|
+ const saved=await Psql.query(`WITH wanted AS (SELECT DISTINCT * FROM jsonb_to_recordset($1::jsonb) AS w(company text,"appointmentId" numeric,"studentId" text))
|
|
|
+ SELECT w.company,w."appointmentId"::text AS "appointmentId",w."studentId",
|
|
|
+ r."courseCategoryKey" AS "reportKey",r."courseCategoryName" AS "reportName",r."durationMinutes" AS "reportMinutes",
|
|
|
+ a."courseCategoryKey" AS "appointmentKey",a."courseCategoryName" AS "appointmentName",a."durationMinutes" AS "appointmentMinutes"
|
|
|
+ FROM wanted w
|
|
|
+ LEFT JOIN LATERAL (SELECT a.* FROM "CommonModel" c JOIN "CourseAppointment" a ON a.company=c.company AND a.id=c."itemId" AND a.szyh::text=w."studentId"
|
|
|
+ WHERE c.company=w.company AND c."modelId"=54 AND c."generalId"=w."appointmentId" ORDER BY c."objectId" LIMIT 1) a ON true
|
|
|
+ LEFT JOIN LATERAL (SELECT r.* FROM "LessonCompletionReport" r WHERE r.company=w.company AND r."appointmentGeneralId"=w."appointmentId" AND r."studentId"::text=w."studentId"
|
|
|
+ ORDER BY r."submittedAt" DESC NULLS LAST,r."objectId" DESC LIMIT 1) r ON true`,[JSON.stringify(wanted)]);
|
|
|
+ const key=row=>[row.company,row.appointmentId,row.studentId].join(':'),byAppointment=new Map(saved.map(row=>[key(row),row]));
|
|
|
+ return rows.map(row=>{
|
|
|
+ const source=byAppointment.get(key({company:row.company,appointmentId:text(row.yyds),studentId:text(row.xymz)}))||{};
|
|
|
+ const own=text(row.courseCategoryKey)||text(row.courseCategoryName),report=text(source.reportKey)||text(source.reportName);
|
|
|
+ return {...row,courseCategoryKey:own?row.courseCategoryKey:report?source.reportKey:source.appointmentKey,
|
|
|
+ courseCategoryName:own?row.courseCategoryName:report?source.reportName:source.appointmentName,
|
|
|
+ durationMinutes:[source.reportMinutes,source.appointmentMinutes,row.durationMinutes].map(value=>number(value)).find(value=>value>0)};
|
|
|
+ });
|
|
|
+}
|
|
|
function lessonHistoryClassType(row) {
|
|
|
- const categoryKey=text(row.courseCategoryKey),categoryName=text(row.courseCategoryName);
|
|
|
- const minutes=({'1':30,'2':60})[text(row.kclx)];
|
|
|
- const isWord=categoryKey==='word'||categoryName==='单词课'||(!categoryKey&&!categoryName&&number(row.kcid)>0&&number(row.kcid)!==398);
|
|
|
- return minutes&&isWord?'单词课 '+minutes+' 分钟':categoryName||classType(row.kclx);
|
|
|
+ const categoryKey=text(row.courseCategoryKey),categoryName=text(row.courseCategoryName),type=text(row.kclx);
|
|
|
+ const minutes=number(row.durationMinutes)||({'1':30,'2':60,'3':60})[type];
|
|
|
+ const isWord=categoryKey==='word'||categoryName==='单词课'||(!categoryKey&&!categoryName&&number(row.kcid)>0&&number(row.kcid)!==398&&['1','2'].includes(type));
|
|
|
+ const name=categoryName||TEACHING_CATEGORY_LABELS.get(categoryKey)||(type==='3'?'体验课':isWord?'单词课':number(row.kcid)===398?'语法写作课':'');
|
|
|
+ return name?name+(minutes?' '+minutes+' 分钟':''):classType(row.kclx);
|
|
|
}
|
|
|
function lessonHistoryRow(row){return {targetId:text(row.targetId||row.objectId),lessonId:row.objectId,generalId:number(row.generalId||row.id),appointmentId:text(row.yyds),studentId:number(row.xymz),studentName:row.studentNameResolved,coachId:number(row.jsmz),coachName:row.coachNameResolved,storeId:number(row.storeIdResolved),storeName:row.storeName,courseId:number(row.kcid),courseName:row.courseNameResolved,classTypeLabel:lessonHistoryClassType(row),lessonAt:dateIso(row.actualAt),score:number(row.pf),comment:text(row.pldp),coachComment:text(row.pldp),studentComment:text(row.pjnr),feedbackSummary:text(row.feedbackSummary||row.pldp),duplicateCount:number(row.duplicates),pendingReviews:number(row.pendingReviews),reviewCoaches:row.reviewCoaches||[],reviewCoachLabel:(row.reviewCoaches||[]).length>1?'多位陪练':row.reviewCoaches?.[0]?.name||'无待复习任务'};}
|
|
|
async function lessonHistoryPage(context,input){
|
|
|
@@ -50,7 +73,7 @@ async function lessonHistoryPage(context,input){
|
|
|
FROM xs_review_task_link link JOIN "MemoryPracticeRecord" m ON m.company=link.company AND m."objectId"=link."taskId"
|
|
|
LEFT JOIN LATERAL(SELECT * FROM "_User" u WHERE u.company=m.company AND u."legacyUserId"::text=m.plid::text ORDER BY u."objectId" LIMIT 1) u ON true
|
|
|
WHERE link.company=p.company AND link."studentId"=p.xymz AND link."appointmentId"=p.yyds AND COALESCE(m.fxzt,'0')<>'1' AND NOT COALESCE(m."rewardDuplicate",false) GROUP BY m.plid,u."realName",u.nickname,u."legacyUserData",u.username) grouped) rv ON true),'[]'::jsonb) items`;
|
|
|
- const result=await Psql.one(q,args);return {page:paging.page,pageSize:paging.pageSize,total:number(result.total),cumulativeTotal:number(result.cumulativeTotal),items:(result.items||[]).map(lessonHistoryRow),refreshedAt:new Date().toISOString(),version:LESSON_RECORDS_VERSION+':'+result.historyVersion};
|
|
|
+ const result=await Psql.one(q,args);return {page:paging.page,pageSize:paging.pageSize,total:number(result.total),cumulativeTotal:number(result.cumulativeTotal),items:(await lessonHistoryTypeRows(result.items||[])).map(lessonHistoryRow),refreshedAt:new Date().toISOString(),version:LESSON_RECORDS_VERSION+':'+result.historyVersion};
|
|
|
}
|
|
|
async function lessonHistoryDetail(context,input){
|
|
|
requireRole(context,['ops-manager','ops-staff','ops-auditor']);const co=companyIdOf(context,input),id=text(input.lessonId||input.targetId),appointment=text(input.appointmentId);
|
|
|
@@ -63,7 +86,7 @@ async function lessonHistoryDetail(context,input){
|
|
|
Psql.oneOrNone('SELECT score,comment,"submittedAt" FROM "LessonReview" WHERE company=$1 AND "appointmentGeneralId"::text=$2 LIMIT 1',[co,row.yyds]),
|
|
|
Psql.query('SELECT m."objectId",m.plid AS "coachId",m.kywrq AS "plannedDate",m.fxzt AS status,m.wcsj AS "completedAt",COALESCE(NULLIF(u."realName",\'\'),NULLIF(u.nickname,\'\'),u."legacyUserData"->>\'HoneyName\',u.username,\'陪练 \'||m.plid::text) AS "coachName" FROM xs_review_task_link l JOIN "MemoryPracticeRecord" m ON m.company=l.company AND m."objectId"=l."taskId" LEFT JOIN LATERAL(SELECT * FROM "_User" u WHERE u.company=m.company AND u."legacyUserId"::text=m.plid::text ORDER BY u."objectId" LIMIT 1) u ON true WHERE l.company=$1 AND l."studentId"=$2 AND l."appointmentId"=$3 ORDER BY m.kywrq,m."objectId"',[co,row.xymz,row.yyds]),
|
|
|
Psql.query('SELECT a.*,c.reason,c."actorId" FROM "ReviewTransferAudit" a JOIN "ReviewTransferCommand" c ON c."objectId"=a."commandId" WHERE a.company=$1 AND a."studentId"=$2 AND a."appointmentId"=$3 ORDER BY a."createdAt" DESC',[co,row.xymz,row.yyds])]);
|
|
|
- return {...lessonHistoryRow(row),report:report||null,coachComment:text(report?.coachComment||row.pldp),studentReview:review||{score:number(row.pf),comment:text(row.pjnr)},tasks,audit:auditRows,canTransfer:!context.storeId&&roleOf(context)==='ops-manager',hasReport:Boolean(report)};
|
|
|
+ return {...lessonHistoryRow((await lessonHistoryTypeRows([row]))[0]),report:report||null,coachComment:text(report?.coachComment||row.pldp),studentReview:review||{score:number(row.pf),comment:text(row.pjnr)},tasks,audit:auditRows,canTransfer:!context.storeId&&roleOf(context)==='ops-manager',hasReport:Boolean(report)};
|
|
|
}
|
|
|
async function lessonTransferPreview(context,input){
|
|
|
requireRole(context,['ops-manager']);const co=companyIdOf(context,input),student=text(input.studentId),appointment=input.scope==='student'?'':text(input.appointmentId),coach=text(input.coachId);
|