|
@@ -39,12 +39,34 @@ function notesReviewSourceSql() {
|
|
|
AND (a.dszt='0' OR (NULLIF(BTRIM(to_jsonb(a)->>'kssj'),'') IS NOT NULL
|
|
AND (a.dszt='0' OR (NULLIF(BTRIM(to_jsonb(a)->>'kssj'),'') IS NOT NULL
|
|
|
AND NULLIF(BTRIM(to_jsonb(a)->>'jssj'),'') IS NOT NULL AND to_jsonb(l)->>'jsmz'=to_jsonb(a)->>'pl'))))))))`;
|
|
AND NULLIF(BTRIM(to_jsonb(a)->>'jssj'),'') IS NOT NULL AND to_jsonb(l)->>'jsmz'=to_jsonb(a)->>'pl'))))))))`;
|
|
|
}
|
|
}
|
|
|
|
|
+// The home card needs aggregates, not task bodies or reward receipts. It reads the
|
|
|
|
|
+// existing projection; synchronization remains owned by the leased importer/detail flow.
|
|
|
|
|
+async function appHomeReviewSummary(where,args,dateSql,today) {
|
|
|
|
|
+ const [dates,syncStatus]=await Promise.all([
|
|
|
|
|
+ Psql.query('SELECT '+dateSql+' AS date,COUNT(*)::int total,COUNT(*) FILTER(WHERE m."fxzt"=\'1\')::int completed FROM "MemoryPracticeRecord" m WHERE '+where+' GROUP BY '+dateSql,args),
|
|
|
|
|
+ appLiveSyncStatus(['review']),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ const summary={total:0,completed:0,due:0,today:0,todayTotal:0,todayCompleted:0,todayPending:0,overduePending:0,income:0,incomeScope:'all'};
|
|
|
|
|
+ for(const row of dates){
|
|
|
|
|
+ const total=number(row.total),completed=number(row.completed),pending=total-completed;
|
|
|
|
|
+ summary.total+=total;summary.completed+=completed;
|
|
|
|
|
+ // SQL NULL dates do not participate in comparison filters.
|
|
|
|
|
+ if(typeof row.date==='string'&&row.date<=today)summary.due+=pending;
|
|
|
|
|
+ if(typeof row.date==='string'&&row.date<today)summary.overduePending+=pending;
|
|
|
|
|
+ if(row.date===today){summary.today+=total;summary.todayCompleted+=completed;}
|
|
|
|
|
+ }
|
|
|
|
|
+ summary.todayTotal=summary.today;summary.todayPending=summary.today-summary.todayCompleted;
|
|
|
|
|
+ return {view:'home',items:[],page:{itemCount:0,pageCount:0},summary,dates,students:[],serverToday:today,
|
|
|
|
|
+ ...mobileSource(),syncStatus,legacyReviewSync:{state:'snapshot',lastSuccessAt:syncStatus.lastSuccessAt||''}};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function appReviewPage(input,current) {
|
|
async function appReviewPage(input,current) {
|
|
|
const uid=await authorizeRequestedUser(current,input.uid),coach=number(current.get('legacyGroupId'))===3,p=mobilePage(input),includeMetadata=input.includeMetadata!==false,today=appOverviewToday();
|
|
const uid=await authorizeRequestedUser(current,input.uid),coach=number(current.get('legacyGroupId'))===3,p=mobilePage(input),includeMetadata=input.includeMetadata!==false,today=appOverviewToday();
|
|
|
const reviewTarget=coach&&number(input.studentId)?await appReviewTarget(current,input.studentId):coach?0:uid;
|
|
const reviewTarget=coach&&number(input.studentId)?await appReviewTarget(current,input.studentId):coach?0:uid;
|
|
|
const refreshCoachToday=coach&&!reviewTarget&&input.scope==='today'&&p.index===1&&input.refresh===true&&uid===ownLegacyId(current);
|
|
const refreshCoachToday=coach&&!reviewTarget&&input.scope==='today'&&p.index===1&&input.refresh===true&&uid===ownLegacyId(current);
|
|
|
if(refreshCoachToday)assertLessonTenant(current);
|
|
if(refreshCoachToday)assertLessonTenant(current);
|
|
|
- const legacyReviewSync=reviewTarget?await syncLegacyReviews(reviewTarget,current,input.refresh===true):
|
|
|
|
|
|
|
+ const homeSnapshot=!coach&&input.view==='home'&&input.source==='database';
|
|
|
|
|
+ const legacyReviewSync=reviewTarget&&!homeSnapshot?await syncLegacyReviews(reviewTarget,current,input.refresh===true):
|
|
|
refreshCoachToday?await syncLegacyCoachTodayReviews(uid,current,today):undefined;
|
|
refreshCoachToday?await syncLegacyCoachTodayReviews(uid,current,today):undefined;
|
|
|
const args=[DEFAULT_COMPANY_ID,String(uid)],clauses=['m."company"=$1','m."'+(coach?'plid':'yhid')+'"=$2::text::numeric','m."'+(coach?'plid':'yhid')+'"::text=$2'];
|
|
const args=[DEFAULT_COMPANY_ID,String(uid)],clauses=['m."company"=$1','m."'+(coach?'plid':'yhid')+'"=$2::text::numeric','m."'+(coach?'plid':'yhid')+'"::text=$2'];
|
|
|
// Keep the exact text identity match while letting the numeric ownership index narrow candidates.
|
|
// Keep the exact text identity match while letting the numeric ownership index narrow candidates.
|
|
@@ -57,6 +79,7 @@ async function appReviewPage(input,current) {
|
|
|
if(coach&&number(input.studentId))add('m."yhid"::text=?',String(input.studentId));
|
|
if(coach&&number(input.studentId))add('m."yhid"::text=?',String(input.studentId));
|
|
|
const baseWhere=clauses.join(' AND '),baseArgs=[...args];
|
|
const baseWhere=clauses.join(' AND '),baseArgs=[...args];
|
|
|
const dateSql='LEFT(COALESCE(NULLIF(m."kywrq",\'\'),m."kywsj"),10)';
|
|
const dateSql='LEFT(COALESCE(NULLIF(m."kywrq",\'\'),m."kywsj"),10)';
|
|
|
|
|
+ if(homeSnapshot)return appHomeReviewSummary(baseWhere,baseArgs,dateSql,today);
|
|
|
const summaryPromise= Psql.one('SELECT COUNT(*)::int total,COUNT(*) FILTER(WHERE m."fxzt"=\'1\')::int completed,COUNT(*) FILTER(WHERE COALESCE(m."fxzt",\'0\')<>\'1\' AND '+dateSql+'<=$'+(args.length+1)+')::int due,COUNT(*) FILTER(WHERE '+dateSql+'=$'+(args.length+1)+')::int today,COUNT(*) FILTER(WHERE '+dateSql+'=$'+(args.length+1)+' AND m."fxzt"=\'1\')::int \"todayCompleted\",COUNT(*) FILTER(WHERE '+dateSql+'<$'+(args.length+1)+' AND COALESCE(m."fxzt",\'0\')<>\'1\')::int \"overduePending\" FROM "MemoryPracticeRecord" m WHERE '+baseWhere,[...args,today]);
|
|
const summaryPromise= Psql.one('SELECT COUNT(*)::int total,COUNT(*) FILTER(WHERE m."fxzt"=\'1\')::int completed,COUNT(*) FILTER(WHERE COALESCE(m."fxzt",\'0\')<>\'1\' AND '+dateSql+'<=$'+(args.length+1)+')::int due,COUNT(*) FILTER(WHERE '+dateSql+'=$'+(args.length+1)+')::int today,COUNT(*) FILTER(WHERE '+dateSql+'=$'+(args.length+1)+' AND m."fxzt"=\'1\')::int \"todayCompleted\",COUNT(*) FILTER(WHERE '+dateSql+'<$'+(args.length+1)+' AND COALESCE(m."fxzt",\'0\')<>\'1\')::int \"overduePending\" FROM "MemoryPracticeRecord" m WHERE '+baseWhere,[...args,today]);
|
|
|
const datesPromise=includeMetadata? Psql.query('SELECT '+dateSql+' AS date,COUNT(*)::int total,COUNT(*) FILTER(WHERE m."fxzt"=\'1\')::int completed FROM "MemoryPracticeRecord" m WHERE '+baseWhere+' GROUP BY '+dateSql,baseArgs):Promise.resolve([]);
|
|
const datesPromise=includeMetadata? Psql.query('SELECT '+dateSql+' AS date,COUNT(*)::int total,COUNT(*) FILTER(WHERE m."fxzt"=\'1\')::int completed FROM "MemoryPracticeRecord" m WHERE '+baseWhere+' GROUP BY '+dateSql,baseArgs):Promise.resolve([]);
|
|
|
if(input.scope==='today')add(dateSql+'=?',today);
|
|
if(input.scope==='today')add(dateSql+'=?',today);
|
|
@@ -65,12 +88,14 @@ async function appReviewPage(input,current) {
|
|
|
if(input.month)add('LEFT('+dateSql+',7)=?',String(input.month));
|
|
if(input.month)add('LEFT('+dateSql+',7)=?',String(input.month));
|
|
|
if(input.status==='completed')clauses.push('m."fxzt"=\'1\'');
|
|
if(input.status==='completed')clauses.push('m."fxzt"=\'1\'');
|
|
|
if(input.status==='pending')clauses.push('COALESCE(m."fxzt",\'0\')<>\'1\'');
|
|
if(input.status==='pending')clauses.push('COALESCE(m."fxzt",\'0\')<>\'1\'');
|
|
|
- const where=clauses.join(' AND '),countPromise=Psql.one('SELECT COUNT(*)::int total FROM "MemoryPracticeRecord" m WHERE '+where,args);
|
|
|
|
|
|
|
+ const where=clauses.join(' AND '),countPromise=input.scope==='today'&&!input.status&&!input.month
|
|
|
|
|
+ ?summaryPromise.then(summary=>({total:number(summary.today)}))
|
|
|
|
|
+ :Psql.one('SELECT COUNT(*)::int total FROM "MemoryPracticeRecord" m WHERE '+where,args);
|
|
|
// Historical createdAt is the import time; the study model preserves the original learning time.
|
|
// Historical createdAt is the import time; the study model preserves the original learning time.
|
|
|
const rowsPromise=Psql.query('SELECT c.*,row_to_json(m) AS "__addon",COALESCE(n."nodeName",CASE WHEN '+notesSource+' THEN \'语法写作复习\' END) AS "kc_title",d."dqrq" AS "learned_date",dc."createTime" AS "learned_at",r."amount" AS money,r."status" AS "rewardStatus",r."ineligibleReason" AS "ineligibleReason",r."completedAt" AS "completedAt",r."originalAmount" AS "originalAmount",r."correctionReason" AS "correctionReason",r."correctionAmount" AS "correctionAmount",'+reviewStudentNameSql()+' AS "honeyName",(SELECT COUNT(*) FROM "MemoryPracticeRecord" sibling WHERE sibling."company"=m."company" AND sibling."yhid"=m."yhid" AND sibling."xxjlid"=m."xxjlid" AND (sibling."kywsj",sibling."id")<=(m."kywsj",m."id"))::int AS "reviewNo" FROM "MemoryPracticeRecord" m JOIN LATERAL (SELECT * FROM "CommonModel" c WHERE c."company"=m."company" AND c."modelId"=60 AND c."itemId"=m."id" OFFSET 0) c ON TRUE LEFT JOIN "Node" n ON n."company"=m."company" AND n."nodeId"::text=m."kcid"::text LEFT JOIN LATERAL (SELECT * FROM "CommonModel" dc WHERE dc."company"=m."company" AND dc."modelId"=56 AND dc."generalId"::text=m."xxjlid"::text OFFSET 0) dc ON TRUE LEFT JOIN "DailyStudyRecord" d ON d."company"=m."company" AND d."id"=dc."itemId" LEFT JOIN "ReviewRewardIncome" r ON r."company"=m."company" AND r."taskId"=m."objectId" LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+where+(input.scope==='today'?' ORDER BY CASE WHEN m."fxzt"=\'1\' THEN 1 ELSE 0 END,m."kywsj",m."id"':' ORDER BY m."kywsj" DESC,m."id" DESC')+' LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
|
|
const rowsPromise=Psql.query('SELECT c.*,row_to_json(m) AS "__addon",COALESCE(n."nodeName",CASE WHEN '+notesSource+' THEN \'语法写作复习\' END) AS "kc_title",d."dqrq" AS "learned_date",dc."createTime" AS "learned_at",r."amount" AS money,r."status" AS "rewardStatus",r."ineligibleReason" AS "ineligibleReason",r."completedAt" AS "completedAt",r."originalAmount" AS "originalAmount",r."correctionReason" AS "correctionReason",r."correctionAmount" AS "correctionAmount",'+reviewStudentNameSql()+' AS "honeyName",(SELECT COUNT(*) FROM "MemoryPracticeRecord" sibling WHERE sibling."company"=m."company" AND sibling."yhid"=m."yhid" AND sibling."xxjlid"=m."xxjlid" AND (sibling."kywsj",sibling."id")<=(m."kywsj",m."id"))::int AS "reviewNo" FROM "MemoryPracticeRecord" m JOIN LATERAL (SELECT * FROM "CommonModel" c WHERE c."company"=m."company" AND c."modelId"=60 AND c."itemId"=m."id" OFFSET 0) c ON TRUE LEFT JOIN "Node" n ON n."company"=m."company" AND n."nodeId"::text=m."kcid"::text LEFT JOIN LATERAL (SELECT * FROM "CommonModel" dc WHERE dc."company"=m."company" AND dc."modelId"=56 AND dc."generalId"::text=m."xxjlid"::text OFFSET 0) dc ON TRUE LEFT JOIN "DailyStudyRecord" d ON d."company"=m."company" AND d."id"=dc."itemId" LEFT JOIN "ReviewRewardIncome" r ON r."company"=m."company" AND r."taskId"=m."objectId" LEFT JOIN "_User" u ON u."company"=m."company" AND u."legacyUserId"=m."yhid" WHERE '+where+(input.scope==='today'?' ORDER BY CASE WHEN m."fxzt"=\'1\' THEN 1 ELSE 0 END,m."kywsj",m."id"':' ORDER BY m."kywsj" DESC,m."id" DESC')+' LIMIT $'+(args.length+1)+' OFFSET $'+(args.length+2),[...args,p.size,(p.index-1)*p.size]);
|
|
|
const versionTarget=coach&&number(input.studentId)?number(input.studentId):uid;
|
|
const versionTarget=coach&&number(input.studentId)?number(input.studentId):uid;
|
|
|
// Keep the receipt view correlated to the selected tasks; flattening it can scan tenant-wide history.
|
|
// Keep the receipt view correlated to the selected tasks; flattening it can scan tenant-wide history.
|
|
|
- const [students,summary,dates,count,rows,income,versions]=await Promise.all([studentsPromise,summaryPromise,datesPromise,countPromise,rowsPromise,coach&&input.includeIncome!==false?Psql.one('SELECT COALESCE(SUM(r."amount"),0) AS income FROM "MemoryPracticeRecord" m JOIN LATERAL (SELECT r."amount" FROM "ReviewRewardIncome" r WHERE r."company"=m."company" AND r."taskId"=m."objectId" OFFSET 0) r ON TRUE WHERE '+where,args).then(row=>number(row.income)):0,appLiveVersionSnapshot(versionTarget,['review','account'],current)]);
|
|
|
|
|
|
|
+ const [students,summary,dates,count,rows,income,versions]=await Promise.all([studentsPromise,summaryPromise,datesPromise,countPromise,rowsPromise,coach&&input.includeIncome!==false?Psql.one('SELECT COALESCE(SUM(r."amount"),0) AS income FROM "MemoryPracticeRecord" m JOIN LATERAL (SELECT r."amount" FROM "ReviewRewardIncome" r WHERE r."company"=m."company" AND r."taskId"=m."objectId" OFFSET 0) r ON TRUE WHERE '+where,args).then(row=>number(row.income)):0,input.includeVersions===false?{}:appLiveVersionSnapshot(versionTarget,['review','account'],current)]);
|
|
|
return {items:rows.map(row=>{const addon=row.__addon;delete row.__addon;return{...legacyAliases({...row,...addon},'CommonModel'),money:number(row.money),rewardStatus:row.rewardStatus||'pending'};}),page:mobilePageMeta(count.total,p),summary:{...summary,todayTotal:number(summary.today),todayCompleted:number(summary.todayCompleted),todayPending:number(summary.today)-number(summary.todayCompleted),overduePending:number(summary.overduePending),income,incomeScope:input.month||'all'},dates,students,legacyReviewSync,serverToday:today,...mobileSource(),...versions};
|
|
return {items:rows.map(row=>{const addon=row.__addon;delete row.__addon;return{...legacyAliases({...row,...addon},'CommonModel'),money:number(row.money),rewardStatus:row.rewardStatus||'pending'};}),page:mobilePageMeta(count.total,p),summary:{...summary,todayTotal:number(summary.today),todayCompleted:number(summary.todayCompleted),todayPending:number(summary.today)-number(summary.todayCompleted),overduePending:number(summary.overduePending),income,incomeScope:input.month||'all'},dates,students,legacyReviewSync,serverToday:today,...mobileSource(),...versions};
|
|
|
}
|
|
}
|
|
|
|
|
|