// Baseline captured before bounded concurrent mobile reads, 2026-11-10. async function appCompanionOverview(input,current) { const uid=await authorizeRequestedUser(current,input.uid),coach=number(current.get('legacyGroupId'))===3,history=input.history===true,p=mobilePage(input); assertLessonTenant(current); if(!coach&&input.source!=='database')return appStudentCompanionOverview(input,current,uid,p); const args=[DEFAULT_COMPANY_ID,String(uid)],clauses=['a."company"=$1','a."'+(coach?'pl':'szyh')+'"=$2']; const date="to_char(xs_appointment_day(a.\"bxrq\",a.\"yysj\"),'YYYY-MM-DD')"; const add=(sql,value)=>{args.push(value);clauses.push(sql.replace('?', '$'+args.length));}; if(history)clauses.push('(a."dszt"::numeric>=30 OR c."status"=-2)');else clauses.push('COALESCE(c."status",99)<>-2'); if(coach&&number(input.studentId))add('a."szyh"=?',String(input.studentId)); if(input.search)add('c."title" ILIKE ?','%'+String(input.search).trim().slice(0,100)+'%'); if(input.month&&/^\d{4}-\d{2}$/.test(input.month))add('LEFT('+date+',7)=?',input.month); const join=' FROM "CourseAppointment" a LEFT JOIN "CommonModel" c ON c."company"=a."company" AND c."modelId"=54 AND c."itemId"=a."id"'; const summaryWhere=clauses.join(' AND '),summaryArgs=[...args]; const dates=await Psql.query('SELECT '+date+' AS date,COUNT(*) FILTER(WHERE a."dszt"::numeric>=30 AND COALESCE(c."status",99)<>-2)::int finished,COUNT(*) FILTER(WHERE a."dszt"::numeric<30 AND COALESCE(c."status",99)<>-2)::int planned'+join+' WHERE '+summaryWhere+' GROUP BY '+date,summaryArgs); const studentOptions=coach?await Psql.query('SELECT a."szyh" AS id,MAX('+companionStudentNameSql()+') AS name,COUNT(*)::int count'+join+' LEFT JOIN "_User" u ON u."company"=a."company" AND u."legacyUserId"::text=a."szyh" WHERE a."company"=$1 AND a."pl"=$2 AND COALESCE(c."status",99)<>-2 GROUP BY a."szyh" ORDER BY name',[DEFAULT_COMPANY_ID,String(uid)]):[]; if(input.dateStart)add(date+'>=?',String(input.dateStart)); if(input.dateEnd)add(date+'<=?',String(input.dateEnd)); if(input.filter==='finished')clauses.push('a."dszt"::numeric>=30 AND COALESCE(c."status",99)<>-2'); if(input.filter==='pending'){clauses.push('a."dszt"::numeric<30 AND COALESCE(c."status",99)<>-2');add(date+'sum+(Number(row.commonStatus)!==-2&&row.payStatus==='earned'?Math.round(Number(row.teacherPay)*100):0),0)/100,pendingCount:paidRows.filter(row=>Number(row.commonStatus)!==-2&&row.payStatus!=='earned').length}; const payById=new Map(paidRows.map(row=>[String(row.generalId),row])); scheduleRows=rows.map(row=>{ const pay=payById.get(String(row.generalId)); if(!pay)return row; return {...row,teacherPay:pay.teacherPay,payStatus:pay.payStatus,payReason:pay.payReason,feedbackStatus:pay.feedbackStatus}; }); } const live=input.source==='database'?await databaseAppointmentVersions({studentId:coach?input.studentId:undefined},current):await appLiveVersionSnapshot(uid,['courses','appointments'],current);return {earningsSummary,schedules:scheduleRows.map(appOverviewScheduleRow),courses:courses.map(appOverviewCourseRow),page:mobilePageMeta(count.total,p),dates,studentOptions,...mobileSource(),...live}; } async function appAccountOverview(input,current) { const uid=await authorizeRequestedUser(current,input.uid),groupId=number(current.get('legacyGroupId')||legacyData(current).GroupID),profile={...legacyUser(current),...legacyUserAddon(current)},today=appOverviewToday(),todayCompact=today.replace(/-/g,''); if(groupId===3){const [lessons,schedules,reviews]=await Promise.all([ Psql.one('SELECT COUNT(*)::int AS total,COUNT(*) FILTER (WHERE "kclx"=\'1\')::int AS t30,COUNT(*) FILTER (WHERE "kclx"=\'2\')::int AS t60,COUNT(*) FILTER (WHERE "kclx"=\'3\')::int AS trial FROM "LessonRecord" WHERE "company"=$1 AND "jsmz"=$2',[DEFAULT_COMPANY_ID,String(uid)]), Psql.one('SELECT COUNT(*) FILTER (WHERE REPLACE(LEFT(COALESCE(NULLIF("bxrq",\'\'),"yysj"),10),\'-\',\'\')=$3)::int AS today,COUNT(*) FILTER (WHERE "dszt"~\'^\\d+(?:\\.0+)?$\' AND "dszt"::numeric>=30)::int AS completed FROM "CourseAppointment" WHERE NOT EXISTS (SELECT 1 FROM "CommonModel" cancelled WHERE cancelled."company"="CourseAppointment"."company" AND cancelled."modelId"=54 AND cancelled."itemId"="CourseAppointment"."id" AND cancelled."status"=-2) AND "company"=$1 AND "pl"=$2',[DEFAULT_COMPANY_ID,String(uid),todayCompact]), Psql.one('SELECT COUNT(*)::int AS total,COUNT(*) FILTER (WHERE "fxzt"=\'1\')::int AS completed FROM "MemoryPracticeRecord" WHERE "company"=$1 AND "plid"=$2',[DEFAULT_COMPANY_ID,uid]) ]);const t30=number(lessons.t30),t60=number(lessons.t60),trial=number(lessons.trial),live=await appLiveVersionSnapshot(uid,['appointments','review','account'],current);return{profile,coachPanel:{todaySchedules:number(schedules.today),completedLessons:number(schedules.completed),...await teacherIncomeSummary(DEFAULT_COMPANY_ID,uid),reviewRecords:number(reviews.total),totalLessons:number(lessons.total),totalHours:t30*.5+t60+trial,course30:t30,course60:t60,trialLessons:trial},...live};} const [legacyReviewSync,legacyAppointmentSync]=await Promise.all([syncLegacyReviews(uid,current,input.refresh===true),input.source==='database'?Promise.resolve({state:'database',items:[]}):syncLegacyAppointments('szyh',uid,input.refresh===true)]),[companion,memory]=await Promise.all([ Psql.one('SELECT COUNT(*) FILTER (WHERE "dszt"~\'^\\d+(?:\\.0+)?$\' AND "dszt"::numeric>=30)::int AS completed,COALESCE(SUM(CASE WHEN "dszt"~\'^\\d+(?:\\.0+)?$\' AND "dszt"::numeric>=30 THEN CASE WHEN "dslx"=\'1\' THEN 30 WHEN "dslx" IN (\'2\',\'3\') THEN 60 ELSE 0 END ELSE 0 END),0)::int AS minutes FROM "CourseAppointment" WHERE NOT EXISTS (SELECT 1 FROM "CommonModel" cancelled WHERE cancelled."company"="CourseAppointment"."company" AND cancelled."modelId"=54 AND cancelled."itemId"="CourseAppointment"."id" AND cancelled."status"=-2) AND "company"=$1 AND "szyh"=$2',[DEFAULT_COMPANY_ID,String(uid)]), Psql.one('SELECT COUNT(*)::int AS total,COUNT(*) FILTER (WHERE "fxzt"=\'1\')::int AS completed,COUNT(*) FILTER (WHERE LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10)=$3)::int AS today_total,COUNT(*) FILTER (WHERE LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10)=$3 AND "fxzt"=\'1\')::int AS today_completed,COUNT(*) FILTER (WHERE COALESCE("fxzt",\'0\')<>\'1\' AND LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10)<=$3)::int AS due_pending,COUNT(*) FILTER (WHERE COALESCE("fxzt",\'0\')<>\'1\' AND LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10)>$3)::int AS scheduled,COUNT(DISTINCT LEFT(COALESCE(NULLIF("kywrq",\'\'),"kywsj"),10))::int AS scheduled_days FROM "MemoryPracticeRecord" WHERE "company"=$1 AND "yhid"=$2',[DEFAULT_COMPANY_ID,uid,today]) ]);const total=number(memory.total),completed=number(memory.completed),legacyCompleted=legacyAppointmentSync.state==='current'?legacyAppointmentSync.items.filter((row)=>row.cancelled!==true&&number(row.dszt)>=30):null,completedSessions=legacyCompleted?legacyCompleted.length:number(companion.completed),completedMinutes=legacyCompleted?legacyCompleted.reduce((sum,row)=>sum+(number(row.dslx)===1?30:[2,3].includes(number(row.dslx))?60:0),0):number(companion.minutes),scopes=['appointments','review','account'],live=withLegacyAppointmentVersion(await appLiveVersionSnapshot(uid,scopes,current),scopes,legacyAppointmentSync);return{profile,companionSummary:{completedSessions,completedMinutes},antiForgetSummary:{total,completed,progress:total?Math.round(completed/total*100):0,todayTotal:number(memory.today_total),todayCompleted:number(memory.today_completed),duePending:number(memory.due_pending),scheduled:number(memory.scheduled),scheduledDays:number(memory.scheduled_days)},legacyReviewSync,legacyAppointmentSync,...live}; } function learnedDate(row) { const raw = String(row.dqrq || '').trim(); return /^\d{8}$/.test(raw) ? raw.slice(0,4) + '-' + raw.slice(4,6) + '-' + raw.slice(6,8) : String(row.kywsj || '').slice(0,10); } function learnedAt(row) { for (const value of [row.studyCommonCreateTime,row.studyCommonCreatedAt,row.studyRecordCreatedAt,row.studyRecordUpdatedAt]) { if (!value) continue; const date = value instanceof Date ? value : new Date(value); if (!Number.isNaN(date.getTime()) && date.getUTCFullYear() > 1970) return date.toISOString(); } return learnedDate(row); }