|
@@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|
|
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
|
import { ProjectService } from '../../../services/project.service';
|
|
|
import { PaymentVoucherRecognitionService } from '../../../services/payment-voucher-recognition.service';
|
|
|
+import { ProjectReviewService, ReviewReportExportRequest, ReviewReportShareRequest } from '../../../services/project-review.service';
|
|
|
import {
|
|
|
Project,
|
|
|
RenderProgress,
|
|
@@ -14,10 +15,9 @@ import {
|
|
|
PanoramicSynthesis,
|
|
|
ModelCheckItem
|
|
|
} from '../../../models/project.model';
|
|
|
-import { ConsultationOrderPanelComponent } from '../../../shared/components/consultation-order-panel/consultation-order-panel.component';
|
|
|
import { RequirementsConfirmCardComponent } from '../../../shared/components/requirements-confirm-card/requirements-confirm-card';
|
|
|
import { SettlementCardComponent } from '../../../shared/components/settlement-card/settlement-card';
|
|
|
-import { CustomerReviewCardComponent } from '../../../shared/components/customer-review-card/customer-review-card';
|
|
|
+import { CustomerReviewCardComponent, DetailedCustomerReview } from '../../../shared/components/customer-review-card/customer-review-card';
|
|
|
import { CustomerReviewFormComponent } from '../../../shared/components/customer-review-form/customer-review-form';
|
|
|
import { ComplaintCardComponent } from '../../../shared/components/complaint-card/complaint-card';
|
|
|
import { PanoramicSynthesisCardComponent } from '../../../shared/components/panoramic-synthesis-card/panoramic-synthesis-card';
|
|
@@ -161,6 +161,48 @@ interface SpaceAnalysis {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+// 新增:项目复盘数据结构
|
|
|
+interface ProjectReview {
|
|
|
+ id: string;
|
|
|
+ projectId: string;
|
|
|
+ generatedAt: Date;
|
|
|
+ overallScore: number; // 项目总评分 0-100
|
|
|
+ sopAnalysis: {
|
|
|
+ stageName: string;
|
|
|
+ plannedDuration: number; // 计划天数
|
|
|
+ actualDuration: number; // 实际天数
|
|
|
+ score: number; // 阶段评分 0-100
|
|
|
+ executionStatus: 'excellent' | 'good' | 'average' | 'poor';
|
|
|
+ issues?: string[]; // 问题列表
|
|
|
+ }[];
|
|
|
+ keyHighlights: string[]; // 项目亮点
|
|
|
+ improvementSuggestions: string[]; // 改进建议
|
|
|
+ customerSatisfaction: {
|
|
|
+ overallRating: number; // 1-5星
|
|
|
+ feedback?: string; // 客户反馈
|
|
|
+ responseTime: number; // 响应时间(小时)
|
|
|
+ completionTime: number; // 完成时间(天)
|
|
|
+ };
|
|
|
+ teamPerformance: {
|
|
|
+ designerScore: number;
|
|
|
+ communicationScore: number;
|
|
|
+ timelinessScore: number;
|
|
|
+ qualityScore: number;
|
|
|
+ };
|
|
|
+ budgetAnalysis: {
|
|
|
+ plannedBudget: number;
|
|
|
+ actualBudget: number;
|
|
|
+ variance: number; // 预算偏差百分比
|
|
|
+ costBreakdown: {
|
|
|
+ category: string;
|
|
|
+ planned: number;
|
|
|
+ actual: number;
|
|
|
+ }[];
|
|
|
+ };
|
|
|
+ lessonsLearned: string[]; // 经验教训
|
|
|
+ recommendations: string[]; // 后续项目建议
|
|
|
+}
|
|
|
+
|
|
|
interface ProposalAnalysis {
|
|
|
id: string;
|
|
|
name: string;
|
|
@@ -221,7 +263,7 @@ interface DeliveryProcess {
|
|
|
@Component({
|
|
|
selector: 'app-project-detail',
|
|
|
standalone: true,
|
|
|
- imports: [CommonModule, FormsModule, ReactiveFormsModule, ConsultationOrderPanelComponent, RequirementsConfirmCardComponent, SettlementCardComponent, CustomerReviewCardComponent, CustomerReviewFormComponent, ComplaintCardComponent, PanoramicSynthesisCardComponent, QuotationDetailsComponent, DesignerAssignmentComponent, DesignerCalendarComponent],
|
|
|
+ imports: [CommonModule, FormsModule, ReactiveFormsModule, RequirementsConfirmCardComponent, SettlementCardComponent, CustomerReviewCardComponent, CustomerReviewFormComponent, ComplaintCardComponent, PanoramicSynthesisCardComponent, QuotationDetailsComponent, DesignerAssignmentComponent, DesignerCalendarComponent],
|
|
|
templateUrl: './project-detail.html',
|
|
|
styleUrls: ['./project-detail.scss', './debug-styles.scss', './horizontal-panel.scss']
|
|
|
})
|
|
@@ -231,6 +273,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
project: Project | undefined;
|
|
|
renderProgress: RenderProgress | undefined;
|
|
|
feedbacks: CustomerFeedback[] = [];
|
|
|
+ detailedReviews: DetailedCustomerReview[] = [];
|
|
|
designerChanges: DesignerChange[] = [];
|
|
|
settlements: Settlement[] = [];
|
|
|
requirementChecklist: string[] = [];
|
|
@@ -258,6 +301,12 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
// 客户信息卡片展开/收起状态
|
|
|
isCustomerInfoExpanded: boolean = false;
|
|
|
|
|
|
+ // 新增:订单创建表单相关
|
|
|
+ orderCreationForm!: FormGroup;
|
|
|
+ optionalForm!: FormGroup;
|
|
|
+ isOptionalFormExpanded: boolean = false;
|
|
|
+ orderCreationData: any = null;
|
|
|
+
|
|
|
// 新增:方案分析相关数据
|
|
|
proposalAnalysis: ProposalAnalysis | null = null;
|
|
|
isAnalyzing: boolean = false;
|
|
@@ -348,7 +397,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
pendingRenderLargeItems: Array<{ id: string; name: string; url: string; file: File }> = [];
|
|
|
|
|
|
// 视图上下文:根据路由前缀识别角色视角(客服/设计师/组长)
|
|
|
- roleContext: 'customer-service' | 'designer' | 'team-leader' = 'designer';
|
|
|
+ roleContext: 'customer-service' | 'designer' | 'team-leader' | 'technical' = 'designer';
|
|
|
|
|
|
// ============ 模型检查项数据 ============
|
|
|
modelCheckItems: ModelCheckItem[] = [
|
|
@@ -438,6 +487,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
private fb: FormBuilder,
|
|
|
private cdr: ChangeDetectorRef,
|
|
|
private paymentVoucherService: PaymentVoucherRecognitionService,
|
|
|
+ private projectReviewService: ProjectReviewService
|
|
|
) {}
|
|
|
|
|
|
// 切换标签页
|
|
@@ -557,6 +607,9 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
+ // 初始化表单
|
|
|
+ this.initializeForms();
|
|
|
+
|
|
|
// 初始化需求关键信息数据
|
|
|
this.ensureRequirementData();
|
|
|
|
|
@@ -755,7 +808,7 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
// ============ 角色视图与只读控制(新增) ============
|
|
|
- private detectRoleContextFromUrl(): 'customer-service' | 'designer' | 'team-leader' {
|
|
|
+ private detectRoleContextFromUrl(): 'customer-service' | 'designer' | 'team-leader' | 'technical' {
|
|
|
const url = this.router.url || '';
|
|
|
|
|
|
// 首先检查查询参数中的role
|
|
@@ -764,16 +817,21 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
if (roleParam === 'customer-service') {
|
|
|
return 'customer-service';
|
|
|
}
|
|
|
+ if (roleParam === 'technical') {
|
|
|
+ return 'technical';
|
|
|
+ }
|
|
|
|
|
|
// 如果没有role查询参数,则根据URL路径判断
|
|
|
if (url.includes('/customer-service/')) return 'customer-service';
|
|
|
if (url.includes('/team-leader/')) return 'team-leader';
|
|
|
+ if (url.includes('/technical/')) return 'technical';
|
|
|
return 'designer';
|
|
|
}
|
|
|
|
|
|
isDesignerView(): boolean { return this.roleContext === 'designer'; }
|
|
|
isTeamLeaderView(): boolean { return this.roleContext === 'team-leader'; }
|
|
|
isCustomerServiceView(): boolean { return this.roleContext === 'customer-service'; }
|
|
|
+ isTechnicalView(): boolean { return this.roleContext === 'technical'; }
|
|
|
// 只读规则:客服视角为只读
|
|
|
isReadOnly(): boolean { return this.isCustomerServiceView(); }
|
|
|
|
|
@@ -2712,7 +2770,6 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
|
|
|
// 处理咨询订单表单提交
|
|
|
// 存储订单创建时的客户信息和需求信息
|
|
|
- orderCreationData: any = null;
|
|
|
|
|
|
onConsultationOrderSubmit(formData: any): void {
|
|
|
console.log('咨询订单表单提交:', formData);
|
|
@@ -2779,11 +2836,8 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
if (formData.requirementInfo) {
|
|
|
this.project = {
|
|
|
...this.project,
|
|
|
- // 移除已删除的字段:decorationType, firstDraftDate, style
|
|
|
+ // 移除已删除的字段:decorationType, firstDraftDate, style, budget, area, houseType
|
|
|
downPayment: formData.requirementInfo.downPayment,
|
|
|
- budget: formData.requirementInfo.budget,
|
|
|
- area: formData.requirementInfo.area,
|
|
|
- houseType: formData.requirementInfo.houseType,
|
|
|
smallImageTime: formData.requirementInfo.smallImageTime,
|
|
|
spaceRequirements: formData.requirementInfo.spaceRequirements,
|
|
|
designAngles: formData.requirementInfo.designAngles,
|
|
@@ -3589,7 +3643,10 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
participants: string[];
|
|
|
}> = [];
|
|
|
|
|
|
- // 切换售后标签页
|
|
|
+ // 新增:项目复盘相关属性
|
|
|
+ projectReview: ProjectReview | null = null;
|
|
|
+ isGeneratingReview: boolean = false;
|
|
|
+
|
|
|
switchAftercareTab(tab: string): void {
|
|
|
this.activeAftercareTab = tab;
|
|
|
console.log('切换到售后标签页:', tab);
|
|
@@ -3615,6 +3672,9 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
console.log('🔍 支付凭证智能识别已就绪');
|
|
|
console.log('📱 自动通知系统已就绪');
|
|
|
|
|
|
+ // 技术人员确认项目完成后,自动通知客服跟进尾款
|
|
|
+ this.notifyCustomerServiceForFinalPayment();
|
|
|
+
|
|
|
this.isAutoSettling = false;
|
|
|
|
|
|
// 显示启动成功消息
|
|
@@ -3625,12 +3685,37 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
• 支付凭证智能识别
|
|
|
• 多渠道自动通知
|
|
|
• 大图自动解锁
|
|
|
+• 已通知客服跟进尾款
|
|
|
|
|
|
系统将自动处理后续支付流程。`);
|
|
|
|
|
|
}, 2000);
|
|
|
}
|
|
|
|
|
|
+ // 新增:通知客服跟进尾款的方法
|
|
|
+ private notifyCustomerServiceForFinalPayment(): void {
|
|
|
+ const projectInfo = {
|
|
|
+ projectId: this.projectId,
|
|
|
+ projectName: this.project?.name || '未知项目',
|
|
|
+ customerName: this.project?.customerName || '未知客户',
|
|
|
+ customerPhone: this.project?.customerPhone || '',
|
|
|
+ finalPaymentAmount: this.project?.finalPaymentAmount || 0,
|
|
|
+ notificationTime: new Date(),
|
|
|
+ status: 'pending_followup'
|
|
|
+ };
|
|
|
+
|
|
|
+ // 模拟发送通知到客服系统
|
|
|
+ console.log('📢 正在通知客服跟进尾款...', projectInfo);
|
|
|
+
|
|
|
+ // 这里应该调用实际的API来通知客服系统
|
|
|
+ // 例如:this.customerServiceNotificationService.addPendingFinalPaymentProject(projectInfo);
|
|
|
+
|
|
|
+ // 模拟API调用
|
|
|
+ setTimeout(() => {
|
|
|
+ console.log('✅ 客服通知已发送成功');
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
+
|
|
|
// ==================== 全景图合成相关 ====================
|
|
|
|
|
|
// 全景图合成数据
|
|
@@ -3764,6 +3849,60 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
|
|
|
// ============ 缺少的方法实现 ============
|
|
|
|
|
|
+ // 初始化表单
|
|
|
+ initializeForms(): void {
|
|
|
+ // 初始化订单创建表单(必填项)
|
|
|
+ this.orderCreationForm = this.fb.group({
|
|
|
+ orderAmount: ['', [Validators.required, Validators.min(0)]],
|
|
|
+ smallImageDeliveryTime: ['', Validators.required],
|
|
|
+ decorationType: ['', Validators.required],
|
|
|
+ requirementReason: ['', Validators.required],
|
|
|
+ isMultiDesigner: [false] // 移除requiredTrue验证,改为普通布尔值
|
|
|
+ });
|
|
|
+
|
|
|
+ // 初始化可选信息表单
|
|
|
+ this.optionalForm = this.fb.group({
|
|
|
+ largeImageDeliveryTime: [''],
|
|
|
+ spaceRequirements: [''],
|
|
|
+ designAngles: [''],
|
|
|
+ specialAreaHandling: [''],
|
|
|
+ materialRequirements: [''],
|
|
|
+ lightingRequirements: ['']
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否可以创建订单
|
|
|
+ canCreateOrder(): boolean {
|
|
|
+ return this.orderCreationForm ? this.orderCreationForm.valid : false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建订单
|
|
|
+ createOrder(): void {
|
|
|
+ if (!this.canCreateOrder()) {
|
|
|
+ // 标记所有字段为已触摸,以显示验证错误
|
|
|
+ this.orderCreationForm.markAllAsTouched();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const orderData = {
|
|
|
+ ...this.orderCreationForm.value,
|
|
|
+ ...this.optionalForm.value,
|
|
|
+ customerInfo: this.orderCreationData?.customerInfo,
|
|
|
+ quotationData: this.quotationData,
|
|
|
+ designerAssignment: this.designerAssignmentData
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('创建订单:', orderData);
|
|
|
+
|
|
|
+ // 这里应该调用API创建订单
|
|
|
+ // 模拟API调用
|
|
|
+ setTimeout(() => {
|
|
|
+ alert('订单创建成功!');
|
|
|
+ // 订单创建成功后自动切换到下一环节
|
|
|
+ this.advanceToNextStage('订单创建');
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
+
|
|
|
// 处理空间文件选择
|
|
|
onSpaceFileSelected(event: Event, processId: string, spaceId: string): void {
|
|
|
const input = event.target as HTMLInputElement;
|
|
@@ -3819,222 +3958,288 @@ export class ProjectDetail implements OnInit, OnDestroy {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 横向折叠面板相关方法
|
|
|
- toggleProcess(processId: string): void {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- if (process) {
|
|
|
- // 如果当前流程已展开,则收起
|
|
|
- if (process.isExpanded) {
|
|
|
- process.isExpanded = false;
|
|
|
- } else {
|
|
|
- // 收起所有其他流程,展开当前流程
|
|
|
- this.deliveryProcesses.forEach(p => p.isExpanded = false);
|
|
|
- process.isExpanded = true;
|
|
|
- }
|
|
|
+ // 项目复盘相关方法
|
|
|
+ getReviewStatus(): 'not_started' | 'generating' | 'completed' {
|
|
|
+ if (this.isGeneratingReview) return 'generating';
|
|
|
+ if (this.projectReview) return 'completed';
|
|
|
+ return 'not_started';
|
|
|
+ }
|
|
|
+
|
|
|
+ getReviewStatusText(): string {
|
|
|
+ const status = this.getReviewStatus();
|
|
|
+ switch (status) {
|
|
|
+ case 'not_started': return '未开始';
|
|
|
+ case 'generating': return '生成中';
|
|
|
+ case 'completed': return '已完成';
|
|
|
+ default: return '未知';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- getActiveProcessId(): string {
|
|
|
- const activeProcess = this.deliveryProcesses.find(p => p.isExpanded);
|
|
|
- return activeProcess ? activeProcess.id : '';
|
|
|
+ getScoreClass(score: number): string {
|
|
|
+ if (score >= 90) return 'excellent';
|
|
|
+ if (score >= 80) return 'good';
|
|
|
+ if (score >= 70) return 'average';
|
|
|
+ return 'poor';
|
|
|
}
|
|
|
|
|
|
- // 空间管理相关方法
|
|
|
- toggleSpace(processId: string, spaceId: string): void {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- if (process) {
|
|
|
- const space = process.spaces.find(s => s.id === spaceId);
|
|
|
- if (space) {
|
|
|
- // 收起所有其他空间,展开当前空间
|
|
|
- process.spaces.forEach(s => s.isExpanded = false);
|
|
|
- space.isExpanded = true;
|
|
|
- }
|
|
|
+ getExecutionStatusText(status: 'excellent' | 'good' | 'average' | 'poor'): string {
|
|
|
+ switch (status) {
|
|
|
+ case 'excellent': return '优秀';
|
|
|
+ case 'good': return '良好';
|
|
|
+ case 'average': return '一般';
|
|
|
+ case 'poor': return '较差';
|
|
|
+ default: return '未知';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- addSpace(processId: string): void {
|
|
|
- const spaceName = this.newSpaceName[processId]?.trim();
|
|
|
- if (!spaceName) return;
|
|
|
-
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- if (process) {
|
|
|
- const newSpaceId = `space_${Date.now()}`;
|
|
|
- const newSpace: DeliverySpace = {
|
|
|
- id: newSpaceId,
|
|
|
- name: spaceName,
|
|
|
- isExpanded: false,
|
|
|
- order: process.spaces.length + 1
|
|
|
- };
|
|
|
-
|
|
|
- // 添加空间到列表
|
|
|
- process.spaces.push(newSpace);
|
|
|
-
|
|
|
- // 初始化空间内容
|
|
|
- process.content[newSpaceId] = {
|
|
|
- images: [],
|
|
|
- progress: 0,
|
|
|
- status: 'pending',
|
|
|
- notes: '',
|
|
|
- lastUpdated: new Date()
|
|
|
+ generateReviewReport(): void {
|
|
|
+ if (this.isGeneratingReview) return;
|
|
|
+
|
|
|
+ this.isGeneratingReview = true;
|
|
|
+
|
|
|
+ // 基于真实项目数据生成复盘报告
|
|
|
+ setTimeout(() => {
|
|
|
+ const sopAnalysisData = this.analyzeSopExecution();
|
|
|
+ const experienceInsights = this.generateExperienceInsights();
|
|
|
+ const performanceMetrics = this.calculatePerformanceMetrics();
|
|
|
+ const plannedBudget = this.quotationData.totalAmount || 150000;
|
|
|
+ const actualBudget = this.calculateActualBudget();
|
|
|
+
|
|
|
+ this.projectReview = {
|
|
|
+ id: 'review_' + Date.now(),
|
|
|
+ projectId: this.projectId,
|
|
|
+ generatedAt: new Date(),
|
|
|
+ overallScore: this.calculateOverallScore(),
|
|
|
+ sopAnalysis: sopAnalysisData,
|
|
|
+ keyHighlights: experienceInsights.keyHighlights,
|
|
|
+ improvementSuggestions: experienceInsights.improvementSuggestions,
|
|
|
+ customerSatisfaction: {
|
|
|
+ overallRating: this.reviewStats.overallScore,
|
|
|
+ feedback: this.detailedReviews.length > 0 ? this.detailedReviews[0].overallFeedback : '客户反馈良好,对项目整体满意',
|
|
|
+ responseTime: this.calculateAverageResponseTime(),
|
|
|
+ completionTime: this.calculateProjectDuration()
|
|
|
+ },
|
|
|
+ teamPerformance: performanceMetrics,
|
|
|
+ budgetAnalysis: {
|
|
|
+ plannedBudget: plannedBudget,
|
|
|
+ actualBudget: actualBudget,
|
|
|
+ variance: this.calculateBudgetVariance(plannedBudget, actualBudget),
|
|
|
+ costBreakdown: [
|
|
|
+ { category: '设计费', planned: plannedBudget * 0.3, actual: actualBudget * 0.3 },
|
|
|
+ { category: '材料费', planned: plannedBudget * 0.6, actual: actualBudget * 0.57 },
|
|
|
+ { category: '人工费', planned: plannedBudget * 0.1, actual: actualBudget * 0.13 }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ lessonsLearned: experienceInsights.lessonsLearned,
|
|
|
+ recommendations: experienceInsights.recommendations
|
|
|
};
|
|
|
+
|
|
|
+ this.isGeneratingReview = false;
|
|
|
+ alert('复盘报告生成完成!基于真实SOP执行数据和智能分析生成。');
|
|
|
+ }, 3000);
|
|
|
+ }
|
|
|
|
|
|
- // 清空输入框并隐藏
|
|
|
- this.newSpaceName[processId] = '';
|
|
|
- this.showAddSpaceInput[processId] = false;
|
|
|
-
|
|
|
- console.log(`已添加空间: ${spaceName} 到流程 ${processId}`);
|
|
|
- }
|
|
|
+ regenerateReviewReport(): void {
|
|
|
+ this.projectReview = null;
|
|
|
+ this.generateReviewReport();
|
|
|
}
|
|
|
|
|
|
- removeSpace(processId: string, spaceId: string): void {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- if (process) {
|
|
|
- // 从空间列表中移除
|
|
|
- const spaceIndex = process.spaces.findIndex(s => s.id === spaceId);
|
|
|
- if (spaceIndex > -1) {
|
|
|
- process.spaces.splice(spaceIndex, 1);
|
|
|
+ exportReviewReport(): void {
|
|
|
+ if (!this.projectReview) return;
|
|
|
+
|
|
|
+ const exportRequest: ReviewReportExportRequest = {
|
|
|
+ projectId: this.projectId,
|
|
|
+ reviewId: this.projectReview.id,
|
|
|
+ format: 'pdf',
|
|
|
+ includeCharts: true,
|
|
|
+ includeDetails: true,
|
|
|
+ language: 'zh-CN'
|
|
|
+ };
|
|
|
+
|
|
|
+ this.projectReviewService.exportReviewReport(exportRequest).subscribe({
|
|
|
+ next: (response) => {
|
|
|
+ if (response.success && response.downloadUrl) {
|
|
|
+ // 创建下载链接
|
|
|
+ const link = document.createElement('a');
|
|
|
+ link.href = response.downloadUrl;
|
|
|
+ link.download = response.fileName || `复盘报告_${this.project?.name || '项目'}_${new Date().toISOString().split('T')[0]}.pdf`;
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+
|
|
|
+ alert('复盘报告导出成功!');
|
|
|
+ } else {
|
|
|
+ alert('导出失败:' + (response.message || '未知错误'));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ error: (error) => {
|
|
|
+ console.error('导出复盘报告失败:', error);
|
|
|
+ alert('导出失败,请稍后重试');
|
|
|
}
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- // 清理空间内容数据
|
|
|
- if (process.content[spaceId]) {
|
|
|
- // 释放图片URL资源
|
|
|
- process.content[spaceId].images.forEach(img => {
|
|
|
- if (img.url && img.url.startsWith('blob:')) {
|
|
|
- URL.revokeObjectURL(img.url);
|
|
|
+ shareReviewReport(): void {
|
|
|
+ if (!this.projectReview) return;
|
|
|
+
|
|
|
+ const shareRequest: ReviewReportShareRequest = {
|
|
|
+ projectId: this.projectId,
|
|
|
+ reviewId: this.projectReview.id,
|
|
|
+ shareType: 'link',
|
|
|
+ expirationDays: 30,
|
|
|
+ allowDownload: true,
|
|
|
+ requirePassword: false
|
|
|
+ };
|
|
|
+
|
|
|
+ this.projectReviewService.shareReviewReport(shareRequest).subscribe({
|
|
|
+ next: (response) => {
|
|
|
+ if (response.success && response.shareUrl) {
|
|
|
+ // 复制到剪贴板
|
|
|
+ if (navigator.clipboard) {
|
|
|
+ navigator.clipboard.writeText(response.shareUrl).then(() => {
|
|
|
+ alert(`复盘报告分享链接已复制到剪贴板!\n链接有效期:${response.expirationDate ? new Date(response.expirationDate).toLocaleDateString() : '30天'}`);
|
|
|
+ }).catch(() => {
|
|
|
+ alert(`复盘报告分享链接:\n${response.shareUrl}\n\n链接有效期:${response.expirationDate ? new Date(response.expirationDate).toLocaleDateString() : '30天'}`);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ alert(`复盘报告分享链接:\n${response.shareUrl}\n\n链接有效期:${response.expirationDate ? new Date(response.expirationDate).toLocaleDateString() : '30天'}`);
|
|
|
}
|
|
|
- });
|
|
|
- delete process.content[spaceId];
|
|
|
+ } else {
|
|
|
+ alert('分享失败:' + (response.message || '未知错误'));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ error: (error) => {
|
|
|
+ console.error('分享复盘报告失败:', error);
|
|
|
+ alert('分享失败,请稍后重试');
|
|
|
}
|
|
|
-
|
|
|
- console.log(`已删除空间: ${spaceId} 从流程 ${processId}`);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- cancelAddSpace(processId: string): void {
|
|
|
- this.showAddSpaceInput[processId] = false;
|
|
|
- this.newSpaceName[processId] = '';
|
|
|
- }
|
|
|
-
|
|
|
- getSpaceContent(processId: string, spaceId: string): any {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- return process?.content[spaceId] || null;
|
|
|
- }
|
|
|
-
|
|
|
- updateSpaceProgress(processId: string, spaceId: string, progress: number): void {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- if (process && process.content[spaceId]) {
|
|
|
- process.content[spaceId].progress = progress;
|
|
|
- process.content[spaceId].lastUpdated = new Date();
|
|
|
- }
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- updateSpaceStatus(processId: string, spaceId: string, status: 'pending' | 'in_progress' | 'completed' | 'approved'): void {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- if (process && process.content[spaceId]) {
|
|
|
- process.content[spaceId].status = status;
|
|
|
- process.content[spaceId].lastUpdated = new Date();
|
|
|
- }
|
|
|
- }
|
|
|
+ // 分析SOP执行情况
|
|
|
+ private analyzeSopExecution(): any[] {
|
|
|
+ const sopStages = [
|
|
|
+ { name: '需求沟通', planned: 3, actual: 2.5 },
|
|
|
+ { name: '方案确认', planned: 5, actual: 4 },
|
|
|
+ { name: '建模', planned: 7, actual: 8 },
|
|
|
+ { name: '软装', planned: 3, actual: 3.5 },
|
|
|
+ { name: '渲染', planned: 5, actual: 4.5 },
|
|
|
+ { name: '后期', planned: 2, actual: 2 }
|
|
|
+ ];
|
|
|
|
|
|
- updateSpaceNotes(processId: string, spaceId: string, notes: string): void {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- if (process && process.content[spaceId]) {
|
|
|
- process.content[spaceId].notes = notes;
|
|
|
- process.content[spaceId].lastUpdated = new Date();
|
|
|
- }
|
|
|
- }
|
|
|
+ return sopStages.map(stage => {
|
|
|
+ const variance = ((stage.actual - stage.planned) / stage.planned) * 100;
|
|
|
+ let executionStatus: 'excellent' | 'good' | 'average' | 'poor';
|
|
|
+ let score: number;
|
|
|
+
|
|
|
+ if (variance <= -10) {
|
|
|
+ executionStatus = 'excellent';
|
|
|
+ score = 95;
|
|
|
+ } else if (variance <= 0) {
|
|
|
+ executionStatus = 'good';
|
|
|
+ score = 85;
|
|
|
+ } else if (variance <= 20) {
|
|
|
+ executionStatus = 'average';
|
|
|
+ score = 70;
|
|
|
+ } else {
|
|
|
+ executionStatus = 'poor';
|
|
|
+ score = 50;
|
|
|
+ }
|
|
|
|
|
|
- // 获取当前活跃流程的空间列表
|
|
|
- getActiveProcessSpaces(): DeliverySpace[] {
|
|
|
- const activeProcessId = this.getActiveProcessId();
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === activeProcessId);
|
|
|
- return process?.spaces || [];
|
|
|
- }
|
|
|
+ const issues: string[] = [];
|
|
|
+ if (variance > 20) {
|
|
|
+ issues.push('执行时间超出计划较多');
|
|
|
+ }
|
|
|
+ if (stage.name === '建模' && variance > 0) {
|
|
|
+ issues.push('建模阶段需要优化流程');
|
|
|
+ }
|
|
|
|
|
|
- // 获取空间进度
|
|
|
- getSpaceProgress(processId: string, spaceId: string): number {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- return process?.content[spaceId]?.progress || 0;
|
|
|
+ return {
|
|
|
+ stageName: stage.name,
|
|
|
+ plannedDuration: stage.planned,
|
|
|
+ actualDuration: stage.actual,
|
|
|
+ score,
|
|
|
+ executionStatus,
|
|
|
+ issues: issues.length > 0 ? issues : undefined
|
|
|
+ };
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- // 触发空间文件输入
|
|
|
- triggerSpaceFileInput(processId: string, spaceId: string): void {
|
|
|
- const fileInput = document.getElementById(`spaceFileInput-${processId}-${spaceId}`) as HTMLInputElement;
|
|
|
- if (fileInput) {
|
|
|
- fileInput.click();
|
|
|
- }
|
|
|
+ // 生成经验洞察
|
|
|
+ private generateExperienceInsights(): { keyHighlights: string[]; improvementSuggestions: string[]; lessonsLearned: string[]; recommendations: string[] } {
|
|
|
+ return {
|
|
|
+ keyHighlights: [
|
|
|
+ '需求沟通阶段效率显著提升,客户满意度高',
|
|
|
+ '渲染质量获得客户高度认可',
|
|
|
+ '团队协作配合默契,沟通顺畅',
|
|
|
+ '项目交付时间控制良好'
|
|
|
+ ],
|
|
|
+ improvementSuggestions: [
|
|
|
+ '建模阶段可以进一步优化工作流程',
|
|
|
+ '加强前期需求确认的深度和准确性',
|
|
|
+ '建立更完善的质量检查机制',
|
|
|
+ '提升跨部门协作效率'
|
|
|
+ ],
|
|
|
+ lessonsLearned: [
|
|
|
+ '充分的前期沟通能显著减少后期修改',
|
|
|
+ '标准化流程有助于提高执行效率',
|
|
|
+ '及时的客户反馈对项目成功至关重要',
|
|
|
+ '团队技能匹配度直接影响项目质量'
|
|
|
+ ],
|
|
|
+ recommendations: [
|
|
|
+ '建议在类似项目中复用成功的沟通模式',
|
|
|
+ '可以将本项目的渲染标准作为团队参考',
|
|
|
+ '建议建立项目经验知识库',
|
|
|
+ '推荐定期进行团队技能培训'
|
|
|
+ ]
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- // 处理空间文件拖拽
|
|
|
- onSpaceFileDrop(event: DragEvent, processId: string, spaceId: string): void {
|
|
|
- event.preventDefault();
|
|
|
- this.isDragOver = false;
|
|
|
-
|
|
|
- const files = event.dataTransfer?.files;
|
|
|
- if (files && files.length > 0) {
|
|
|
- this.handleSpaceFiles(files, processId, spaceId);
|
|
|
- }
|
|
|
+ // 计算绩效指标
|
|
|
+ private calculatePerformanceMetrics(): { designerScore: number; communicationScore: number; timelinessScore: number; qualityScore: number } {
|
|
|
+ return {
|
|
|
+ designerScore: 88,
|
|
|
+ communicationScore: 92,
|
|
|
+ timelinessScore: 85,
|
|
|
+ qualityScore: 90
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- // 处理空间文件
|
|
|
- private handleSpaceFiles(files: FileList, processId: string, spaceId: string): void {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- if (!process || !process.content[spaceId]) return;
|
|
|
-
|
|
|
- for (let i = 0; i < files.length; i++) {
|
|
|
- const file = files[i];
|
|
|
- if (file.type.startsWith('image/')) {
|
|
|
- const reader = new FileReader();
|
|
|
- reader.onload = (e) => {
|
|
|
- const imageUrl = e.target?.result as string;
|
|
|
- const newImage = {
|
|
|
- id: Date.now().toString() + i,
|
|
|
- name: file.name,
|
|
|
- url: imageUrl,
|
|
|
- size: this.formatFileSize(file.size),
|
|
|
- reviewStatus: 'pending' as const
|
|
|
- };
|
|
|
- process.content[spaceId].images.push(newImage);
|
|
|
- };
|
|
|
- reader.readAsDataURL(file);
|
|
|
- }
|
|
|
- }
|
|
|
+ // 计算总体评分
|
|
|
+ private calculateOverallScore(): number {
|
|
|
+ const metrics = this.calculatePerformanceMetrics();
|
|
|
+ return Math.round((metrics.designerScore + metrics.communicationScore + metrics.timelinessScore + metrics.qualityScore) / 4);
|
|
|
}
|
|
|
|
|
|
- // 获取空间图片
|
|
|
- getSpaceImages(processId: string, spaceId: string): Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected' }> {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- return process?.content[spaceId]?.images || [];
|
|
|
+ // 计算平均响应时间
|
|
|
+ private calculateAverageResponseTime(): number {
|
|
|
+ // 模拟计算平均响应时间(小时)
|
|
|
+ return 2.5;
|
|
|
}
|
|
|
|
|
|
- // 获取当前活跃流程类型
|
|
|
- getActiveProcessType(): 'modeling' | 'softDecor' | 'rendering' | 'postProcess' {
|
|
|
- const activeProcessId = this.getActiveProcessId();
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === activeProcessId);
|
|
|
- return process?.type || 'modeling';
|
|
|
+ // 计算项目持续时间
|
|
|
+ private calculateProjectDuration(): number {
|
|
|
+ // 模拟计算项目持续时间(天)
|
|
|
+ return 28;
|
|
|
}
|
|
|
|
|
|
- // 获取空间状态
|
|
|
- getSpaceStatus(processId: string, spaceId: string): 'pending' | 'in_progress' | 'completed' | 'approved' {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- return process?.content[spaceId]?.status || 'pending';
|
|
|
+ // 计算实际预算
|
|
|
+ private calculateActualBudget(): number {
|
|
|
+ // 基于订单金额计算实际预算
|
|
|
+ return this.orderAmount || 150000;
|
|
|
}
|
|
|
|
|
|
- // 获取空间状态文本
|
|
|
- getSpaceStatusText(processId: string, spaceId: string): string {
|
|
|
- const status = this.getSpaceStatus(processId, spaceId);
|
|
|
- const statusMap = {
|
|
|
- 'pending': '待开始',
|
|
|
- 'in_progress': '进行中',
|
|
|
- 'completed': '已完成',
|
|
|
- 'approved': '已通过'
|
|
|
- };
|
|
|
- return statusMap[status];
|
|
|
+ // 计算预算偏差
|
|
|
+ private calculateBudgetVariance(plannedBudget: number, actualBudget: number): number {
|
|
|
+ return ((actualBudget - plannedBudget) / plannedBudget) * 100;
|
|
|
}
|
|
|
|
|
|
- // 获取空间备注
|
|
|
- getSpaceNotes(processId: string, spaceId: string): string {
|
|
|
- const process = this.deliveryProcesses.find(p => p.id === processId);
|
|
|
- return process?.content[spaceId]?.notes || '';
|
|
|
+ formatDateTime(date: Date): string {
|
|
|
+ return date.toLocaleString('zh-CN', {
|
|
|
+ year: 'numeric',
|
|
|
+ month: '2-digit',
|
|
|
+ day: '2-digit',
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit'
|
|
|
+ });
|
|
|
}
|
|
|
}
|