|
@@ -3,48 +3,61 @@ import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output, compu
|
|
|
import { FormsModule } from '@angular/forms';
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { ScheduleAppointment } from '../admin.models';
|
|
import { ScheduleAppointment } from '../admin.models';
|
|
|
import { OperationsService } from '../operations.service';
|
|
import { OperationsService } from '../operations.service';
|
|
|
|
|
+import { OperationsDateFilterComponent } from '../components/operations-date-filter.component';
|
|
|
|
|
|
|
|
/** Cross-date search and explicit selection; each cancellation retains the normal audit and credit workflow. */
|
|
/** Cross-date search and explicit selection; each cancellation retains the normal audit and credit workflow. */
|
|
|
-@Component({selector:'app-schedule-bulk-manager',standalone:true,imports:[CommonModule,FormsModule],templateUrl:'./schedule-bulk-manager.component.html',styleUrls:['./schedule-bulk-manager.component.scss'],changeDetection:ChangeDetectionStrategy.OnPush})
|
|
|
|
|
|
|
+@Component({selector:'app-schedule-bulk-manager',standalone:true,imports:[CommonModule,FormsModule,OperationsDateFilterComponent],templateUrl:'./schedule-bulk-manager.component.html',styleUrls:['./schedule-bulk-manager.component.scss'],changeDetection:ChangeDetectionStrategy.OnPush})
|
|
|
export class ScheduleBulkManagerComponent implements OnInit {
|
|
export class ScheduleBulkManagerComponent implements OnInit {
|
|
|
@Output() closed=new EventEmitter<void>();
|
|
@Output() closed=new EventEmitter<void>();
|
|
|
@Output() changed=new EventEmitter<void>();
|
|
@Output() changed=new EventEmitter<void>();
|
|
|
private readonly operations=inject(OperationsService);
|
|
private readonly operations=inject(OperationsService);
|
|
|
readonly rows=signal<ScheduleAppointment[]>([]);readonly selected=signal(new Map<string,ScheduleAppointment>());
|
|
readonly rows=signal<ScheduleAppointment[]>([]);readonly selected=signal(new Map<string,ScheduleAppointment>());
|
|
|
readonly page=signal(1);readonly pageSize=signal(25);readonly total=signal(0);readonly query=signal('');
|
|
readonly page=signal(1);readonly pageSize=signal(25);readonly total=signal(0);readonly query=signal('');
|
|
|
|
|
+ readonly dateFrom=signal('');readonly dateTo=signal('');readonly resultsReady=signal(false);
|
|
|
readonly loading=signal(false);readonly selecting=signal(false);readonly saving=signal(false);readonly error=signal('');readonly message=signal('');
|
|
readonly loading=signal(false);readonly selecting=signal(false);readonly saving=signal(false);readonly error=signal('');readonly message=signal('');
|
|
|
readonly progress=signal({done:0,total:0});readonly pageCount=computed(()=>Math.max(1,Math.ceil(this.total()/this.pageSize())));
|
|
readonly progress=signal({done:0,total:0});readonly pageCount=computed(()=>Math.max(1,Math.ceil(this.total()/this.pageSize())));
|
|
|
readonly selectablePage=computed(()=>this.rows().filter(row=>this.canSelect(row)));
|
|
readonly selectablePage=computed(()=>this.rows().filter(row=>this.canSelect(row)));
|
|
|
readonly pageSelected=computed(()=>this.selectablePage().length>0&&this.selectablePage().every(row=>this.selected().has(row.targetId)));
|
|
readonly pageSelected=computed(()=>this.selectablePage().length>0&&this.selectablePage().every(row=>this.selected().has(row.targetId)));
|
|
|
- searchInput='';reason='批量调整排课';private request=0;
|
|
|
|
|
|
|
+ searchInput='';dateFromInput='';dateToInput='';reason='批量调整排课';private request=0;
|
|
|
ngOnInit():void{void this.loadPage();}
|
|
ngOnInit():void{void this.loadPage();}
|
|
|
canSelect(row:ScheduleAppointment):boolean{return Boolean(row.targetId&&row.updatedAt)&&!row.readOnly&&!row.statusUnverified&&!row.cancelled&&Number(row.statusCode)===0;}
|
|
canSelect(row:ScheduleAppointment):boolean{return Boolean(row.targetId&&row.updatedAt)&&!row.readOnly&&!row.statusUnverified&&!row.cancelled&&Number(row.statusCode)===0;}
|
|
|
- async searchAppointments():Promise<void>{if(this.loading()||this.selecting()||this.saving())return;this.query.set(this.searchInput.trim());this.page.set(1);this.selected.set(new Map());this.message.set('');await this.loadPage();}
|
|
|
|
|
|
|
+ hasPendingFilters():boolean{return this.searchInput.trim()!==this.query()||this.dateFromInput!==this.dateFrom()||this.dateToInput!==this.dateTo();}
|
|
|
|
|
+ private filters():Record<string,unknown>{return{dateFrom:this.dateFrom(),dateTo:this.dateTo()};}
|
|
|
|
|
+ async searchAppointments():Promise<void>{
|
|
|
|
|
+ if(this.loading()||this.selecting()||this.saving())return;
|
|
|
|
|
+ if(this.dateFromInput&&this.dateToInput&&this.dateFromInput>this.dateToInput){this.error.set('开始日期不能晚于结束日期');return;}
|
|
|
|
|
+ this.query.set(this.searchInput.trim());this.dateFrom.set(this.dateFromInput);this.dateTo.set(this.dateToInput);this.page.set(1);this.selected.set(new Map());this.message.set('');await this.loadPage();
|
|
|
|
|
+ }
|
|
|
|
|
+ async resetFilters():Promise<void>{if(this.loading()||this.selecting()||this.saving())return;this.searchInput='';this.dateFromInput='';this.dateToInput='';await this.searchAppointments();}
|
|
|
async loadPage():Promise<void>{
|
|
async loadPage():Promise<void>{
|
|
|
- const id=++this.request;this.loading.set(true);this.error.set('');
|
|
|
|
|
- try{const result=await this.operations.list('appointments',this.page(),this.pageSize(),this.query());if(id!==this.request)return;
|
|
|
|
|
|
|
+ const id=++this.request;this.loading.set(true);this.resultsReady.set(false);this.error.set('');
|
|
|
|
|
+ try{const result=await this.operations.list('appointments',this.page(),this.pageSize(),this.query(),this.filters());if(id!==this.request)return;
|
|
|
const last=Math.max(1,Math.ceil(result.total/this.pageSize()));if(this.page()>last){this.page.set(last);await this.loadPage();return;}
|
|
const last=Math.max(1,Math.ceil(result.total/this.pageSize()));if(this.page()>last){this.page.set(last);await this.loadPage();return;}
|
|
|
- this.rows.set(result.items as unknown as ScheduleAppointment[]);this.total.set(result.total);
|
|
|
|
|
|
|
+ this.rows.set(result.items as unknown as ScheduleAppointment[]);this.total.set(result.total);this.resultsReady.set(true);
|
|
|
}catch(e){if(id===this.request)this.error.set(e instanceof Error?e.message:'预约查询失败,请重试');}
|
|
}catch(e){if(id===this.request)this.error.set(e instanceof Error?e.message:'预约查询失败,请重试');}
|
|
|
finally{if(id===this.request)this.loading.set(false);}
|
|
finally{if(id===this.request)this.loading.set(false);}
|
|
|
}
|
|
}
|
|
|
async changePage(page:number):Promise<void>{if(this.loading()||this.selecting()||this.saving()||page<1||page>this.pageCount())return;this.page.set(page);await this.loadPage();}
|
|
async changePage(page:number):Promise<void>{if(this.loading()||this.selecting()||this.saving()||page<1||page>this.pageCount())return;this.page.set(page);await this.loadPage();}
|
|
|
async changePageSize(size:number):Promise<void>{if(this.loading()||this.selecting()||this.saving())return;this.pageSize.set(Number(size));this.page.set(1);await this.loadPage();}
|
|
async changePageSize(size:number):Promise<void>{if(this.loading()||this.selecting()||this.saving())return;this.pageSize.set(Number(size));this.page.set(1);await this.loadPage();}
|
|
|
- toggleRow(row:ScheduleAppointment):void{if(this.loading()||this.selecting()||this.saving()||!this.canSelect(row))return;this.message.set('');this.selected.update(current=>{const next=new Map(current);if(next.has(row.targetId))next.delete(row.targetId);else next.set(row.targetId,row);return next;});}
|
|
|
|
|
- togglePage():void{if(this.loading()||this.selecting()||this.saving())return;this.message.set('');const clear=this.pageSelected();this.selected.update(current=>{const next=new Map(current);for(const row of this.selectablePage())if(clear)next.delete(row.targetId);else next.set(row.targetId,row);return next;});}
|
|
|
|
|
|
|
+ toggleRow(row:ScheduleAppointment):void{if(this.loading()||this.selecting()||this.saving()||this.hasPendingFilters()||!this.resultsReady()||!this.canSelect(row))return;this.message.set('');this.selected.update(current=>{const next=new Map(current);if(next.has(row.targetId))next.delete(row.targetId);else next.set(row.targetId,row);return next;});}
|
|
|
|
|
+ togglePage():void{if(this.loading()||this.selecting()||this.saving()||this.hasPendingFilters()||!this.resultsReady())return;this.message.set('');const clear=this.pageSelected();this.selected.update(current=>{const next=new Map(current);for(const row of this.selectablePage())if(clear)next.delete(row.targetId);else next.set(row.targetId,row);return next;});}
|
|
|
async selectAllMatching():Promise<void>{
|
|
async selectAllMatching():Promise<void>{
|
|
|
if(this.loading()||this.selecting()||this.saving())return;
|
|
if(this.loading()||this.selecting()||this.saving())return;
|
|
|
- if(!this.query()){this.error.set('请先按学员或老师姓名搜索,再全选全部结果');return;}
|
|
|
|
|
|
|
+ if(this.hasPendingFilters()){this.error.set('筛选条件已修改,请先点击查询');return;}
|
|
|
|
|
+ if(!this.resultsReady()){this.error.set('请先完成查询后再操作预约');return;}
|
|
|
|
|
+ if(!this.query()&&!this.dateFrom()&&!this.dateTo()){this.error.set('请先按姓名或预约日期查询,再全选全部结果');return;}
|
|
|
this.selecting.set(true);this.error.set('');this.message.set('');
|
|
this.selecting.set(true);this.error.set('');this.message.set('');
|
|
|
- try{const found=new Map<string,ScheduleAppointment>();const first=await this.operations.list('appointments',1,100,this.query()),total=first.total;
|
|
|
|
|
|
|
+ try{const found=new Map<string,ScheduleAppointment>(),filters=this.filters();const first=await this.operations.list('appointments',1,100,this.query(),filters),total=first.total;
|
|
|
const add=(items:Record<string,unknown>[])=>{for(const raw of items){const row=raw as ScheduleAppointment;if(row.targetId)found.set(row.targetId,row);}};
|
|
const add=(items:Record<string,unknown>[])=>{for(const raw of items){const row=raw as ScheduleAppointment;if(row.targetId)found.set(row.targetId,row);}};
|
|
|
- add(first.items);for(let page=2;page<=Math.ceil(total/100);page++){const result=await this.operations.list('appointments',page,100,this.query());if(result.total!==total)throw Error('查询结果已变化,请重新搜索后再全选');add(result.items);}
|
|
|
|
|
|
|
+ add(first.items);for(let page=2;page<=Math.ceil(total/100);page++){const result=await this.operations.list('appointments',page,100,this.query(),filters);if(result.total!==total)throw Error('查询结果已变化,请重新搜索后再全选');add(result.items);}
|
|
|
if(found.size!==total)throw Error('查询结果已变化,请重新搜索后再全选');
|
|
if(found.size!==total)throw Error('查询结果已变化,请重新搜索后再全选');
|
|
|
this.selected.set(new Map([...found].filter(([,row])=>this.canSelect(row))));this.message.set(`已选择 ${this.selected().size} 条未开始的预约`);
|
|
this.selected.set(new Map([...found].filter(([,row])=>this.canSelect(row))));this.message.set(`已选择 ${this.selected().size} 条未开始的预约`);
|
|
|
}catch(e){this.error.set(e instanceof Error?e.message:'全选失败,请重新搜索');}finally{this.selecting.set(false);}
|
|
}catch(e){this.error.set(e instanceof Error?e.message:'全选失败,请重新搜索');}finally{this.selecting.set(false);}
|
|
|
}
|
|
}
|
|
|
clearSelection():void{if(!this.saving()&&!this.selecting()){this.selected.set(new Map());this.message.set('');}}
|
|
clearSelection():void{if(!this.saving()&&!this.selecting()){this.selected.set(new Map());this.message.set('');}}
|
|
|
async cancelSelected():Promise<void>{
|
|
async cancelSelected():Promise<void>{
|
|
|
|
|
+ if(this.hasPendingFilters()){this.error.set('筛选条件已修改,请先点击查询');return;}
|
|
|
|
|
+ if(!this.resultsReady()){this.error.set('请先完成查询后再操作预约');return;}
|
|
|
if(this.loading()||this.selecting()||this.saving())return;const items=[...this.selected().values()].filter(row=>this.canSelect(row));const reason=this.reason.trim();
|
|
if(this.loading()||this.selecting()||this.saving())return;const items=[...this.selected().values()].filter(row=>this.canSelect(row));const reason=this.reason.trim();
|
|
|
if(!items.length){this.error.set('请先勾选未开始的预约');return;}if(!reason){this.error.set('请填写取消原因');return;}
|
|
if(!items.length){this.error.set('请先勾选未开始的预约');return;}if(!reason){this.error.set('请填写取消原因');return;}
|
|
|
if(!window.confirm(`确认取消所选的 ${items.length} 条未开始预约?此操作会释放对应课时预留。`))return;
|
|
if(!window.confirm(`确认取消所选的 ${items.length} 条未开始预约?此操作会释放对应课时预留。`))return;
|