teacher-earnings.js 4.4 KB

123456789101112131415161718192021222324252627282930313233
  1. // Read-only teacher view of the SAME canonical sources/calculator as operations payroll.
  2. async function teacherLessonEarnings(companyId,coachId,appointmentIds=null) {
  3. const calc=teacherEarningsCalculator();
  4. const [sources,storedRates,storedRules,mappings]=await Promise.all([
  5. Psql.query('SELECT s FROM xs_payroll_sources($1) s WHERE COALESCE(s->>\'coachId\',s->>\'jsmz\')=$2 AND ($3::text[] IS NULL OR COALESCE(s->>\'appointmentGeneralId\',s->>\'yyds\')=ANY($3))',[companyId,String(coachId),appointmentIds]),
  6. Psql.query('SELECT * FROM "CoachPayRate" WHERE company=$1 ORDER BY "createdAt" DESC,"objectId" DESC',[companyId]),
  7. Psql.query('SELECT * FROM "TeachingCompensationRule" WHERE company=$1 ORDER BY revision DESC,"createdAt" DESC,"objectId"',[companyId]),
  8. Psql.query('WITH RECURSIVE ancestors AS (SELECT n."nodeId" AS course_id,n."nodeId",n."parentId",n."nodeName",0 AS depth FROM "Node" n WHERE company=$1 UNION ALL SELECT a.course_id,n."nodeId",n."parentId",n."nodeName",a.depth+1 FROM ancestors a JOIN "Node" n ON n.company=$1 AND n."nodeId"=a."parentId" WHERE a.depth<12) SELECT p.*,n."nodeName" AS "courseName",array_agg(a."nodeName" ORDER BY a.depth) AS ancestor_names FROM "CourseTeachingProfile" p LEFT JOIN "Node" n ON n.company=p.company AND n."nodeId"=p."courseId" LEFT JOIN ancestors a ON a.course_id=p."courseId" WHERE p.company=$1 GROUP BY p."objectId",n."nodeName"',[companyId])
  9. ]);
  10. const rateKeys=new Set(),rates=[];
  11. for(const row of storedRates){const key=row.classType+':'+row.effectiveFrom;if(rateKeys.has(key))continue;rateKeys.add(key);rates.push({...row,classType:Number(row.classType),amount:Number(row.amount),durationHours:Number(row.durationHours),active:row.active!==false,isDefault:false});}
  12. rates.push(...calc.defaultRates().filter(r=>!rateKeys.has(r.classType+':'+r.effectiveFrom)));
  13. const ruleKeys=new Set(),rules=[];
  14. for(const row of storedRules){const key=[row.categoryKey,row.durationMinutes,row.deliveryMode,row.effectiveFrom].join(':');const superseded=ruleKeys.has(key);ruleKeys.add(key);rules.push({...calc.rule(row),active:row.active!==false&&!superseded});}
  15. rules.push(...calc.defaultRules().filter(r=>!ruleKeys.has([r.categoryKey,r.durationMinutes,r.deliveryMode,r.effectiveFrom].join(':'))));
  16. const mappingMap=new Map(mappings.filter(r=>calc.category(r)).map(r=>[Number(r.courseId),r]));
  17. const unique=new Map();
  18. for(const {s:raw} of sources){const row={...calc.project(raw,'cache'),standardLessonKey:raw.standardLessonKey,commonStatus:Number(raw.commonStatus??99),teacherPaySnapshot:raw.teacherPaySnapshot==null?null:Number(raw.teacherPaySnapshot)};if(row.commonStatus===-2)continue;if(unique.has(row.standardLessonKey))unique.get(row.standardLessonKey).duplicate=true;else unique.set(row.standardLessonKey,row);}
  19. const lines=[...unique.values()].map(r=>calc.calculate(r,rates,'',rules,mappingMap));
  20. return {lines,lessonIncome:lines.reduce((sum,l)=>sum+Math.round(l.amount*100),0)/100,pendingPayCount:lines.filter(l=>l.exception).length};
  21. }
  22. async function teacherIncomeSummary(companyId,coachId) {
  23. const [lessons,reviewIncome]=await Promise.all([teacherLessonEarnings(companyId,coachId),reviewIncomeTotal(companyId,coachId)]);
  24. return {lessonIncome:lessons.lessonIncome,reviewIncome,totalIncome:(Math.round(lessons.lessonIncome*100)+Math.round(reviewIncome*100))/100,pendingPayCount:lessons.pendingPayCount};
  25. }
  26. async function teacherSchedulePay(rows,coachId) {
  27. const ids=rows.filter(r=>Number(r.dszt)>=30&&Number(r.commonStatus)!==-2).map(r=>String(r.generalId));
  28. if(!ids.length)return rows;
  29. const [earnings,reports]=await Promise.all([teacherLessonEarnings(DEFAULT_COMPANY_ID,coachId,ids),Psql.query('SELECT "appointmentGeneralId"::text AS id,"feedbackStatus" FROM "LessonCompletionReport" WHERE company=$1 AND "coachId"=$2 AND "appointmentGeneralId"::text=ANY($3::text[])',[DEFAULT_COMPANY_ID,Number(coachId),ids])]);
  30. return rows.map(row=>{const matches=earnings.lines.filter(l=>String(l.appointmentGeneralId)===String(row.generalId)),line=matches[0],valid=matches.length===1&&!line.exception;return {...row,teacherPay:valid?line.amount:null,payStatus:valid?'earned':'pending',payReason:line?.exception||'工资待核对',feedbackStatus:reports.find(r=>r.id===String(row.generalId))?.feedbackStatus||'pending'};});
  31. }
  32. async function teacherIncomeLegacySummary(companyId,coachId){const result=await teacherIncomeSummary(companyId,coachId);return {...result,t_yongji:String(result.totalIncome)};}