|
|
@@ -2770,7 +2770,7 @@ async function availabilityRows(context,input,coachId=0) { const rows=await newC
|
|
|
function defaultPayRates() { return [{objectId:'default-1',classType:1,classTypeLabel:classType(1),amount:20,durationHours:.5,effectiveFrom:'1970-01-01',active:true,isDefault:true},{objectId:'default-2',classType:2,classTypeLabel:classType(2),amount:40,durationHours:1,effectiveFrom:'1970-01-01',active:true,isDefault:true},{objectId:'default-3',classType:3,classTypeLabel:classType(3),amount:40,durationHours:1,effectiveFrom:'1970-01-01',active:true,isDefault:true}]; }
|
|
|
async function payRateRows(context,input) { const stored=(await newClassObjects('CoachPayRate',context,input,1000)).map((row)=>({objectId:row.id,classType:number(row.get('classType')),classTypeLabel:classType(row.get('classType')),amount:number(row.get('amount')),durationHours:number(row.get('durationHours'),durationHours(row.get('classType'))),effectiveFrom:text(row.get('effectiveFrom')),active:row.get('active')!==false,isDefault:false,updatedAt:dateIso(row.updatedAt)})); return defaultPayRates().concat(stored).sort((a,b)=>String(b.effectiveFrom).localeCompare(String(a.effectiveFrom))); }
|
|
|
function rateFor(rates,type,date) { return rates.filter((row)=>row.active&&row.classType===number(type)&&row.effectiveFrom<=date.slice(0,10)).sort((a,b)=>b.effectiveFrom.localeCompare(a.effectiveFrom))[0]||null; }
|
|
|
-async function coachRows(context,input) {
|
|
|
+async function coachRows(context,input,paginate=true) {
|
|
|
const companyId=companyIdOf(context,input),today=new Date().toISOString().slice(0,10),month=today.slice(0,7);
|
|
|
const users=await Psql.query('SELECT u.*,p."realName" AS "storeRealName",p."nickname" AS "storeNickname",p."username" AS "storeUsername",p."legacyUserData" AS "storeLegacy" FROM "_User" u LEFT JOIN "_User" p ON p."company"=u."company" AND COALESCE(CAST(p."legacyUserId" AS text),p."legacyUserData"->>\'UserID\')=COALESCE(u."legacyUserData"->>\'ParentUserID\',\'0\') WHERE ($1::text IS NULL OR u."company"=$1) AND COALESCE(CAST(u."legacyGroupId" AS text),u."legacyUserData"->>\'GroupID\')=\'3\' AND (u."isDeleted" IS NULL OR u."isDeleted"=FALSE) ORDER BY u."createdAt" DESC',[companyId]);
|
|
|
const appts=await Psql.query('SELECT CAST("pl" AS text) AS uid,COUNT(*) FILTER (WHERE COALESCE("yysj","bxrq") LIKE $2||\'%\')::int AS today_count,COUNT(*) FILTER (WHERE CAST(COALESCE("dszt",\'0\') AS text) IN (\'0\',\'10\'))::int AS pending_count FROM "CourseAppointment" WHERE ($1::text IS NULL OR "company"=$1) GROUP BY CAST("pl" AS text)',[companyId,today]);
|
|
|
@@ -2779,7 +2779,7 @@ async function coachRows(context,input) {
|
|
|
for(const row of lessonFacts){const uid=text(row.uid);if(!uid)continue;const lessonAt=firstDateIso(row.appointmentEnd,row.kzsj,row.appointmentStart,row.plpjsj),type=number(row.kclx),score=number(row.pf),entry=lessonMap.get(uid)||{month_count:0,month_hours:0,estimated_pay:0,score_total:0,score_count:0,average_score:0,last_at:''};if(lessonAt&&lessonAt.slice(0,7)===month){entry.month_count++;entry.month_hours+=durationHours(type);entry.estimated_pay+=type===1?20:[2,3].includes(type)?40:0;}if(score>0){entry.score_total+=score;entry.score_count++;entry.average_score=entry.score_total/entry.score_count;}if(lessonAt&&lessonAt>entry.last_at)entry.last_at=lessonAt;lessonMap.set(uid,entry);}
|
|
|
const availability=new Set((await availabilityRows(context,input)).map((row)=>row.coachId)),apptMap=new Map(appts.map((row)=>[text(row.uid),row]));
|
|
|
let normalized=users.map((row)=>{const legacy=legacyOf(row),uid=number(row.legacyUserId||legacy.UserID),storeLegacy=legacyOf({legacyUserData:row.storeLegacy}),a=apptMap.get(String(uid))||{},l=lessonMap.get(String(uid))||{},mobile=mobileOf(row);return{objectId:text(row.objectId),userId:uid,username:text(row.username),displayName:displayName(row),mobile:roleOf(context)==='ops-manager'?mobile:'',mobileMasked:maskMobile(mobile),parentUserId:number(legacy.ParentUserID),storeName:text(row.storeRealName||row.storeNickname||storeLegacy.HoneyName||row.storeUsername)||'未绑定',status:row.isDisabled===true?'disabled':'active',statusLabel:row.isDisabled===true?'已停用':'正常',todayAppointments:number(a.today_count),pendingAppointments:number(a.pending_count),monthLessons:number(l.month_count),monthHours:number(l.month_hours),estimatedPay:number(l.estimated_pay),averageScore:number(l.average_score),lastLessonAt:dateIso(l.last_at),availabilityConfigured:availability.has(uid),updatedAt:dateIso(row.updatedAt)}});
|
|
|
- const paging=pageInput(input),status=text(paging.filters.status||'all');if(status!=='all')normalized=normalized.filter((row)=>row.status===status);return pageRows(searchRows(normalized,input),input);
|
|
|
+ const paging=pageInput(input),status=text(paging.filters.status||'all');if(status!=='all')normalized=normalized.filter((row)=>row.status===status);const filtered=searchRows(normalized,input);return paginate?pageRows(filtered,input):filtered;
|
|
|
}
|
|
|
async function scheduleData(context,input) { const from=text(input.dateFrom)||new Date().toISOString().slice(0,10),to=text(input.dateTo)||from,coachId=number((input.filters||{}).coachId); const coachPage=await coachRows(context,{...input,page:1,pageSize:100,search:'',filters:{status:'active'}}),all=await allAppointmentRows(context,input),appointments=all.filter((row)=>row.startsAt.slice(0,10)>=from&&row.startsAt.slice(0,10)<=to&&(!coachId||row.coachId===coachId)),unassigned=appointments.filter((row)=>!row.coachId&&!row.cancelled); return {dateFrom:from,dateTo:to,refreshedAt:new Date().toISOString(),coaches:coachPage.items,appointments,unassigned}; }
|
|
|
function recurrenceCandidates(payload) { const count=Math.min(60,Math.max(1,number(payload.occurrences,1))),base=appointmentDate('',payload.date,payload.startTime),type=number(payload.classType),mode=text(payload.recurrence)||'once',items=[]; for(let index=0;index<count;index++){const start=new Date(base); if(mode==='daily')start.setDate(start.getDate()+index);else if(mode==='alternate')start.setDate(start.getDate()+index*2);else if(mode==='weekly')start.setDate(start.getDate()+index*7);else if(mode==='same_day_30')start.setMinutes(start.getMinutes()+index*30);else if(mode==='same_day_60')start.setMinutes(start.getMinutes()+index*60);else if(index>0)break; items.push({index,start,end:new Date(start.getTime()+durationMinutes(type)*60000)});} return items; }
|
|
|
@@ -2875,18 +2875,27 @@ 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=[];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:''};current.totalAppointments+=1;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]||''};}
|
|
|
-async function professionalCoachRows(context,input) { const page=await coachRows(context,input),all=await allAppointmentRows(context,input),counts=await appointmentCountsForBusinessDate(context,input),history=appointmentHistory(all),companyId=companyIdOf(context,input),storeRows=await Psql.query('SELECT "objectId","username","realName","nickname","legacyUserId","legacyUserData" FROM "_User" WHERE ($1::text IS NULL OR "company"=$1) AND COALESCE(CAST("legacyGroupId" AS text),"legacyUserData"->>\'GroupID\')=\'2\' AND ("isDeleted" IS NULL OR "isDeleted"=FALSE) ORDER BY COALESCE(NULLIF("realName",\'\'),NULLIF("nickname",\'\'),"username")',[companyId]);return{...page,canManage:roleOf(context)==='ops-manager',stores:storeRows.map((row)=>({objectId:text(row.objectId),userId:number(row.legacyUserId||legacyOf(row).UserID),displayName:displayName(row)})).filter((row)=>row.userId),items:page.items.map((row)=>({...row,todayAppointments:counts.byCoach.get(row.userId)||0,...(history.byCoach.get(row.userId)||{totalAppointments:0,lastAppointmentAt:''})}))}; }
|
|
|
+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),live=await liveAppointmentRows({dateFrom}),appointments=mergeAppointments(stored,live.allItems);return{stored,live,appointments}; }
|
|
|
+async function professionalCoachRows(context,input,providedSnapshot=null) {
|
|
|
+ const local=await coachRows(context,{...input,page:1,pageSize:100,search:'',filters:{...(input.filters||{}),status:'all'}},false),snapshot=providedSnapshot||await appointmentSnapshot(context,input),history=appointmentHistory(snapshot.appointments),today=businessDate(),todayCounts=new Map(),pendingCounts=new Map();
|
|
|
+ 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)pendingCounts.set(row.coachId,(pendingCounts.get(row.coachId)||0)+1);}
|
|
|
+ const rows=local.map((row)=>({...row,todayAppointments:todayCounts.get(row.userId)||0,pendingAppointments:pendingCounts.get(row.userId)||0,...(history.byCoach.get(row.userId)||{totalAppointments:0,lastAppointmentAt:''}),source:'hybrid',sourceLabel:snapshot.live.available?'新系统账号 · 实时排课':'新系统账号',readOnly:false})),knownCoachIds=new Set(rows.map((row)=>row.userId));
|
|
|
+ for(const appointment of snapshot.live.allItems){if(!appointment.coachId||knownCoachIds.has(appointment.coachId))continue;knownCoachIds.add(appointment.coachId);const coachHistory=history.byCoach.get(appointment.coachId)||{totalAppointments:0,lastAppointmentAt:''};rows.push({objectId:'legacy-live-coach:'+appointment.coachId,userId:appointment.coachId,username:'',displayName:appointment.coachName,mobile:'',mobileMasked:'',parentUserId:0,storeName:'实时业务系统',status:'active',statusLabel:'实时账号',todayAppointments:todayCounts.get(appointment.coachId)||0,pendingAppointments:pendingCounts.get(appointment.coachId)||0,...coachHistory,monthLessons:0,monthHours:0,estimatedPay:0,averageScore:0,lastLessonAt:'',availabilityConfigured:false,updatedAt:appointment.updatedAt||'',source:'legacy-live',sourceLabel:'实时业务系统',readOnly:true});}
|
|
|
+ const paging=pageInput(input),status=text(paging.filters.status||'all'),filtered=searchRows(rows.filter((row)=>status==='all'||row.status===status),input).sort((a,b)=>a.displayName.localeCompare(b.displayName,'zh-CN')),page=pageRows(filtered,input),companyId=companyIdOf(context,input),storeRows=await Psql.query('SELECT "objectId","username","realName","nickname","legacyUserId","legacyUserData" FROM "_User" WHERE ($1::text IS NULL OR "company"=$1) AND COALESCE(CAST("legacyGroupId" AS text),"legacyUserData"->>\'GroupID\')=\'2\' AND ("isDeleted" IS NULL OR "isDeleted"=FALSE) ORDER BY COALESCE(NULLIF("realName",\'\'),NULLIF("nickname",\'\'),"username")',[companyId]);
|
|
|
+ return{...page,canManage:roleOf(context)==='ops-manager',stores:storeRows.map((row)=>({objectId:text(row.objectId),userId:number(row.legacyUserId||legacyOf(row).UserID),displayName:displayName(row)})).filter((row)=>row.userId),liveSourceAvailable:snapshot.live.available,liveSourceMessage:snapshot.live.message,liveSourceCount:snapshot.live.allItems.length,dataDateFrom:history.dataDateFrom,dataDateTo:history.dataDateTo};
|
|
|
+}
|
|
|
async function professionalCoachDetail(context,input,objectId) { const detail=await coachDetailData(context,input,objectId),all=await allAppointmentRows(context,input),counts=await appointmentCountsForBusinessDate(context,input),history=appointmentHistory(all),coachHistory=history.byCoach.get(detail.coach.userId)||{totalAppointments:0,lastAppointmentAt:''};return{...detail,coach:{...detail.coach,todayAppointments:counts.byCoach.get(detail.coach.userId)||0,...coachHistory},appointments:all.filter((row)=>row.coachId===detail.coach.userId).slice(0,500)}; }
|
|
|
async function professionalScheduleDataLocal(context,input) {
|
|
|
const data=await professionalScheduleData(context,input),stored=await allAppointmentRows(context,input),live=await liveAppointmentRows(input),coachId=number((input.filters||{}).coachId),memberMap=new Map(data.members.map((row)=>[row.userId,row])),baseCoachMap=new Map(data.coaches.map((row)=>[row.userId,row]));
|
|
|
const hydratedLive=live.allItems.map((row)=>{const member=memberMap.get(row.studentId),coach=baseCoachMap.get(row.coachId);return{...row,studentObjectId:member?member.objectId:'',studentName:member?member.displayName:row.studentName,coachObjectId:coach?coach.objectId:'',coachName:coach?coach.displayName:row.coachName};});
|
|
|
- const merged=new Map();for(const row of stored)merged.set(row.generalId?'general:'+row.generalId:'target:'+row.targetId,row);for(const row of hydratedLive)merged.set(row.generalId?'general:'+row.generalId:'target:'+row.targetId,row);const all=Array.from(merged.values()),history=appointmentHistory(all),appointments=all.filter((row)=>{const date=row.startsAt?businessDate(row.startsAt):'';return date>=data.dateFrom&&date<=data.dateTo&&(!coachId||row.coachId===coachId);}).sort((a,b)=>a.startsAt.localeCompare(b.startsAt)),rangeCounts=new Map();for(const row of appointments)if(row.coachId)rangeCounts.set(row.coachId,(rangeCounts.get(row.coachId)||0)+1);
|
|
|
+ const all=mergeAppointments(stored,hydratedLive),history=appointmentHistory(all),appointments=all.filter((row)=>{const date=row.startsAt?businessDate(row.startsAt):'';return date>=data.dateFrom&&date<=data.dateTo&&(!coachId||row.coachId===coachId);}).sort((a,b)=>a.startsAt.localeCompare(b.startsAt)),rangeCounts=new Map();for(const row of appointments)if(row.coachId)rangeCounts.set(row.coachId,(rangeCounts.get(row.coachId)||0)+1);
|
|
|
const coaches=data.coaches.map((row)=>({...row,todayAppointments:rangeCounts.get(row.userId)||0,...(history.byCoach.get(row.userId)||{totalAppointments:0,lastAppointmentAt:''})})),knownCoachIds=new Set(coaches.map((row)=>row.userId));
|
|
|
for(const row of hydratedLive){if(!row.coachId||knownCoachIds.has(row.coachId))continue;knownCoachIds.add(row.coachId);const coachHistory=history.byCoach.get(row.coachId)||{totalAppointments:0,lastAppointmentAt:''};coaches.push({objectId:'legacy-live-coach:'+row.coachId,userId:row.coachId,displayName:row.coachName,mobile:'',mobileMasked:'',storeName:'实时业务系统',status:'active',statusLabel:'实时账号',todayAppointments:rangeCounts.get(row.coachId)||0,pendingAppointments:0,...coachHistory,monthLessons:0,monthHours:0,estimatedPay:0,averageScore:0,lastLessonAt:'',availabilityConfigured:false,updatedAt:''});
|
|
|
}
|
|
|
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(),counts=await appointmentCountsForBusinessDate(context,input),metrics=data.metrics.map((row)=>row.key==='today'?{...row,value:counts.active.length}:row),alerts=data.alerts.map((row)=>row.key==='unassigned'?{...row,value:counts.active.filter((item)=>!item.coachId).length}:row);return{...data,date,metrics,alerts}; }
|
|
|
+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}; }
|
|
|
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:{};
|