|
|
@@ -3328,12 +3328,13 @@ async function deleteCoach(context,input) {
|
|
|
}
|
|
|
function businessDate(value=new Date()) { return new Date(value).toLocaleDateString('sv-SE',{timeZone:'Asia/Shanghai'}); }
|
|
|
async function appointmentCountsForBusinessDate(context,input,date=businessDate()) { const appointments=await allAppointmentRows(context,input),active=appointments.filter((row)=>!row.cancelled&&businessDate(row.startsAt)===date),byCoach=new Map();for(const row of active)if(row.coachId)byCoach.set(row.coachId,(byCoach.get(row.coachId)||0)+1);return{active,byCoach}; }
|
|
|
-function appointmentHistory(rows) { const byCoach=new Map(),dates=[],now=new Date().toISOString();for(const row of rows){const date=row.startsAt?businessDate(row.startsAt):'';if(/^\d{4}-\d{2}-\d{2}$/.test(date))dates.push(date);if(!row.coachId)continue;const current=byCoach.get(row.coachId)||{totalAppointments:0,lastAppointmentAt:'',nextAppointmentAt:''};current.totalAppointments+=1;if(row.startsAt>=now){if(!current.nextAppointmentAt||row.startsAt<current.nextAppointmentAt)current.nextAppointmentAt=row.startsAt;}else if(row.startsAt>current.lastAppointmentAt)current.lastAppointmentAt=row.startsAt;byCoach.set(row.coachId,current);}dates.sort();return{byCoach,totalAppointments:rows.length,dataDateFrom:dates[0]||'',dataDateTo:dates[dates.length-1]||''};}
|
|
|
+function appointmentHistory(rows) { const byCoach=new Map(),dates=[],now=new Date().toISOString();let totalAppointments=0;for(const row of rows){if(row.cancelled)continue;totalAppointments+=1;const date=row.startsAt?businessDate(row.startsAt):'';if(/^\d{4}-\d{2}-\d{2}$/.test(date))dates.push(date);if(!row.coachId)continue;const current=byCoach.get(row.coachId)||{totalAppointments:0,lastAppointmentAt:'',nextAppointmentAt:''};current.totalAppointments+=1;if(row.startsAt>=now&&number(row.statusCode)<30){if(!current.nextAppointmentAt||row.startsAt<current.nextAppointmentAt)current.nextAppointmentAt=row.startsAt;}else if(row.startsAt<now&&row.startsAt>current.lastAppointmentAt)current.lastAppointmentAt=row.startsAt;byCoach.set(row.coachId,current);}dates.sort();return{byCoach,totalAppointments,dataDateFrom:dates[0]||'',dataDateTo:dates[dates.length-1]||''};}
|
|
|
+function coachActivityCompare(a,b) { const aNext=text(a.nextAppointmentAt),bNext=text(b.nextAppointmentAt);if(Boolean(aNext)!==Boolean(bNext))return aNext?-1:1;if(aNext&&bNext){const futureOrder=aNext.localeCompare(bNext);if(futureOrder)return futureOrder;}const aLast=text(a.lastAppointmentAt),bLast=text(b.lastAppointmentAt);if(Boolean(aLast)!==Boolean(bLast))return aLast?-1:1;if(aLast&&bLast){const historyOrder=bLast.localeCompare(aLast);if(historyOrder)return historyOrder;}return text(a.displayName).localeCompare(text(b.displayName),'zh-CN')||number(a.userId)-number(b.userId);}
|
|
|
function mergeAppointments(stored,liveRows) { const merged=new Map();for(const row of stored)merged.set(row.generalId?'general:'+row.generalId:'target:'+row.targetId,row);for(const row of liveRows)merged.set(row.generalId?'general:'+row.generalId:'target:'+row.targetId,row);return Array.from(merged.values()); }
|
|
|
async function appointmentSnapshot(context,input,dateFrom=businessDate()) { const stored=await allAppointmentRows(context,input),legacyRefresh=input.legacyRefresh===true,live=legacyRefresh?await cachedLiveAppointmentRows(context,input,dateFrom,true):{available:true,mode:'parse',message:'新数据库排课投影',items:[],allItems:[],total:stored.length,latestAt:stored.reduce((latest,row)=>row.startsAt>latest?row.startsAt:latest,''),refreshedAt:new Date().toISOString(),cacheHit:false,stale:false},appointments=legacyRefresh?mergeAppointments(stored,live.allItems):stored;return{stored,live,appointments}; }
|
|
|
-function professionalCoachPageFromBase(context,input,month,base){const paging=pageInput(input),status=text(paging.filters.status||'all'),filtered=searchRows((base.rows||[]).filter((row)=>status==='all'||row.status===status),input).sort((a,b)=>a.displayName.localeCompare(b.displayName,'zh-CN')),page=pageRows(filtered,input);return{...page,summary:base.summary||{total:(base.rows||[]).length,active:(base.rows||[]).filter((row)=>row.status==='active').length,disabled:(base.rows||[]).filter((row)=>row.status==='disabled').length,monthActive:(base.rows||[]).filter((row)=>row.monthLessons>0).length,liveOnly:(base.rows||[]).filter((row)=>row.readOnly===true).length},canManage:roleOf(context)==='ops-manager',stores:base.stores||[],liveSourceAvailable:base.liveSourceAvailable===true,liveSourceMessage:text(base.liveSourceMessage),liveSourceCount:number(base.liveSourceCount),dataDateFrom:text(base.dataDateFrom),dataDateTo:text(base.dataDateTo),payrollMonth:month,payrollSourceStatus:base.payrollSourceStatus,payrollRefreshedAt:text(base.payrollRefreshedAt),sync:base.sync};}
|
|
|
+function professionalCoachPageFromBase(context,input,month,base){const paging=pageInput(input),status=text(paging.filters.status||'all'),filtered=searchRows((base.rows||[]).filter((row)=>status==='all'||row.status===status),input).sort(coachActivityCompare),page=pageRows(filtered,input);return{...page,summary:base.summary||{total:(base.rows||[]).length,active:(base.rows||[]).filter((row)=>row.status==='active').length,disabled:(base.rows||[]).filter((row)=>row.status==='disabled').length,monthActive:(base.rows||[]).filter((row)=>row.monthLessons>0).length,liveOnly:(base.rows||[]).filter((row)=>row.readOnly===true).length},canManage:roleOf(context)==='ops-manager',stores:base.stores||[],liveSourceAvailable:base.liveSourceAvailable===true,liveSourceMessage:text(base.liveSourceMessage),liveSourceCount:number(base.liveSourceCount),dataDateFrom:text(base.dataDateFrom),dataDateTo:text(base.dataDateTo),payrollMonth:month,payrollSourceStatus:base.payrollSourceStatus,payrollRefreshedAt:text(base.payrollRefreshedAt),sync:base.sync};}
|
|
|
async function professionalCoachRows(context,input,providedSnapshot=null,providedPayroll=null) {
|
|
|
- const month=/^\d{4}-\d{2}$/.test(text(input.month))?text(input.month):businessDate().slice(0,7),baseKey='coach-page-base-v5:'+month+':'+roleOf(context);if(!providedSnapshot&&!providedPayroll&&input.refresh!==true){const cached=await realtimeCacheRead(context,input,baseKey);if(cached&&cached.age<REALTIME_CACHE_TTL_MS)return professionalCoachPageFromBase(context,input,month,cached.payload);}
|
|
|
+ const month=/^\d{4}-\d{2}$/.test(text(input.month))?text(input.month):businessDate().slice(0,7),baseKey='coach-page-base-v6:'+month+':'+roleOf(context);if(!providedSnapshot&&!providedPayroll&&input.refresh!==true){const cached=await realtimeCacheRead(context,input,baseKey);if(cached&&cached.age<REALTIME_CACHE_TTL_MS)return professionalCoachPageFromBase(context,input,month,cached.payload);}
|
|
|
const [local,snapshot,payroll,legacyAccounts]=await Promise.all([coachRows(context,{...input,page:1,pageSize:100,search:'',filters:{...(input.filters||{}),status:'all'}},false),providedSnapshot||appointmentSnapshot(context,input,month+'-01'),providedPayroll||payrollOverviewData(context,{...input,month,storeId:0,search:'',refresh:input.refresh===true}),optionalLegacyMemberSnapshot(context,input)]),history=appointmentHistory(snapshot.appointments),today=businessDate(),todayCounts=new Map(),pendingCounts=new Map(),payrollMap=new Map(payroll.coachItems.map((row)=>[number(row.coachId),row])),accountMap=new Map((legacyAccounts.items||[]).map((row)=>[number(row.userId),row]));
|
|
|
for(const row of snapshot.appointments){if(!row.coachId||row.cancelled)continue;if(businessDate(row.startsAt)===today)todayCounts.set(row.coachId,(todayCounts.get(row.coachId)||0)+1);if(row.statusCode<30&&businessDate(row.startsAt)>=today)pendingCounts.set(row.coachId,(pendingCounts.get(row.coachId)||0)+1);}
|
|
|
const rows=local.map((row)=>{const pay=payrollMap.get(row.userId),account=accountMap.get(row.userId),accountName=account&&text(account.realName||account.nickname||account.username),mobile=text(account&&account.mobile)||row.mobile||row.mobileMasked,parentUserId=number(account&&account.parentUserId,row.parentUserId),status=account?(number(account.state,1)===0?'disabled':'active'):row.status;return{...row,username:text(account&&account.username)||row.username,displayName:accountName||row.displayName,mobile:roleOf(context)==='ops-manager'?mobile:'',mobileMasked:maskMobile(mobile),parentUserId,storeName:text(account&&account.parentDisplayName)||row.storeName,status,statusLabel:status==='disabled'?'已停用':'正常',todayAppointments:todayCounts.get(row.userId)||0,pendingAppointments:pendingCounts.get(row.userId)||0,...(history.byCoach.get(row.userId)||{totalAppointments:0,lastAppointmentAt:'',nextAppointmentAt:''}),monthLessons:number(pay&&pay.lessonCount),monthHours:number(pay&&pay.totalHours),estimatedPay:number(pay&&pay.payableAmount),averageScore:number(pay&&pay.averageScore,row.averageScore),lastLessonAt:text(pay&&pay.latestLessonAt)||row.lastLessonAt,registeredAt:text(account&&account.registeredAt),hasCurrentBusiness:Boolean(number(pay&&pay.lessonCount)||todayCounts.get(row.userId)||pendingCounts.get(row.userId)),source:'hybrid',sourceLabel:account?(payroll.sourceStatus.mode==='live'?'旧账号已关联 · 实时工资':'旧账号已关联 · 缓存工资'):(payroll.sourceStatus.mode==='live'?'新系统账号 · 实时工资':'新系统账号 · 缓存工资'),readOnly:false};}),knownCoachIds=new Set(rows.map((row)=>row.userId));
|
|
|
@@ -3344,7 +3345,7 @@ async function professionalCoachRows(context,input,providedSnapshot=null,provide
|
|
|
}
|
|
|
async function professionalCoachDetail(context,input,objectId) {
|
|
|
const month=/^\d{4}-\d{2}$/.test(text(input.month))?text(input.month):businessDate().slice(0,7),detailKey='coach-detail:'+objectId+':'+month+':'+roleOf(context);if(input.refresh!==true){const cached=await realtimeCacheRead(context,input,detailKey);if(cached&&cached.age<REALTIME_CACHE_TTL_MS)return cached.payload;}
|
|
|
- const [payroll,snapshot,availability,legacyAccounts]=await Promise.all([payrollOverviewData(context,{...input,month,storeId:0,search:'',refresh:input.refresh===true}),appointmentSnapshot(context,input,month+'-01'),availabilityRows(context,input),optionalLegacyMemberSnapshot(context,input)]),syntheticCoachId=number((objectId.match(/:(\d+)$/)||[])[1]),selectedBase=await realtimeCacheRead(context,input,'coach-page-base-v5:'+month+':'+roleOf(context)),currentBase=selectedBase||await realtimeCacheRead(context,input,'coach-page-base-v5:'+businessDate().slice(0,7)+':'+roleOf(context));let coach=currentBase&¤tBase.payload&&Array.isArray(currentBase.payload.rows)?currentBase.payload.rows.find((row)=>row.objectId===objectId):null;
|
|
|
+ const [payroll,snapshot,availability,legacyAccounts]=await Promise.all([payrollOverviewData(context,{...input,month,storeId:0,search:'',refresh:input.refresh===true}),appointmentSnapshot(context,input,month+'-01'),availabilityRows(context,input),optionalLegacyMemberSnapshot(context,input)]),syntheticCoachId=number((objectId.match(/:(\d+)$/)||[])[1]),selectedBase=await realtimeCacheRead(context,input,'coach-page-base-v6:'+month+':'+roleOf(context)),currentBase=selectedBase||await realtimeCacheRead(context,input,'coach-page-base-v6:'+businessDate().slice(0,7)+':'+roleOf(context));let coach=currentBase&¤tBase.payload&&Array.isArray(currentBase.payload.rows)?currentBase.payload.rows.find((row)=>row.objectId===objectId):null;
|
|
|
if(!coach&&!syntheticCoachId){const local=await coachRows(context,{...input,page:1,pageSize:100,search:'',filters:{status:'all'}},false);coach=local.find((row)=>row.objectId===objectId);}
|
|
|
if(!coach&&syntheticCoachId){const pay=payroll.coachItems.find((row)=>row.coachId===syntheticCoachId),appointment=snapshot.appointments.find((row)=>row.coachId===syntheticCoachId),account=(legacyAccounts.items||[]).find((row)=>number(row.userId)===syntheticCoachId),mobile=text(account&&account.mobile),status=account&&number(account.state,1)===0?'disabled':'active';coach={objectId,userId:syntheticCoachId,username:text(account&&account.username),displayName:text(account&&(account.realName||account.nickname||account.username)||pay&&pay.coachName||appointment&&appointment.coachName)||'陪练 '+syntheticCoachId,mobile:roleOf(context)==='ops-manager'?mobile:'',mobileMasked:maskMobile(mobile),parentUserId:number(account&&account.parentUserId||pay&&pay.storeId),storeName:text(account&&account.parentDisplayName||pay&&pay.storeName)||'未绑定门店',status,statusLabel:status==='disabled'?'已停用':'正常',todayAppointments:0,pendingAppointments:0,totalAppointments:0,lastAppointmentAt:'',monthLessons:number(pay&&pay.lessonCount),monthHours:number(pay&&pay.totalHours),estimatedPay:number(pay&&pay.payableAmount),averageScore:number(pay&&pay.averageScore),lastLessonAt:text(pay&&pay.latestLessonAt),availabilityConfigured:false,updatedAt:text(account&&account.updatedAt),source:'legacy-live',sourceLabel:pay?'旧系统账号 · 实时工资':'旧系统账号 · 历史授课证据',readOnly:true};}
|
|
|
if(!coach)fail(404,'陪练不存在或尚未完成身份同步');
|