| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662 |
- import { Component, Input, computed, signal, OnInit } from '@angular/core';
- import { CommonModule, DatePipe } from '@angular/common';
- import { FormsModule } from '@angular/forms';
- import { MatButtonModule } from '@angular/material/button';
- import { MatIconModule } from '@angular/material/icon';
- import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
- import { MatTooltipModule } from '@angular/material/tooltip';
- import { MatDialogModule, MatDialog } from '@angular/material/dialog';
- import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
- import { Settlement } from '../../../models/project.model';
- import { AutoSettlementService } from '../../../services/auto-settlement.service';
- import { ProjectService } from '../../../services/project.service';
- export interface SettlementStats {
- totalAmount: number;
- pendingAmount: number;
- overdueAmount: number;
- completedAmount: number;
- totalCount: number;
- pendingCount: number;
- overdueCount: number;
- completedCount: number;
- }
- // 项目验收确认接口
- export interface ProjectAcceptance {
- id: string;
- projectId: string;
- projectName: string;
- technicianId: string;
- technicianName: string;
- acceptanceStatus: 'pending' | 'confirmed' | 'rejected';
- acceptanceDate?: Date;
- acceptanceNotes?: string;
- customerNotified: boolean;
- }
- // 客服跟进提醒接口
- export interface CustomerServiceReminder {
- id: string;
- projectId: string;
- projectName: string;
- customerName: string;
- reminderType: 'payment_follow_up' | 'acceptance_notification' | 'delivery_confirmation';
- reminderStatus: 'pending' | 'in_progress' | 'completed';
- assignedTo: string;
- dueDate: Date;
- notes?: string;
- createdAt: Date;
- }
- // 企业微信群发图接口
- export interface WeChatImageDelivery {
- id: string;
- projectId: string;
- groupId: string;
- groupName: string;
- imageUrls: string[];
- deliveryStatus: 'pending' | 'sending' | 'completed' | 'failed';
- deliveryDate?: Date;
- errorMessage?: string;
- }
- @Component({
- selector: 'app-settlement-card',
- standalone: true,
- imports: [
- CommonModule,
- DatePipe,
- FormsModule,
- MatButtonModule,
- MatIconModule,
- MatProgressSpinnerModule,
- MatTooltipModule,
- MatDialogModule,
- MatSnackBarModule
- ],
- templateUrl: './settlement-card.html',
- styleUrls: ['./settlement-card.scss']
- })
- export class SettlementCardComponent implements OnInit {
- @Input() settlements: Settlement[] = [];
- @Input() showAutomationControls = false;
- @Input() projectAcceptances: ProjectAcceptance[] = [];
- @Input() customerServiceReminders: CustomerServiceReminder[] = [];
-
- // 筛选条件
- statusFilter = signal<string>('all');
- searchKeyword = signal<string>('');
-
- // 自动化处理状态
- isProcessing = signal(false);
- processingSettlementId = signal<string | null>(null);
-
- // 新增功能状态
- acceptanceStatus = signal<{[key: string]: 'pending' | 'confirming' | 'confirmed'}>({});
- reminderStatus = signal<{[key: string]: 'pending' | 'creating' | 'created'}>({});
- isSendingImages = signal(false);
- sendingProjectId = signal<string | null>(null);
- constructor(
- private autoSettlementService: AutoSettlementService,
- private dialog: MatDialog,
- private snackBar: MatSnackBar,
- private projectService: ProjectService
- ) { }
- ngOnInit() {
- // 启动定时自动化处理
- this.autoSettlementService.startScheduledProcessing();
-
- // 初始化项目验收状态
- this.initializeAcceptanceStatus();
-
- // 初始化客服提醒状态
- this.initializeReminderStatus();
- }
- // 初始化项目验收状态
- private initializeAcceptanceStatus(): void {
- const status: {[key: string]: 'pending' | 'confirming' | 'confirmed'} = {};
- this.settlements.forEach(settlement => {
- const acceptance = this.projectAcceptances.find(a => a.projectId === settlement.projectId);
- if (acceptance) {
- status[settlement.projectId] = acceptance.acceptanceStatus === 'confirmed' ? 'confirmed' : 'pending';
- } else {
- status[settlement.projectId] = 'pending';
- }
- });
- this.acceptanceStatus.set(status);
- }
- // 初始化客服提醒状态
- private initializeReminderStatus(): void {
- const status: {[key: string]: 'pending' | 'creating' | 'created'} = {};
- this.settlements.forEach(settlement => {
- const reminder = this.customerServiceReminders.find(r =>
- r.projectId === settlement.projectId && r.reminderType === 'payment_follow_up'
- );
- if (reminder) {
- status[settlement.projectId] = reminder.reminderStatus === 'completed' ? 'created' : 'pending';
- } else {
- status[settlement.projectId] = 'pending';
- }
- });
- this.reminderStatus.set(status);
- }
- // 技术确认项目验收
- confirmProjectAcceptance(settlement: Settlement): void {
- const currentStatus = this.acceptanceStatus();
- currentStatus[settlement.projectId] = 'confirming';
- this.acceptanceStatus.set({...currentStatus});
- // 模拟API调用
- setTimeout(() => {
- const updatedStatus = this.acceptanceStatus();
- updatedStatus[settlement.projectId] = 'confirmed';
- this.acceptanceStatus.set({...updatedStatus});
-
- this.snackBar.open(`项目 ${settlement.projectName} 验收确认成功`, '关闭', {
- duration: 3000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- });
- // 自动创建客服跟进提醒
- this.createCustomerServiceReminder(settlement, 'acceptance_notification');
- // 验收完成后,自动发起尾款结算申请
- this.initiateFinalSettlement(settlement);
- }, 1500);
- }
- // 创建客服跟进提醒
- createCustomerServiceReminder(settlement: Settlement, type: 'payment_follow_up' | 'acceptance_notification' | 'delivery_confirmation'): void {
- const currentStatus = this.reminderStatus();
- currentStatus[settlement.projectId] = 'creating';
- this.reminderStatus.set({...currentStatus});
- // 模拟API调用创建提醒
- setTimeout(() => {
- const updatedStatus = this.reminderStatus();
- updatedStatus[settlement.projectId] = 'created';
- this.reminderStatus.set({...updatedStatus});
-
- let message = '';
- switch(type) {
- case 'payment_follow_up':
- message = `已创建尾款跟进提醒`;
- break;
- case 'acceptance_notification':
- message = `已创建验收通知提醒`;
- break;
- case 'delivery_confirmation':
- message = `已创建交付确认提醒`;
- break;
- }
-
- this.snackBar.open(`${settlement.projectName} ${message}`, '关闭', {
- duration: 3000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- });
- }, 1000);
- }
- // 一键发图到企业微信群
- sendImagesToWeChatGroup(settlement: Settlement): void {
- this.isSendingImages.set(true);
- this.sendingProjectId.set(settlement.projectId);
- // 模拟获取项目大图和发送到企业微信群
- setTimeout(() => {
- this.isSendingImages.set(false);
- this.sendingProjectId.set(null);
-
- this.snackBar.open(`项目 ${settlement.projectName} 大图已发送到企业微信群`, '关闭', {
- duration: 4000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- });
- // 自动创建交付确认提醒
- this.createCustomerServiceReminder(settlement, 'delivery_confirmation');
- }, 3000);
- }
- // 获取项目验收状态
- getAcceptanceStatus(projectId: string): 'pending' | 'confirming' | 'confirmed' {
- return this.acceptanceStatus()[projectId] || 'pending';
- }
- // 获取客服提醒状态
- getReminderStatus(projectId: string): 'pending' | 'creating' | 'created' {
- return this.reminderStatus()[projectId] || 'pending';
- }
- // 检查是否可以发送图片
- canSendImages(settlement: Settlement): boolean {
- return settlement.status === '已结算' && this.getAcceptanceStatus(settlement.projectId) === 'confirmed';
- }
- // 检查是否正在发送图片
- isSendingImagesForProject(projectId: string): boolean {
- return this.isSendingImages() && this.sendingProjectId() === projectId;
- }
- // 处理单个结算
- processSettlement(settlementId: string): void {
- const settlement = this.settlements.find(s => s.id === settlementId);
- if (!settlement) return;
-
- // 如果是逾期状态,显示逾期处理弹窗
- if (settlement.status === '逾期' || this.isOverdue(settlement)) {
- this.handleOverdueSettlement(settlement);
- } else {
- this.processSettlementAutomation(settlement);
- }
- }
-
- // 处理逾期结算
- async handleOverdueSettlement(settlement: Settlement): Promise<void> {
- const daysOverdue = this.getDaysOverdue(settlement);
- const overdueMessage = `
- 项目:${settlement.projectName}
- 阶段:${settlement.stage}
- 金额:¥${settlement.amount}
- 逾期天数:${daysOverdue}天
- 请选择处理方式:
- 1. 立即催款
- 2. 延期处理
- 3. 标记为已结算
- `.trim();
-
- if (await window?.fmode?.confirm(overdueMessage + '\n\n点击"确定"发送催款提醒,点击"取消"查看更多选项')) {
- // 发送催款提醒
- this.sendOverdueReminder(settlement);
- } else {
- // 显示更多处理选项
- await this.showOverdueOptions(settlement);
- }
- }
-
- // 发送逾期催款提醒
- private sendOverdueReminder(settlement: Settlement): void {
- this.snackBar.open('正在发送催款提醒...', '', {
- duration: 2000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- });
-
- setTimeout(() => {
- // 创建客服跟进提醒
- this.createCustomerServiceReminder(settlement, 'payment_follow_up');
-
- // 模拟发送多渠道催款通知
- const channels = ['短信', '微信', '电话'];
- const channelText = channels.join('、');
-
- this.snackBar.open(
- `✅ 催款提醒已通过${channelText}发送给客户\n项目:${settlement.projectName}\n金额:¥${settlement.amount}`,
- '关闭',
- {
- duration: 5000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- }
- );
-
- // 记录催款历史
- console.log('催款记录:', {
- settlementId: settlement.id,
- projectName: settlement.projectName,
- amount: settlement.amount,
- daysOverdue: this.getDaysOverdue(settlement),
- reminderDate: new Date(),
- channels: channels
- });
- }, 2000);
- }
-
- // 显示逾期处理选项
- private async showOverdueOptions(settlement: Settlement): Promise<void> {
- const option = await window?.fmode?.input(`
- 逾期处理选项:
- 1 - 延期7天
- 2 - 延期15天
- 3 - 延期30天
- 4 - 标记为已结算
- 5 - 取消结算
- 请输入选项编号(1-5):
- `.trim());
-
- switch(option) {
- case '1':
- this.extendSettlementDeadline(settlement, 7);
- break;
- case '2':
- this.extendSettlementDeadline(settlement, 15);
- break;
- case '3':
- this.extendSettlementDeadline(settlement, 30);
- break;
- case '4':
- this.markSettlementAsCompleted(settlement);
- break;
- case '5':
- this.cancelSettlement(settlement);
- break;
- default:
- if (option !== null) {
- this.snackBar.open('无效的选项', '关闭', {
- duration: 2000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- });
- }
- }
- }
-
- // 延长结算期限
- private extendSettlementDeadline(settlement: Settlement, days: number): void {
- const currentDueDate = settlement.dueDate || new Date();
- const newDeadline = new Date(currentDueDate);
- newDeadline.setDate(newDeadline.getDate() + days);
-
- settlement.dueDate = newDeadline;
- settlement.status = '待结算';
-
- this.snackBar.open(
- `✅ 已将结算期限延长${days}天\n新期限:${newDeadline.toLocaleDateString()}`,
- '关闭',
- {
- duration: 4000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- }
- );
-
- console.log('延期记录:', {
- settlementId: settlement.id,
- projectName: settlement.projectName,
- extendedDays: days,
- newDeadline: newDeadline
- });
- }
-
- // 标记为已结算
- private async markSettlementAsCompleted(settlement: Settlement): Promise<void> {
- const reason = await window?.fmode?.input('请输入标记为已结算的原因:');
- if (reason && reason.trim()) {
- settlement.status = '已结算';
- settlement.paidDate = new Date();
-
- this.snackBar.open(
- `✅ 已标记为已结算\n项目:${settlement.projectName}\n原因:${reason}`,
- '关闭',
- {
- duration: 4000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- }
- );
-
- console.log('手动结算记录:', {
- settlementId: settlement.id,
- projectName: settlement.projectName,
- reason: reason,
- completedDate: new Date()
- });
- }
- }
-
- // 取消结算
- private async cancelSettlement(settlement: Settlement): Promise<void> {
- const reason = await window?.fmode?.input('请输入取消结算的原因:');
- if (reason && reason.trim()) {
- if (await window?.fmode?.confirm(`确定要取消此结算吗?\n项目:${settlement.projectName}\n金额:¥${settlement.amount}`)) {
- settlement.status = '已取消';
-
- this.snackBar.open(
- `✅ 已取消结算\n项目:${settlement.projectName}\n原因:${reason}`,
- '关闭',
- {
- duration: 4000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- }
- );
-
- console.log('取消结算记录:', {
- settlementId: settlement.id,
- projectName: settlement.projectName,
- reason: reason,
- cancelledDate: new Date()
- });
- }
- }
- }
- // 发送提醒
- sendReminder(settlementId: string): void {
- const settlement = this.settlements.find(s => s.id === settlementId);
- if (settlement) {
- this.createCustomerServiceReminder(settlement, 'payment_follow_up');
- }
- }
- // 处理单个结算自动化
- processSettlementAutomation(settlement: Settlement): void {
- this.processingSettlementId.set(settlement.id);
- this.isProcessing.set(true);
-
- this.autoSettlementService.processSettlementAutomation(settlement).subscribe({
- next: (processed) => {
- if (processed) {
- console.log(`结算 ${settlement.id} 已自动处理`);
- }
- this.processingSettlementId.set(null);
- this.isProcessing.set(false);
- },
- error: (error) => {
- console.error('自动化处理失败:', error);
- this.processingSettlementId.set(null);
- this.isProcessing.set(false);
- }
- });
- }
- // 项目验收确认相关方法
- isProjectAccepted(projectId: string): boolean {
- return this.projectAcceptances.some(acceptance =>
- acceptance.projectId === projectId && acceptance.acceptanceStatus === 'confirmed'
- );
- }
- // 客服跟进提醒相关方法
- hasCustomerServiceReminder(projectId: string): boolean {
- return this.customerServiceReminders.some(reminder =>
- reminder.projectId === projectId && reminder.reminderStatus === 'completed'
- );
- }
- // 企业微信发图相关方法
- canSendToWeChat(projectId: string): boolean {
- return this.isProjectAccepted(projectId) && !this.isSendingImagesForProject(projectId);
- }
- // 批量处理自动化
- processAllAutomation(): void {
- const pendingSettlements = this.settlements.filter(
- s => s.status === '待结算' && !this.isOverdue(s)
- );
-
- this.isProcessing.set(true);
-
- // 模拟批量处理
- let processedCount = 0;
- const processNext = () => {
- if (processedCount >= pendingSettlements.length) {
- this.isProcessing.set(false);
- return;
- }
-
- const settlement = pendingSettlements[processedCount];
- this.processingSettlementId.set(settlement.id);
-
- this.autoSettlementService.processSettlementAutomation(settlement).subscribe({
- next: () => {
- processedCount++;
- this.processingSettlementId.set(null);
- setTimeout(processNext, 500); // 添加延迟以避免同时处理过多
- },
- error: () => {
- processedCount++;
- this.processingSettlementId.set(null);
- setTimeout(processNext, 500);
- }
- });
- };
-
- processNext();
- }
-
- // 计算统计数据
- stats = computed<SettlementStats>(() => {
- const settlements = this.settlements || [];
-
- return {
- totalAmount: settlements.reduce((sum, s) => sum + (s.amount || 0), 0),
- pendingAmount: settlements.filter(s => s.status === '待结算').reduce((sum, s) => sum + (s.amount || 0), 0),
- overdueAmount: settlements.filter(s => this.isOverdue(s)).reduce((sum, s) => sum + (s.amount || 0), 0),
- completedAmount: settlements.filter(s => s.status === '已结算').reduce((sum, s) => sum + (s.amount || 0), 0),
- totalCount: settlements.length,
- pendingCount: settlements.filter(s => s.status === '待结算').length,
- overdueCount: settlements.filter(s => this.isOverdue(s)).length,
- completedCount: settlements.filter(s => s.status === '已结算').length
- };
- });
-
- // 筛选后的结算列表
- filteredSettlements = computed(() => {
- let filtered = this.settlements || [];
-
- // 状态筛选
- const status = this.statusFilter();
- if (status !== 'all') {
- if (status === 'overdue') {
- filtered = filtered.filter(s => this.isOverdue(s));
- } else {
- filtered = filtered.filter(s => s.status === status);
- }
- }
-
- // 关键词搜索
- const keyword = this.searchKeyword().toLowerCase();
- if (keyword) {
- filtered = filtered.filter(s =>
- (s.projectName || '').toLowerCase().includes(keyword) ||
- (s.stage || '').toLowerCase().includes(keyword)
- );
- }
-
- return filtered;
- });
-
- // 判断是否逾期
- isOverdue(settlement: Settlement): boolean {
- if (settlement.status === '已结算') return false;
- // 简化逾期判断:如果是待结算状态且创建时间超过30天,则认为逾期
- const thirtyDaysAgo = new Date();
- thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
- return settlement.createdAt < thirtyDaysAgo;
- }
-
- // 获取状态样式类
- getStatusClass(settlement: Settlement): string {
- if (this.isOverdue(settlement)) return 'overdue';
- return settlement.status === '已结算' ? 'completed' : 'pending';
- }
-
- // 格式化金额
- formatAmount(amount: number): string {
- return new Intl.NumberFormat('zh-CN', {
- style: 'currency',
- currency: 'CNY',
- minimumFractionDigits: 0,
- maximumFractionDigits: 2
- }).format(amount);
- }
-
- // 更新筛选条件
- updateStatusFilter(status: string): void {
- this.statusFilter.set(status);
- }
-
- updateSearchKeyword(keyword: string): void {
- this.searchKeyword.set(keyword);
- }
- onSearchInput(event: Event): void {
- const target = event.target as HTMLInputElement;
- if (target) {
- this.updateSearchKeyword(target.value);
- }
- }
-
- // 计算逾期天数
- getDaysOverdue(settlement: Settlement): number {
- if (settlement.status === '已结算') return 0;
- const thirtyDaysAgo = new Date();
- thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
- if (settlement.createdAt >= thirtyDaysAgo) return 0;
-
- const today = new Date();
- const diffTime = today.getTime() - thirtyDaysAgo.getTime();
- return Math.max(0, Math.ceil(diffTime / (1000 * 60 * 60 * 24)));
- }
-
- // 验收完成后自动发起尾款结算申请
- private initiateFinalSettlement(contextSettlement: Settlement): void {
- // 查找当前项目的“尾款结算”记录
- this.projectService.getSettlements().subscribe(settlements => {
- const finalSettlement = settlements.find(s => s.projectId === contextSettlement.projectId && s.stage === '尾款结算');
-
- if (finalSettlement) {
- // 标记为已发起(更新时间)并触发自动化处理
- finalSettlement.createdAt = new Date();
- this.snackBar.open(`已自动发起尾款结算申请:${finalSettlement.projectName}`, '关闭', {
- duration: 3000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- });
- // 触发自动化处理(提醒/折扣/自动确认等)
- this.processSettlementAutomation(finalSettlement);
- } else {
- // 若不存在尾款结算记录,则创建一条默认记录并发起
- const newSettlement: Settlement = {
- id: `s${Date.now()}`,
- projectId: contextSettlement.projectId,
- projectName: contextSettlement.projectName,
- stage: '尾款结算',
- amount: 0,
- percentage: 100,
- status: '待结算',
- createdAt: new Date()
- };
- this.projectService.addSettlement(newSettlement).subscribe(() => {
- this.snackBar.open(`已创建并自动发起尾款结算申请:${newSettlement.projectName}`, '关闭', {
- duration: 3000,
- horizontalPosition: 'center',
- verticalPosition: 'top'
- });
- this.processSettlementAutomation(newSettlement);
- });
- }
- });
- }
- }
|