|
|
@@ -2,17 +2,21 @@ import {AdminSessionService} from '../admin-session.service';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output, computed, inject, signal } from '@angular/core';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
+import { DropdownDirective } from '../shared/dropdown.directive';
|
|
|
+import { OperationsDateFilterComponent } from './operations-date-filter.component';
|
|
|
+import { Download, RefreshCw, Search, RotateCcw, LucideAngularModule } from 'lucide-angular';
|
|
|
import { OperationsService } from '../operations.service';
|
|
|
import { CreditAccounts } from '../member-enrollment.models';
|
|
|
|
|
|
interface CreditLine {account:string;delta:number;before:number|null;after:number|null;availableBefore?:number;availableAfter?:number;kind?:string;operator?:string;reason?:string;reference?:string;sourceTransactionId?:string;source?:string;}
|
|
|
interface CreditPlan {version:string;storeId:number;lines:CreditLine[];}
|
|
|
interface Ledger {total:number;version:string;items:Array<{groupId:string;at:string;lines:CreditLine[]}>;}
|
|
|
-@Component({selector:'app-store-credit-account',standalone:true,imports:[CommonModule,FormsModule],templateUrl:'./store-credit-account.component.html',styleUrls:['./store-credit-account.component.scss']})
|
|
|
+@Component({selector:'app-store-credit-account',standalone:true,imports:[CommonModule,FormsModule,DropdownDirective,OperationsDateFilterComponent,LucideAngularModule],templateUrl:'./store-credit-account.component.html',styleUrls:['./store-credit-account.component.scss']})
|
|
|
export class StoreCreditAccountComponent implements OnChanges,OnDestroy {
|
|
|
readonly isStore=(()=>{const sessions=inject(AdminSessionService);return computed(()=>sessions.session()?.operationsRole==='store-admin');})();
|
|
|
@Input({required:true}) objectId='';@Input() compact=false;@Input() initialOpen=false;@Output() closed=new EventEmitter<void>();@Output() posted=new EventEmitter<string>();
|
|
|
private readonly operations=inject(OperationsService);private serial=0;private ledgerSerial=0;private destroyed=false;private key='';private fingerprint='';private previewFingerprint='';
|
|
|
+ readonly icons={Download,RefreshCw,Search,RotateCcw};
|
|
|
readonly accounts=[{key:'Purse',label:'30分钟课时',unit:'节'},{key:'SilverCoin',label:'60分钟课时',unit:'节'},{key:'UserExp',label:'体验次数',unit:'次'},{key:'UserPoint',label:'陪练时长',unit:'小时'}];
|
|
|
data=signal<CreditAccounts|null>(null);loading=signal(false);error=signal('');message=signal('');open=signal(false);busy=signal(false);preview=signal<CreditPlan|null>(null);ledger=signal<Ledger|null>(null);ledgerBusy=signal(false);ledgerError=signal('');exporting=signal(false);tab=signal<'accounts'|'ledger'>('accounts');
|
|
|
amounts:Record<string,number>={Purse:0,SilverCoin:0,UserExp:0,UserPoint:0};method='';reason='';direction='add';sourceId='';sourceAccount='';accountFilter='';kindFilter='';dateFrom='';dateTo='';page=1;
|
|
|
@@ -32,6 +36,8 @@ export class StoreCreditAccountComponent implements OnChanges,OnDestroy {
|
|
|
async submit(){const plan=this.preview();if(this.busy()||!plan||this.previewFingerprint!==this.currentFingerprint())return;const fingerprint=this.currentFingerprint();if(this.fingerprint!==fingerprint){this.key=crypto.randomUUID();this.fingerprint=fingerprint;}this.busy.set(true);this.error.set('');try{await this.operations.mutate('ops/stores/credit-post',this.objectId,this.reason.trim(),this.payload(),plan.version,this.key);this.preview.set(null);this.open.set(false);this.message.set(this.direction==='reverse'?'已纠正原入账,余额与流水已更新':'门店充值成功,四类额度已统一入账');this.key='';this.fingerprint='';await this.load();if(this.tab()==='ledger')await this.loadLedger();this.posted.emit(this.message());if(this.compact)this.closed.emit();}catch(e){this.error.set(e instanceof Error?e.message:'提交未确认,请保留当前内容重试');}finally{this.busy.set(false);}}
|
|
|
params(page=this.page){return {objectId:this.objectId,page,pageSize:20,account:this.accountFilter,kind:this.kindFilter,dateFrom:this.dateFrom,dateTo:this.dateTo};}
|
|
|
async showLedger(){this.tab.set('ledger');if(!this.ledger())await this.loadLedger();}
|
|
|
- async loadLedger(reset=false){if(reset)this.page=1;const serial=++this.ledgerSerial;this.ledgerBusy.set(true);this.ledgerError.set('');try{const result=await this.operations.request<Ledger>('ops/stores/credit-ledger',this.params());if(serial===this.ledgerSerial&&!this.destroyed)this.ledger.set(result);}catch(e){if(serial===this.ledgerSerial)this.ledgerError.set(e instanceof Error?e.message:'流水加载失败');}finally{if(serial===this.ledgerSerial)this.ledgerBusy.set(false);}}
|
|
|
- async export(){if(this.exporting())return;this.exporting.set(true);this.ledgerError.set('');const params={...this.params(1),pageSize:100};try{const first=await this.operations.request<Ledger>('ops/stores/credit-ledger',params),rows=[...first.items];for(let page=2;rows.length<first.total;page++){const next=await this.operations.request<Ledger>('ops/stores/credit-ledger',{...params,page});if(!next.items.length||next.version!==first.version||next.total!==first.total)throw Error('导出期间流水变化,请重试');rows.push(...next.items);}const latest=await this.operations.request<Ledger>('ops/stores/credit-ledger',params);if(latest.version!==first.version||new Set(rows.map(r=>r.groupId)).size!==rows.length)throw Error('导出期间流水变化,请重试');const xlsx=await import('xlsx'),book=xlsx.utils.book_new();xlsx.utils.book_append_sheet(book,xlsx.utils.json_to_sheet(rows.flatMap(r=>r.lines.map(l=>({'业务单号':r.groupId,'时间(北京时间)':new Date(r.at).toLocaleString('sv-SE',{timeZone:'Asia/Shanghai'}),'账户':this.label(l.account),'业务':this.kindLabel(l.kind),'变动':l.delta,'变动前':l.before??'未记录','变动后':l.after??'未记录','操作人':l.operator||'未记录','原因':l.reason||'','凭据':l.reference||'','关联原单':l.sourceTransactionId||'','来源':l.source==='legacy'?'历史流水':'新系统'})))),'门店额度流水');xlsx.writeFile(book,`门店${this.data()?.storeId||''}额度流水.xlsx`);}catch(e){this.ledgerError.set(e instanceof Error?e.message:'导出失败');}finally{this.exporting.set(false);}}
|
|
|
+ resetFilters(){this.accountFilter='';this.kindFilter='';this.dateFrom='';this.dateTo='';void this.loadLedger(true);}
|
|
|
+ private validDateRange(){if(this.dateFrom&&this.dateTo&&this.dateFrom>this.dateTo){this.ledgerError.set('开始日期不能晚于结束日期,请调整后查询');return false;}return true;}
|
|
|
+ async loadLedger(reset=false){if(!this.validDateRange())return;if(reset)this.page=1;const serial=++this.ledgerSerial;this.ledgerBusy.set(true);this.ledgerError.set('');try{const result=await this.operations.request<Ledger>('ops/stores/credit-ledger',this.params());if(serial===this.ledgerSerial&&!this.destroyed)this.ledger.set(result);}catch(e){if(serial===this.ledgerSerial)this.ledgerError.set(e instanceof Error?e.message:'流水加载失败');}finally{if(serial===this.ledgerSerial)this.ledgerBusy.set(false);}}
|
|
|
+ async export(){if(this.exporting()||!this.validDateRange())return;this.exporting.set(true);this.ledgerError.set('');const params={...this.params(1),pageSize:100};try{const first=await this.operations.request<Ledger>('ops/stores/credit-ledger',params),rows=[...first.items];for(let page=2;rows.length<first.total;page++){const next=await this.operations.request<Ledger>('ops/stores/credit-ledger',{...params,page});if(!next.items.length||next.version!==first.version||next.total!==first.total)throw Error('导出期间流水变化,请重试');rows.push(...next.items);}const latest=await this.operations.request<Ledger>('ops/stores/credit-ledger',params);if(latest.version!==first.version||new Set(rows.map(r=>r.groupId)).size!==rows.length)throw Error('导出期间流水变化,请重试');const xlsx=await import('xlsx'),book=xlsx.utils.book_new();xlsx.utils.book_append_sheet(book,xlsx.utils.json_to_sheet(rows.flatMap(r=>r.lines.map(l=>({'业务单号':r.groupId,'时间(北京时间)':new Date(r.at).toLocaleString('sv-SE',{timeZone:'Asia/Shanghai'}),'账户':this.label(l.account),'业务':this.kindLabel(l.kind),'变动':l.delta,'变动前':l.before??'未记录','变动后':l.after??'未记录','操作人':l.operator||'未记录','原因':l.reason||'','凭据':l.reference||'','关联原单':l.sourceTransactionId||'','来源':l.source==='legacy'?'历史流水':'新系统'})))),'门店额度流水');xlsx.writeFile(book,`门店${this.data()?.storeId||''}额度流水.xlsx`);}catch(e){this.ledgerError.set(e instanceof Error?e.message:'导出失败');}finally{this.exporting.set(false);}}
|
|
|
}
|