Преглед изворни кода

fix(payroll): persist course mapping confirmations

彭峰 пре 3 недеља
родитељ
комит
6a00b5ba9c

+ 1 - 1
projects/xiaoshu-admin/src/app/pages/operations-payroll.component.html

@@ -22,7 +22,7 @@
    @if(isManager()){<div class="panel standard-editor"><div class="form-grid"><label>课程分类<select [ngModel]="ruleCategoryKey()" (ngModelChange)="updateRuleCategory($event)">@for(category of activeCategories();track category.key){<option [value]="category.key">{{category.name}}</option>}</select></label><label>课程时长<select [ngModel]="ruleDuration()" (ngModelChange)="ruleDuration.set(+$event)">@for(duration of ruleDurations();track duration){<option [ngValue]="duration">{{duration}} 分钟</option>}</select></label><label>授课方式<select [ngModel]="ruleDelivery()" (ngModelChange)="ruleDelivery.set($event)"><option value="online">线上</option><option value="offline">线下</option></select></label><label>每节教师工资<input type="number" min="0" step="0.5" [ngModel]="rulePay()" (ngModelChange)="rulePay.set(+$event)" /></label><label>生效日期<input type="date" [ngModel]="effectiveFrom()" (ngModelChange)="effectiveFrom.set($event)" /></label><div class="credit-preview"><span>课时扣减</span><strong>@for(cost of creditCosts();track cost.account){ {{cost.account==='Purse'?'30分钟课时':cost.account==='SilverCoin'?'60分钟课时':cost.account==='UserExp'?'体验课时':'陪练时长'}} -{{cost.amount}} }</strong></div><button class="ui-button primary" type="button" (click)="saveTeachingRule()" [disabled]="saving()">保存新版本</button></div></div>}
    <div class="table-scroll"><table class="data-table standards-table rate-matrix"><thead><tr><th>课程分类</th><th>课程时长</th><th>线上单价</th><th>线下单价</th><th>课时扣减</th><th>当前版本</th><th>操作</th></tr></thead><tbody>@for(row of standardMatrix();track row.categoryKey+':'+row.durationMinutes){<tr><td><strong>{{row.categoryName}}</strong></td><td>{{row.durationMinutes}}分钟</td><td class="money">¥{{row.online?.teacherPay|number:'1.2-2'}}</td><td class="money offline-price">¥{{row.offline?.teacherPay|number:'1.2-2'}}</td><td>{{row.online?creditSummary(row.online):row.offline?creditSummary(row.offline):'—'}}</td><td>{{row.online?.effectiveFrom||row.offline?.effectiveFrom}}<small>{{row.online?.isDefault&&row.offline?.isDefault?'系统价目':'管理员版本'}}</small></td><td>@if(isManager()){<span class="matrix-actions"><button class="ui-button secondary compact" type="button" (click)="editMatrixRule(row,'online')">编辑线上</button><button class="ui-button secondary compact" type="button" (click)="editMatrixRule(row,'offline')">编辑线下</button></span>}@else{<span class="muted-text">—</span>}</td></tr>}</tbody></table></div></section>
   } @else if(standardsSection()==='mappings'){
-   <section class="content-card"><header><div><strong>历史课程映射</strong><span>课程大类由数据库的课程层级自动读取;管理员只需确认具体课程分类</span></div><label class="mapping-search"><span class="sr-only">搜索课程</span><input [ngModel]="mappingSearch()" (ngModelChange)="mappingSearch.set($event)" placeholder="搜索课程、学段、分类或 ID" /></label></header><div class="table-scroll"><table class="data-table mapping-table"><thead><tr><th>历史课程</th><th>系统建议</th><th>数据库大类</th><th>课程分类</th><th>状态</th><th>操作</th></tr></thead><tbody>@for(row of filteredMappings();track row.courseId){<tr><td><strong>{{row.courseName}}</strong><small>ID {{row.courseId}}</small></td><td>{{row.suggestedCategoryName}}</td><td><span class="stage-badge" [class.missing]="!row.stageConfirmed">{{row.stageName||'数据库未归类'}}</span></td><td><select [ngModel]="mappingSelections()[row.courseId]" (ngModelChange)="setMapping(row.courseId,$event)" [disabled]="!row.stageConfirmed">@if(mappingCategoryUnavailable(row)){<option [value]="row.categoryKey" disabled>{{row.categoryName||'旧分类'}}(不再使用)</option>}@for(category of mappingCategories(row);track category.key){<option [value]="category.key">{{category.name}}</option>}</select></td><td><span class="badge" [class.warn]="!row.confirmed">{{row.confirmed?'已确认':!row.stageConfirmed?'数据库大类缺失':'待确认'}}</span></td><td>@if(isManager()){<button class="ui-button secondary compact" type="button" (click)="confirmMapping(row)" [disabled]="saving()||!row.stageConfirmed">{{row.confirmed?'更新':'确认'}}</button>}</td></tr>}@empty{<tr><td colspan="6"><div class="empty">没有匹配的课程</div></td></tr>}</tbody></table></div></section>
+   <section class="content-card"><header><div><strong>历史课程映射</strong><span>课程大类由数据库的课程层级自动读取;管理员只需确认具体课程分类</span></div><label class="mapping-search"><span class="sr-only">搜索课程</span><input [ngModel]="mappingSearch()" (ngModelChange)="mappingSearch.set($event)" placeholder="搜索课程、学段、分类或 ID" /></label></header><div class="table-scroll"><table class="data-table mapping-table"><thead><tr><th>历史课程</th><th>系统建议</th><th>数据库大类</th><th>课程分类</th><th>状态</th><th>操作</th></tr></thead><tbody>@for(row of filteredMappings();track row.courseId){<tr><td><strong>{{row.courseName}}</strong><small>ID {{row.courseId}}</small></td><td>{{row.suggestedCategoryName}}</td><td><span class="stage-badge" [class.missing]="!row.stageConfirmed">{{row.stageName||'数据库未归类'}}</span></td><td><select [ngModel]="mappingSelections()[row.courseId]" (ngModelChange)="setMapping(row.courseId,$event)" [disabled]="!row.stageConfirmed">@if(mappingCategoryUnavailable(row)){<option [value]="row.categoryKey" disabled>{{row.categoryName||'旧分类'}}(不再使用)</option>}@for(category of mappingCategories(row);track category.key){<option [value]="category.key">{{category.name}}</option>}</select></td><td><span class="badge" [class.warn]="!row.confirmed">{{row.confirmed?'已确认':!row.stageConfirmed?'数据库大类缺失':'待确认'}}</span></td><td>@if(isManager()){<button class="ui-button secondary compact" type="button" (click)="confirmMapping(row)" [disabled]="saving()||!row.stageConfirmed">{{savingMappingId()===row.courseId?'确认中…':row.confirmed?'更新':'确认'}}</button>}</td></tr>}@empty{<tr><td colspan="6"><div class="empty">没有匹配的课程</div></td></tr>}</tbody></table></div></section>
   } @else if(standardsSection()==='categories') {
    <section class="content-card"><header><div><strong>标准课程分类</strong><span>分类名称与价目表完全一致,不再显示或使用旧分类</span></div></header><div class="stage-overview" aria-label="数据库课程大类">@for(stage of stageOptions;track stage.key){<span>{{stage.name}}·数据库大类</span>}</div><div class="category-grid">@for(category of activeCategories();track category.key){<article><span>{{category.key}}</span><strong>{{category.name}}</strong><small>固定分类</small></article>}</div></section>
   } @else if(standardsSection()==='reservations') {

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
projects/xiaoshu-admin/src/app/pages/operations-payroll.component.ts


+ 2 - 2
scripts/deploy-admin-functions.mjs

@@ -2948,7 +2948,7 @@ function maskMobile(value) { const mobile = text(value); if (mobile.length < 7)
 function roleOf(context) { if (context.isSuperAdmin || context.roles.includes('ops-manager')) return 'ops-manager'; if (context.roles.includes('ops-staff')) return 'ops-staff'; if (context.roles.includes('ops-auditor')) return 'ops-auditor'; return 'ops-manager'; }
 function requireRole(context, allowed) { const role = roleOf(context); if (!allowed.includes(role)) fail(403, '当前运营角色无权执行该操作'); return role; }
 function companyIdOf(context, input) { if (context.isSuperAdmin && input.companyId) return String(input.companyId); return pointerId(context.company) || null; }
-function companyPointer(context, input) { const id = companyIdOf(context, input); return id ? Parse.Object.createWithoutData('Company', id) : null; }
+function companyPointer(context, input) { const id = companyIdOf(context, input); if(!id)return null;const pointer=new Parse.Object('Company');pointer.id=id;return pointer; }
 function safe(value, depth = 0) { if (value == null || depth > 4) return value; if (Array.isArray(value)) return value.map((item) => safe(item, depth + 1)); if (value instanceof Date) return value.toISOString(); if (value && typeof value.toJSON === 'function') return safe(value.toJSON(), depth + 1); if (typeof value === 'object') { const out = {}; for (const [key,item] of Object.entries(value)) if (!/(?:password|secret|token|hash|masterkey|privatekey)/i.test(key)) out[key] = safe(item, depth + 1); return out; } return value; }
 async function auth(request) {
   const current = request.user || (typeof user !== 'undefined' ? user : null); if (!current) fail(401, '管理员会话已失效'); await current.fetch({ useMasterKey:true });
@@ -2958,7 +2958,7 @@ async function auth(request) {
   const company = request.company || current.get('company') || null; if (!company && !isSuperAdmin) fail(403, '管理员账号尚未分配帐套'); return { current, roles, isSuperAdmin, company };
 }
 async function fieldsOf(className) { return (await new Parse.Schema(className).get({ useMasterKey:true })).fields || {}; }
-function tenant(query, fields, context, input) { if (!fields.company) return; const companyId = companyIdOf(context, input); if (companyId) query.equalTo('company', Parse.Object.createWithoutData('Company', companyId)); }
+function tenant(query, fields, context, input) { if (!fields.company) return; const company=companyPointer(context,input);if(company)query.equalTo('company',company); }
 async function scopedObject(className, objectId, context, input) { if (!objectId) fail(400, '缺少目标记录'); const fields = await fieldsOf(className); const query = new Parse.Query(className); tenant(query, fields, context, input); return query.get(objectId, { useMasterKey:true }); }
 function assertVersion(target, expected) { if (!expected) return; const actual = target.updatedAt ? target.updatedAt.toISOString() : ''; const wanted = dateIso(expected); if (actual && wanted && actual !== wanted) fail(409, '记录已被其他管理员修改,请刷新后重试'); }
 function reasonOf(input) { const reason = text(input.reason); if (reason.length < 2 || reason.length > 500) fail(400, '操作原因必须为 2 至 500 个字符'); return reason; }

Неке датотеке нису приказане због велике количине промена