|
@@ -113,8 +113,40 @@ async function historicalReservationPreview(context,input){const co=payrollTenan
|
|
|
async function commitHistoricalReservations(context,input,payload){requireRole(context,['ops-manager']);const co=payrollTenant(context,input),selected=new Set((payload.appointmentIds||[]).map(text));if(!selected.size)fail(400,'请选择预约');const before=await Psql.one('SELECT xs_payroll_source_version($1) AS version',[co]),preview=await historicalReservationPreview(context,input),items=preview.items.filter(i=>selected.has(i.appointmentId));if(!items.length)return{reserved:0,held:0};return(await payrollSql('SELECT xs_payroll_reservations($1,$2,$3::jsonb,$4,$5) AS result',[co,context.current.id,JSON.stringify(items),reasonOf(input),before.version])).result;}
|
|
async function commitHistoricalReservations(context,input,payload){requireRole(context,['ops-manager']);const co=payrollTenant(context,input),selected=new Set((payload.appointmentIds||[]).map(text));if(!selected.size)fail(400,'请选择预约');const before=await Psql.one('SELECT xs_payroll_source_version($1) AS version',[co]),preview=await historicalReservationPreview(context,input),items=preview.items.filter(i=>selected.has(i.appointmentId));if(!items.length)return{reserved:0,held:0};return(await payrollSql('SELECT xs_payroll_reservations($1,$2,$3::jsonb,$4,$5) AS result',[co,context.current.id,JSON.stringify(items),reasonOf(input),before.version])).result;}
|
|
|
async function creditReconciliationPreview(context,input){const rows=await Psql.query('SELECT s AS row FROM xs_payroll_refund_rows($1) s',[payrollTenant(context,input)]),items=rows.map(r=>r.row);return{items,summary:{completedReviewCount:items.length,differenceCount:items.filter(i=>i.adjustment>0).length,refundTotal:items.reduce((s,i)=>s+Number(i.adjustment),0)},refreshedAt:new Date().toISOString()};}
|
|
async function creditReconciliationPreview(context,input){const rows=await Psql.query('SELECT s AS row FROM xs_payroll_refund_rows($1) s',[payrollTenant(context,input)]),items=rows.map(r=>r.row);return{items,summary:{completedReviewCount:items.length,differenceCount:items.filter(i=>i.adjustment>0).length,refundTotal:items.reduce((s,i)=>s+Number(i.adjustment),0)},refreshedAt:new Date().toISOString()};}
|
|
|
async function commitCreditReconciliation(context,input,payload){requireRole(context,['ops-manager']);if(!payload.appointmentIds?.length)fail(400,'请选择要核对的预约');return(await payrollSql('SELECT xs_payroll_refunds($1,$2,$3::jsonb,$4) AS result',[payrollTenant(context,input),context.current.id,JSON.stringify(payload.appointmentIds.map(text)),reasonOf(input)])).result;}
|
|
async function commitCreditReconciliation(context,input,payload){requireRole(context,['ops-manager']);if(!payload.appointmentIds?.length)fail(400,'请选择要核对的预约');return(await payrollSql('SELECT xs_payroll_refunds($1,$2,$3::jsonb,$4) AS result',[payrollTenant(context,input),context.current.id,JSON.stringify(payload.appointmentIds.map(text)),reasonOf(input)])).result;}
|
|
|
-async function writingMappingPage(context,input){const co=payrollTenant(context,input),month=text(input.month);if(month&&!/^\d{4}-(0[1-9]|1[0-2])$/.test(month))fail(400,'工资月份无效');const rows=await Psql.query(`SELECT c."objectId" AS "appointmentId",c."generalId" AS "appointmentGeneralId",c."title" AS "studentName",c."subtitle" AS "courseName",a."szyh" AS "studentId",a."yysj" AS "startsAt",a."updatedAt",a."courseCategoryKey" AS "categoryKey",a."categoryEvidence" AS evidence,COALESCE(to_jsonb(u)->>'grade',u."legacyUserData"->>'Position',u."legacyUserData"->'UserBase'->>'Position','') AS grade,u."nickname",u."username" FROM "CommonModel" c JOIN "CourseAppointment" a ON a."company"=c."company" AND a."id"::text=c."itemId"::text LEFT JOIN "_User" u ON u."company"=c."company" AND u."legacyUserId"::text=a."szyh"::text WHERE c."company"=$1 AND c."modelId"=54 AND c."status"<>-2 AND c."subtitle" ~ '阅读|写作|语法|作文|强化' AND (a."dszt" IN ('0','10') OR ($2<>'' AND c."generalId"::text IN (SELECT source->>'yyds' FROM xs_payroll_sources($1) source WHERE to_char(xs_payroll_time(source->>'lessonAt') AT TIME ZONE 'Asia/Shanghai','YYYY-MM')=$2))) AND NOT (COALESCE(a."courseCategoryKey",'') IN ('primary_writing','middle_writing','high_writing') AND length(trim(COALESCE(a."categoryEvidence",'')))>=2) ORDER BY a."yysj",c."objectId"`,[co,month]);
|
|
|
|
|
- return{items:rows.map(r=>{const stage=teachingStageFromDatabase([r.grade,r.nickname,r.username]);return{...r,suggestedCategoryKey:stage?stage+'_writing':'',evidence:r.evidence||r.grade||'',categoryKey:r.categoryKey||'',studentName:r.studentName||r.nickname};})};}
|
|
|
|
|
|
|
+function writingSchoolYear(value){
|
|
|
|
|
+ const match=text(value).match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T]|$)/);if(!match)return null;
|
|
|
|
|
+ const year=Number(match[1]),month=Number(match[2]),day=Number(match[3]),date=new Date(Date.UTC(year,month-1,day));
|
|
|
|
|
+ if(date.getUTCFullYear()!==year||date.getUTCMonth()+1!==month||date.getUTCDate()!==day)return null;
|
|
|
|
|
+ return month>=9?year:year-1;
|
|
|
|
|
+}
|
|
|
|
|
+function writingStagesInText(value){
|
|
|
|
|
+ const raw=text(value),stages=[];if(!raw||/待确认|不确定|未知|可能|疑似|不详|暂定|[??]|待核/.test(raw))return stages;
|
|
|
|
|
+ if(/高中|高[一二三123]|(?:^|[^一二三四五六七八九十\d])(?:十[一二]?|1[012])年级/.test(raw))stages.push('high');
|
|
|
|
|
+ if(/初中|初[一二三123]|(?:^|[^一二三四五六七八九十\d])[七八九789]年级/.test(raw))stages.push('middle');
|
|
|
|
|
+ if(/小学|(?:^|[^一二三四五六七八九十\d])[一二三四五六123456]年级/.test(raw))stages.push('primary');
|
|
|
|
|
+ return stages;
|
|
|
|
|
+}
|
|
|
|
|
+function writingStageAssessment(row,confirmed,asOf=businessDate()){
|
|
|
|
|
+ const schoolYear=writingSchoolYear(row.startsAt),currentYear=writingSchoolYear(asOf),sources=[];
|
|
|
|
|
+ const labels={primary:'小学',middle:'初中',high:'高中'};
|
|
|
|
|
+ if(schoolYear==null)return{canAutoConfirm:false,suggestedCategoryKey:'',evidence:'',assessmentReason:'预约日期无效,需人工核对'};
|
|
|
|
|
+ for(const prior of confirmed||[]){if(text(prior.studentId)!==text(row.studentId)||writingSchoolYear(prior.startsAt)!==schoolYear||text(prior.evidence).length<2)continue;
|
|
|
|
|
+ const stage={primary_writing:'primary',middle_writing:'middle',high_writing:'high'}[prior.category];
|
|
|
|
|
+ if(stage)sources.push({stage,evidence:'本学年已确认预约 '+text(prior.appointmentGeneralId)+' 的'+labels[stage]+'分类'});
|
|
|
|
|
+ }
|
|
|
|
|
+ // Current profile labels cannot establish a student's stage in another school year.
|
|
|
|
|
+ if(schoolYear===currentYear){
|
|
|
|
|
+ const fields=[['档案年级',row.grade],['旧档案学段',row.position],['旧扩展档案学段',row.basePosition],['学员姓名',row.realName],['学员昵称',row.nickname],['学员账号',row.username],['旧学员昵称',row.oldName],['旧学员账号',row.oldUsername],['预约学员名称',row.studentName]];
|
|
|
|
|
+ for(const [label,value]of fields)for(const stage of writingStagesInText(value))sources.push({stage,evidence:label+'“'+text(value)+'”明确标注'+labels[stage]});
|
|
|
|
|
+ }
|
|
|
|
|
+ const stages=[...new Set(sources.map(s=>s.stage))];
|
|
|
|
|
+ if(stages.length!==1)return{canAutoConfirm:false,suggestedCategoryKey:'',evidence:'',assessmentReason:stages.length?'学段证据冲突:'+stages.map(s=>labels[s]).join('/'):schoolYear!==currentYear?'跨学年且无该学年确认记录,需人工核对':'档案和本学年记录均无明确学段,需人工核对'};
|
|
|
|
|
+ const stage=stages[0],evidence=[...new Set(sources.map(s=>s.evidence))].slice(0,3).join(';');
|
|
|
|
|
+ return{canAutoConfirm:true,suggestedCategoryKey:stage+'_writing',evidence,assessmentReason:'系统证据一致:'+labels[stage]};
|
|
|
|
|
+}
|
|
|
|
|
+async function writingMappingPage(context,input){const co=payrollTenant(context,input),month=text(input.month);if(month&&!/^\d{4}-(0[1-9]|1[0-2])$/.test(month))fail(400,'工资月份无效');const rows=await Psql.query(`SELECT c."objectId" AS "appointmentId",c."generalId" AS "appointmentGeneralId",c."title" AS "studentName",c."subtitle" AS "courseName",a."szyh" AS "studentId",a."yysj" AS "startsAt",a."updatedAt",a."courseCategoryKey" AS "categoryKey",a."categoryEvidence" AS evidence,to_jsonb(u)->>'grade' AS grade,u."legacyUserData"->>'Position' AS position,u."legacyUserData"->'UserBase'->>'Position' AS "basePosition",u."legacyUserData"->>'HoneyName' AS "oldName",u."legacyUserData"->>'UserName' AS "oldUsername",u."realName",u."nickname",u."username" FROM "CommonModel" c JOIN "CourseAppointment" a ON a."company"=c."company" AND a."id"::text=c."itemId"::text LEFT JOIN "_User" u ON u."company"=c."company" AND u."legacyUserId"::text=a."szyh"::text WHERE c."company"=$1 AND c."modelId"=54 AND c."status"<>-2 AND c."subtitle" ~ '阅读|写作|语法|作文|强化' AND (a."dszt" IN ('0','10') OR ($2<>'' AND c."generalId"::text IN (SELECT source->>'yyds' FROM xs_payroll_sources($1) source WHERE to_char(xs_payroll_time(source->>'lessonAt') AT TIME ZONE 'Asia/Shanghai','YYYY-MM')=$2))) AND NOT (COALESCE(a."courseCategoryKey",'') IN ('primary_writing','middle_writing','high_writing') AND length(trim(COALESCE(a."categoryEvidence",'')))>=2) ORDER BY a."yysj",c."objectId"`,[co,month]);
|
|
|
|
|
+ const ids=[...new Set(rows.map(r=>text(r.studentId)))],confirmed=ids.length?await Psql.query(`SELECT c."generalId" AS "appointmentGeneralId",a."szyh" AS "studentId",a."yysj" AS "startsAt",a."courseCategoryKey" AS category,a."categoryEvidence" AS evidence FROM "CommonModel" c JOIN "CourseAppointment" a ON a."company"=c."company" AND a."id"::text=c."itemId"::text WHERE c."company"=$1 AND c."modelId"=54 AND c."status"<>-2 AND a."szyh"::text=ANY($2::text[]) AND a."courseCategoryKey" IN ('primary_writing','middle_writing','high_writing') AND length(trim(COALESCE(a."categoryEvidence",'')))>=2`,[co,ids]):[];
|
|
|
|
|
+ return{items:rows.map(r=>{const decision=writingStageAssessment(r,confirmed);return{...r,...decision,grade:r.grade||r.position||r.basePosition||'',evidence:r.evidence||decision.evidence,categoryKey:r.categoryKey||decision.suggestedCategoryKey,studentName:r.studentName||r.nickname};})};}
|
|
|
async function writingMappingCommit(context,input,payload){requireRole(context,['ops-manager']);return(await payrollSql('SELECT xs_payroll_writing_mapping($1,$2,$3,$4,$5,$6::timestamptz,$7) AS result',[payrollTenant(context,input),context.current.id,input.targetId,text(payload.categoryKey),text(payload.evidence),dateIso(input.expectedUpdatedAt)||null,reasonOf(input)])).result;}
|
|
async function writingMappingCommit(context,input,payload){requireRole(context,['ops-manager']);return(await payrollSql('SELECT xs_payroll_writing_mapping($1,$2,$3,$4,$5,$6::timestamptz,$7) AS result',[payrollTenant(context,input),context.current.id,input.targetId,text(payload.categoryKey),text(payload.evidence),dateIso(input.expectedUpdatedAt)||null,reasonOf(input)])).result;}
|
|
|
async function payRateRows(context,input){
|
|
async function payRateRows(context,input){
|
|
|
const rows=await Psql.query('SELECT * FROM "CoachPayRate" WHERE "company"=$1 ORDER BY "createdAt" DESC,"objectId" DESC',[payrollTenant(context,input)]),seen=new Set(),current=[];
|
|
const rows=await Psql.query('SELECT * FROM "CoachPayRate" WHERE "company"=$1 ORDER BY "createdAt" DESC,"objectId" DESC',[payrollTenant(context,input)]),seen=new Set(),current=[];
|