|
|
@@ -2895,11 +2895,24 @@ async function professionalScheduleDataLocal(context,input) {
|
|
|
}
|
|
|
const unassigned=appointments.filter((row)=>!row.coachId);return{...data,refreshedAt:new Date().toISOString(),totalAppointments:Math.max(history.totalAppointments,live.total),dataDateFrom:history.dataDateFrom,dataDateTo:history.dataDateTo,appointments,unassigned,coaches:coaches.sort((a,b)=>a.displayName.localeCompare(b.displayName,'zh-CN')),liveSourceAvailable:live.available,liveSourceMessage:live.message,liveSourceCount:hydratedLive.length};
|
|
|
}
|
|
|
-async function professionalDashboard(context,input) { const data=await dashboard(context,input),date=businessDate(),snapshot=await appointmentSnapshot(context,input,date),todayRows=snapshot.appointments.filter((row)=>!row.cancelled&&businessDate(row.startsAt)===date),coachPage=await professionalCoachRows(context,{...input,page:1,pageSize:100,search:'',filters:{status:'active'}},snapshot),estimatedPay=coachPage.items.reduce((sum,row)=>sum+number(row.estimatedPay),0),metrics=data.metrics.map((row)=>row.key==='today'?{...row,value:todayRows.length}:row.key==='coaches'?{...row,value:coachPage.total,hint:snapshot.live.available?'新系统与实时业务账号去重统计':row.hint}:row.key==='pay'?{...row,value:estimatedPay}:row),alerts=data.alerts.map((row)=>row.key==='unassigned'?{...row,value:todayRows.filter((item)=>!item.coachId).length}:row);return{...data,date,metrics,alerts,refreshedAt:new Date().toISOString(),liveSourceAvailable:snapshot.live.available,liveSourceMessage:snapshot.live.message}; }
|
|
|
+function addBusinessDays(dateKey,days) { return businessDate(new Date(new Date(dateKey+'T12:00:00+08:00').getTime()+days*86400000)); }
|
|
|
+function scheduleTrendRange(period,today=businessDate()) { if(!['week','month','year'].includes(period))fail(400,'排课趋势时间维度无效');if(period==='week')return{dateFrom:addBusinessDays(today,-6),dateTo:today,periodLabel:'近 7 天'};if(period==='month')return{dateFrom:today.slice(0,8)+'01',dateTo:today,periodLabel:'本月'};return{dateFrom:today.slice(0,4)+'-01-01',dateTo:today,periodLabel:'本年'}; }
|
|
|
+function scheduleTrendFromSnapshot(snapshot,period,today=businessDate()) {
|
|
|
+ const range=scheduleTrendRange(period,today),buckets=[],weekdays=['周日','周一','周二','周三','周四','周五','周六'];
|
|
|
+ if(period==='year'){const year=today.slice(0,4),monthCount=number(today.slice(5,7));for(let month=1;month<=monthCount;month++){const key=year+'-'+String(month).padStart(2,'0');buckets.push({key,label:year+'年'+month+'月',shortLabel:month+'月'});}}
|
|
|
+ else{for(let key=range.dateFrom;key<=range.dateTo;key=addBusinessDays(key,1)){const month=number(key.slice(5,7)),day=number(key.slice(8,10)),weekday=new Date(key+'T12:00:00+08:00').getDay();buckets.push({key,label:month+'月'+day+'日',shortLabel:period==='week'?weekdays[weekday]:day+'日'});}}
|
|
|
+ const byKey=new Map(buckets.map((bucket)=>[bucket.key,{courseCount:0,completedCount:0,students:new Set()}])),allStudents=new Set(),todayStudents=new Set();let todayCourseCount=0;
|
|
|
+ for(const row of snapshot.appointments){if(row.cancelled||!row.startsAt)continue;const date=businessDate(row.startsAt);if(date<range.dateFrom||date>range.dateTo)continue;const key=period==='year'?date.slice(0,7):date,bucket=byKey.get(key);if(!bucket)continue;bucket.courseCount++;if(row.statusCode>=30)bucket.completedCount++;if(row.studentId){bucket.students.add(row.studentId);allStudents.add(row.studentId);}if(date===today){todayCourseCount++;if(row.studentId)todayStudents.add(row.studentId);}}
|
|
|
+ const points=buckets.map((bucket)=>{const value=byKey.get(bucket.key);return{...bucket,courseCount:value.courseCount,studentCount:value.students.size,completedCount:value.completedCount,isToday:period==='year'?bucket.key===today.slice(0,7):bucket.key===today};}),totalCourseCount=points.reduce((sum,point)=>sum+point.courseCount,0),maxValue=Math.max(1,...points.flatMap((point)=>[point.courseCount,point.studentCount]));
|
|
|
+ return{period,periodLabel:range.periodLabel,dateFrom:range.dateFrom,dateTo:range.dateTo,points,todayCourseCount,todayStudentCount:todayStudents.size,totalCourseCount,totalStudentCount:allStudents.size,maxValue,refreshedAt:new Date().toISOString(),liveSourceAvailable:snapshot.live.available,liveSourceMessage:snapshot.live.message};
|
|
|
+}
|
|
|
+async function scheduleTrendData(context,input) { const period=text(input.period||'week'),today=businessDate(),range=scheduleTrendRange(period,today),snapshot=await appointmentSnapshot(context,input,range.dateFrom);return scheduleTrendFromSnapshot(snapshot,period,today); }
|
|
|
+async function professionalDashboard(context,input) { const data=await dashboard(context,input),date=businessDate(),range=scheduleTrendRange('week',date),snapshot=await appointmentSnapshot(context,input,range.dateFrom),todayRows=snapshot.appointments.filter((row)=>!row.cancelled&&row.startsAt&&businessDate(row.startsAt)===date),currentLiveRows=snapshot.live.allItems.filter((row)=>row.startsAt&&businessDate(row.startsAt)>=date),coachSnapshot={...snapshot,live:{...snapshot.live,allItems:currentLiveRows},appointments:mergeAppointments(snapshot.stored,currentLiveRows)},coachPage=await professionalCoachRows(context,{...input,page:1,pageSize:100,search:'',filters:{status:'active'}},coachSnapshot),estimatedPay=coachPage.items.reduce((sum,row)=>sum+number(row.estimatedPay),0),metrics=data.metrics.map((row)=>row.key==='today'?{...row,value:todayRows.length}:row.key==='coaches'?{...row,value:coachPage.total,hint:snapshot.live.available?'新系统与实时业务账号去重统计':row.hint}:row.key==='pay'?{...row,value:estimatedPay}:row),alerts=data.alerts.map((row)=>row.key==='unassigned'?{...row,value:todayRows.filter((item)=>!item.coachId).length}:row);return{...data,date,metrics,alerts,scheduleTrend:scheduleTrendFromSnapshot(snapshot,'week',date),refreshedAt:new Date().toISOString(),liveSourceAvailable:snapshot.live.available,liveSourceMessage:snapshot.live.message}; }
|
|
|
async function handler(request,response) {
|
|
|
try {
|
|
|
const input=inputOf(request); const operation=text(input.operation); const context=await auth(request); const payload=input.payload&&typeof input.payload==='object'?input.payload:{};
|
|
|
if(operation==='ops/dashboard/summary') return response.json({success:true,data:await professionalDashboard(context,input)});
|
|
|
+ if(operation==='ops/dashboard/schedule-trend') return response.json({success:true,data:await scheduleTrendData(context,input)});
|
|
|
if(operation==='ops/coaches/list') return response.json({success:true,data:await professionalCoachRows(context,input)});
|
|
|
if(operation==='ops/coaches/detail') return response.json({success:true,data:await professionalCoachDetail(context,input,text(input.objectId))});
|
|
|
if(operation==='ops/coaches/create') return response.json({success:true,data:await createCoach(context,input,payload)});
|