|
|
@@ -2740,7 +2740,7 @@ async function findAddon(className, company, id) { const fields=await fieldsOf(c
|
|
|
function durationMinutes(type) { return number(type)===2||number(type)===3?60:30; }
|
|
|
function durationHours(type) { return durationMinutes(type)/60; }
|
|
|
function appointmentDate(value,date,time) { const raw=text(value); if(raw) { const normalized=raw.includes('T')?raw:raw.replace(' ','T')+(raw.includes('+')||raw.endsWith('Z')?'':'+08:00'); const parsed=new Date(normalized); if(!Number.isNaN(parsed.getTime())) return parsed; } const fallback=new Date(text(date)+'T'+(text(time)||'00:00')+':00+08:00'); return fallback; }
|
|
|
-function appointmentProjection(row) { const start=appointmentDate(row.yysj,row.bxrq,row.sdsd),type=number(row.dslx),end=new Date(start.getTime()+durationMinutes(type)*60000),student=legacyOf({legacyUserData:row.studentLegacy}),coach=legacyOf({legacyUserData:row.coachLegacy}),cancelled=number(row.commonStatus)===-2; return { targetId:text(row.targetId),generalId:number(row.generalId),studentObjectId:text(row.studentObjectId),studentId:number(row.szyh),studentName:text(row.studentRealName||row.studentNickname||student.HoneyName||row.studentUsername)||'用户 '+number(row.szyh),coachObjectId:text(row.coachObjectId),coachId:number(row.pl||row.fxpl),coachName:text(row.coachRealName||row.coachNickname||coach.HoneyName||row.coachUsername)||'未分配',courseId:number(row.kcid),courseName:text(row.courseTitle)||'课程 '+number(row.kcid),classType:type,classTypeLabel:classType(type),deliveryMethod:text(row.jffs)||'未设置',startsAt:dateIso(start),endsAt:dateIso(end),statusCode:number(row.dszt),statusLabel:cancelled?'已取消':statusLabel(row.dszt),cancelled,cancellationReason:text(row.cancellationReason),availabilityWarning:row.availabilityConfigured===false,updatedAt:dateIso(row.commonUpdatedAt||row.updatedAt) }; }
|
|
|
+function appointmentProjection(row) { const start=appointmentDate(row.yysj,row.bxrq,row.sdsd),type=number(row.dslx),end=new Date(start.getTime()+durationMinutes(type)*60000),student=legacyOf({legacyUserData:row.studentLegacy}),coach=legacyOf({legacyUserData:row.coachLegacy}),cancelled=number(row.commonStatus)===-2,coachId=number(row.pl||row.fxpl),coachObjectId=text(row.coachObjectId),startsAt=Number.isNaN(start.getTime())?'':start.toISOString(),endsAt=Number.isNaN(end.getTime())?'':end.toISOString(); return { targetId:text(row.targetId),generalId:number(row.generalId),studentObjectId:text(row.studentObjectId),studentId:number(row.szyh),studentName:text(row.studentRealName||row.studentNickname||student.HoneyName||row.studentUsername)||'用户 '+number(row.szyh),coachObjectId,coachId,coachName:text(row.coachRealName||row.coachNickname||coach.HoneyName||row.coachUsername||row.plxm)||(coachId?'历史陪练 '+coachId:'未分配'),courseId:number(row.kcid),courseName:text(row.courseTitle)||'课程 '+number(row.kcid),classType:type,classTypeLabel:classType(type),deliveryMethod:text(row.jffs)||'未设置',startsAt,endsAt,statusCode:number(row.dszt),statusLabel:cancelled?'已取消':statusLabel(row.dszt),cancelled,cancellationReason:text(row.cancellationReason),coachAccountMissing:Boolean(coachId&&!coachObjectId),availabilityWarning:row.availabilityConfigured===false,updatedAt:dateIso(row.commonUpdatedAt||row.updatedAt) }; }
|
|
|
async function allAppointmentRows(context,input) { const companyId=companyIdOf(context,input); const raw=await Psql.query('SELECT c."objectId" AS "targetId",c."generalId",c."status" AS "commonStatus",c."updatedAt" AS "commonUpdatedAt",a.*,s."objectId" AS "studentObjectId",s."realName" AS "studentRealName",s."nickname" AS "studentNickname",s."username" AS "studentUsername",s."legacyUserData" AS "studentLegacy",p."objectId" AS "coachObjectId",p."realName" AS "coachRealName",p."nickname" AS "coachNickname",p."username" AS "coachUsername",p."legacyUserData" AS "coachLegacy",n."nodeName" AS "courseTitle" FROM "CommonModel" c JOIN "CourseAppointment" a ON a."company"=c."company" AND CAST(a."id" AS text)=CAST(c."itemId" AS text) LEFT JOIN "_User" s ON s."company"=c."company" AND COALESCE(CAST(s."legacyUserId" AS text),s."legacyUserData"->>\'UserID\')=CAST(a."szyh" AS text) LEFT JOIN "_User" p ON p."company"=c."company" AND COALESCE(CAST(p."legacyUserId" AS text),p."legacyUserData"->>\'UserID\')=COALESCE(NULLIF(CAST(a."pl" AS text),\'\'),CAST(a."fxpl" AS text)) LEFT JOIN "Node" n ON n."company"=c."company" AND CAST(n."nodeId" AS text)=CAST(a."kcid" AS text) WHERE ($1::text IS NULL OR c."company"=$1) AND CAST(c."modelId" AS text)=\'54\' ORDER BY c."updatedAt" DESC LIMIT 10000',[companyId]); return raw.map(appointmentProjection); }
|
|
|
async function newClassObjects(className,context,input,limit=1000) { try { const query=new Parse.Query(className); const company=companyPointer(context,input); if(company) query.equalTo('company',company); query.limit(limit); query.descending('createdAt'); return await query.find({useMasterKey:true}); } catch(error) { if(error.code===100||error.code===103||/class/i.test(text(error.message))) return []; throw error; } }
|
|
|
async function availabilityRows(context,input,coachId=0) { const rows=await newClassObjects('CoachAvailability',context,input,1000); return rows.filter((row)=>!coachId||number(row.get('coachId'))===coachId).map((row)=>({objectId:row.id,coachId:number(row.get('coachId')),ruleType:text(row.get('ruleType'))||'weekly',weekday:number(row.get('weekday')),date:text(row.get('date')),startTime:text(row.get('startTime')),endTime:text(row.get('endTime')),available:row.get('available')!==false,reason:text(row.get('reason')),updatedAt:dateIso(row.updatedAt)})); }
|
|
|
@@ -2827,9 +2827,10 @@ async function ensureCompletionLedger(context,input,pair) {
|
|
|
async function completeAppointmentSafe(context,input) { const pair=await appointmentPair(context,input,text(input.targetId)),result=await completeAppointment(context,input);await ensureCompletionLedger(context,input,pair);return result; }
|
|
|
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}; }
|
|
|
-async function professionalCoachRows(context,input) { const page=await coachRows(context,input),counts=await appointmentCountsForBusinessDate(context,input);return{...page,items:page.items.map((row)=>({...row,todayAppointments:counts.byCoach.get(row.userId)||0}))}; }
|
|
|
-async function professionalCoachDetail(context,input,objectId) { const detail=await coachDetailData(context,input,objectId),counts=await appointmentCountsForBusinessDate(context,input);return{...detail,coach:{...detail.coach,todayAppointments:counts.byCoach.get(detail.coach.userId)||0}}; }
|
|
|
-async function professionalScheduleDataLocal(context,input) { const data=await professionalScheduleData(context,input),counts=await appointmentCountsForBusinessDate(context,input);return{...data,coaches:data.coaches.map((row)=>({...row,todayAppointments:counts.byCoach.get(row.userId)||0}))}; }
|
|
|
+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);return{...page,items:page.items.map((row)=>({...row,todayAppointments:counts.byCoach.get(row.userId)||0,...(history.byCoach.get(row.userId)||{totalAppointments:0,lastAppointmentAt:''})}))}; }
|
|
|
+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),all=await allAppointmentRows(context,input),counts=await appointmentCountsForBusinessDate(context,input),history=appointmentHistory(all),coachId=number((input.filters||{}).coachId),appointments=all.filter((row)=>{const date=row.startsAt?businessDate(row.startsAt):'';return date>=data.dateFrom&&date<=data.dateTo&&(!coachId||row.coachId===coachId);}),unassigned=appointments.filter((row)=>!row.coachId||row.coachAccountMissing);return{...data,totalAppointments:history.totalAppointments,dataDateFrom:history.dataDateFrom,dataDateTo:history.dataDateTo,appointments,unassigned,coaches:data.coaches.map((row)=>({...row,todayAppointments:counts.byCoach.get(row.userId)||0,...(history.byCoach.get(row.userId)||{totalAppointments:0,lastAppointmentAt:''})}))}; }
|
|
|
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 handler(request,response) {
|
|
|
try {
|
|
|
@@ -2881,6 +2882,7 @@ async function handler(request,response) {
|
|
|
|
|
|
const definitions = [
|
|
|
{ name: 'xiaoshu.ops.gateway', desc: '小树陪练运营管理后台专用网关(业务投影、分级隐私、三级权限与审计)', path: 'xiaoshu/ops/gateway', code: operationsGatewayCode, params: [{ name: 'operation', type: 'String', required: true }] },
|
|
|
+ { name: 'xiaoshu.ops.gateway.v3', desc: '小树陪练运营管理后台专用网关 V3(含历史排课投影、上海业务日期与陪练排课汇总)', path: 'xiaoshu/ops/gateway-v3', code: operationsGatewayCode, params: [{ name: 'operation', type: 'String', required: true }] },
|
|
|
{ name: 'xiaoshu.admin.gateway', desc: '小树陪练 Angular 管理后台统一数据网关(权限、帐套、CRUD、统计)', path: 'xiaoshu/admin/gateway', code: adminGatewayCode, params: [{ name: 'operation', type: 'String', required: true }] },
|
|
|
{ name: 'cms.content-normalizer', desc: '替代 ZL_StripeHtmlTag/ZL_StripeTrimstr,保守输出纯文本', path: 'xiaoshu/cms/content-normalizer', code: cmsReadCode('content-normalizer'), params: [{ name: 'content', type: 'String', required: false }] },
|
|
|
{ name: 'cms.users.extended', desc: '替代 ZL_EX_UserView,返回安全用户扩展资料', path: 'xiaoshu/cms/users/extended', code: cmsReadCode('user-extended'), params: [{ name: 'userId', type: 'String', required: true }] },
|
|
|
@@ -2927,7 +2929,7 @@ async function upsert(definition) {
|
|
|
respType: 'json',
|
|
|
respJson: { success: true, data: {} },
|
|
|
enabled: true,
|
|
|
- version: '1.2.3',
|
|
|
+ version: '1.2.4',
|
|
|
};
|
|
|
const objectId = existing.results?.[0]?.objectId;
|
|
|
if (objectId) {
|