|
|
@@ -270,6 +270,9 @@ export class StageAftercareComponent implements OnInit, OnChanges {
|
|
|
@Input() canEdit: boolean = true;
|
|
|
@Input() orderTotal: number = 0; // 支持外部父组件实时传入订单分配总金额
|
|
|
|
|
|
+ // 导出Math对象,使其可以在模板中使用
|
|
|
+ Math = Math;
|
|
|
+
|
|
|
// 路由参数
|
|
|
cid: string = '';
|
|
|
projectId: string = '';
|
|
|
@@ -393,7 +396,7 @@ export class StageAftercareComponent implements OnInit, OnChanges {
|
|
|
|
|
|
let wwauth = new WxworkAuth({ cid: this.cid });
|
|
|
this.currentUser = await wwauth.currentProfile();
|
|
|
-
|
|
|
+
|
|
|
// ⭐ 修复:添加权限判断逻辑(与其他阶段对齐)
|
|
|
if (this.currentUser) {
|
|
|
const role = this.currentUser.get('roleName') || '';
|
|
|
@@ -1381,10 +1384,34 @@ export class StageAftercareComponent implements OnInit, OnChanges {
|
|
|
// 收集项目数据
|
|
|
const projectData = await this.collectProjectData();
|
|
|
|
|
|
- // 使用豆包1.6生成复盘(与教辅名师项目一致)
|
|
|
- const ai = await this.retroAI.generate({ project: this.project, data: projectData, onProgress: (m) => console.log(m) });
|
|
|
+ // 使用真实数据进行分析计算
|
|
|
+ const efficiencyAnalysis = this.analyzeEfficiency(projectData);
|
|
|
+ const teamPerformance = this.analyzeTeamPerformance(projectData);
|
|
|
+ const financialAnalysis = this.analyzeFinancial(projectData);
|
|
|
+ const satisfactionAnalysis = this.analyzeSatisfaction(projectData);
|
|
|
+ const risksAndOpportunities = this.identifyRisksAndOpportunities(projectData);
|
|
|
+ const productRetrospectives = this.generateProductRetrospectives(projectData);
|
|
|
+ // 基准对比需要效率分析结果
|
|
|
+ const benchmarking = this.generateBenchmarking({
|
|
|
+ ...projectData,
|
|
|
+ efficiencyAnalysis,
|
|
|
+ financialAnalysis
|
|
|
+ });
|
|
|
|
|
|
- // 组装到现有结构
|
|
|
+ // 使用豆包1.6生成AI洞察(基于真实分析结果)
|
|
|
+ const ai = await this.retroAI.generate({
|
|
|
+ project: this.project,
|
|
|
+ data: {
|
|
|
+ ...projectData,
|
|
|
+ efficiencyAnalysis,
|
|
|
+ teamPerformance,
|
|
|
+ financialAnalysis,
|
|
|
+ satisfactionAnalysis
|
|
|
+ },
|
|
|
+ onProgress: (m) => console.log(m)
|
|
|
+ });
|
|
|
+
|
|
|
+ // 组装到现有结构(优先使用真实数据,AI作为补充)
|
|
|
this.projectRetrospective = {
|
|
|
generated: true,
|
|
|
generatedAt: new Date(),
|
|
|
@@ -1395,30 +1422,30 @@ export class StageAftercareComponent implements OnInit, OnChanges {
|
|
|
lessons: this.generateLessons(projectData),
|
|
|
recommendations: ai.recommendations || this.generateRecommendations(projectData),
|
|
|
efficiencyAnalysis: {
|
|
|
- overallScore: ai?.efficiencyAnalysis?.overallScore ?? 82,
|
|
|
- grade: ai?.efficiencyAnalysis?.grade ?? 'B',
|
|
|
- timeEfficiency: { score: 85, plannedDuration: 30, actualDuration: 30, variance: 0 },
|
|
|
- qualityEfficiency: { score: 90, firstPassYield: 90, revisionRate: 10, issueCount: 0 },
|
|
|
- resourceUtilization: { score: 80, teamSize: (this.projectProducts?.length || 3), workload: 85, idleRate: 5 },
|
|
|
- stageMetrics: ai?.efficiencyAnalysis?.stageMetrics || [],
|
|
|
- bottlenecks: ai?.efficiencyAnalysis?.bottlenecks || []
|
|
|
+ ...efficiencyAnalysis,
|
|
|
+ // AI可以优化瓶颈识别
|
|
|
+ bottlenecks: ai?.efficiencyAnalysis?.bottlenecks || efficiencyAnalysis.bottlenecks
|
|
|
},
|
|
|
- teamPerformance: this.analyzeTeamPerformance(projectData),
|
|
|
+ teamPerformance,
|
|
|
financialAnalysis: {
|
|
|
- budgetVariance: ai?.financialAnalysis?.budgetVariance ?? 0,
|
|
|
- profitMargin: ai?.financialAnalysis?.profitMargin ?? 20,
|
|
|
- costBreakdown: { labor: 60, materials: 20, overhead: 15, revisions: 5 },
|
|
|
- revenueAnalysis: ai?.financialAnalysis?.revenueAnalysis || { contracted: 0, received: 0, pending: 0 }
|
|
|
+ ...financialAnalysis,
|
|
|
+ // AI可以优化财务分析
|
|
|
+ profitMargin: ai?.financialAnalysis?.profitMargin ?? financialAnalysis.profitMargin,
|
|
|
+ budgetVariance: ai?.financialAnalysis?.budgetVariance ?? financialAnalysis.budgetVariance
|
|
|
},
|
|
|
satisfactionAnalysis: {
|
|
|
- overallScore: ai?.satisfactionAnalysis?.overallScore ?? (this.customerFeedback?.overallRating || 0) * 20,
|
|
|
- nps: ai?.satisfactionAnalysis?.nps ?? 0,
|
|
|
- dimensions: [],
|
|
|
- improvementAreas: []
|
|
|
+ ...satisfactionAnalysis,
|
|
|
+ // AI可以优化满意度分析
|
|
|
+ nps: ai?.satisfactionAnalysis?.nps ?? satisfactionAnalysis.nps
|
|
|
+ },
|
|
|
+ risksAndOpportunities: {
|
|
|
+ ...risksAndOpportunities,
|
|
|
+ // AI可以补充风险和机会
|
|
|
+ risks: [...risksAndOpportunities.risks, ...(ai?.risksAndOpportunities?.risks || [])],
|
|
|
+ opportunities: [...risksAndOpportunities.opportunities, ...(ai?.risksAndOpportunities?.opportunities || [])]
|
|
|
},
|
|
|
- risksAndOpportunities: ai?.risksAndOpportunities || this.identifyRisksAndOpportunities(projectData),
|
|
|
- productRetrospectives: this.generateProductRetrospectives(projectData),
|
|
|
- benchmarking: this.generateBenchmarking(projectData)
|
|
|
+ productRetrospectives,
|
|
|
+ benchmarking
|
|
|
};
|
|
|
|
|
|
await this.saveDraft();
|
|
|
@@ -1435,11 +1462,16 @@ export class StageAftercareComponent implements OnInit, OnChanges {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 收集项目数据
|
|
|
+ * 收集项目数据(增强版:从数据库收集真实数据)
|
|
|
*/
|
|
|
async collectProjectData(): Promise<any> {
|
|
|
if (!this.project) return {};
|
|
|
|
|
|
+ console.log('📊 开始收集项目数据...', {
|
|
|
+ projectId: this.projectId,
|
|
|
+ projectName: this.project.get('name')
|
|
|
+ });
|
|
|
+
|
|
|
const data = this.project.get('data');
|
|
|
// Parse Server 上的 Project.data 可能是对象或字符串,这里做兼容
|
|
|
let parsedData: any = {};
|
|
|
@@ -1456,6 +1488,21 @@ export class StageAftercareComponent implements OnInit, OnChanges {
|
|
|
parsedData = data;
|
|
|
}
|
|
|
|
|
|
+ // 收集真实数据
|
|
|
+ const [issues, files, teamMembers, activities] = await Promise.all([
|
|
|
+ this.collectProjectIssues(),
|
|
|
+ this.collectProjectFiles(),
|
|
|
+ this.collectTeamMembers(),
|
|
|
+ this.collectActivityLogs()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ console.log('✅ 数据收集完成:', {
|
|
|
+ issues: issues.length,
|
|
|
+ files: files.length,
|
|
|
+ teamMembers: teamMembers.length,
|
|
|
+ activities: activities.length
|
|
|
+ });
|
|
|
+
|
|
|
return {
|
|
|
project: this.project,
|
|
|
projectData: parsedData,
|
|
|
@@ -1463,10 +1510,157 @@ export class StageAftercareComponent implements OnInit, OnChanges {
|
|
|
finalPayment: this.finalPayment,
|
|
|
customerFeedback: this.customerFeedback,
|
|
|
createdAt: this.project.get('createdAt'),
|
|
|
- updatedAt: this.project.get('updatedAt')
|
|
|
+ updatedAt: this.project.get('updatedAt'),
|
|
|
+ deadline: this.project.get('deadline'),
|
|
|
+ // 新增真实数据
|
|
|
+ issues,
|
|
|
+ files,
|
|
|
+ teamMembers,
|
|
|
+ activities
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 收集项目问题数据(ProjectIssue表)
|
|
|
+ */
|
|
|
+ private async collectProjectIssues(): Promise<any[]> {
|
|
|
+ if (!this.projectId) return [];
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ProjectIssue');
|
|
|
+ const project = new Parse.Object('Project');
|
|
|
+ project.id = this.projectId;
|
|
|
+ query.equalTo('project', project);
|
|
|
+ query.notEqualTo('isDeleted', true);
|
|
|
+ query.include('assignee');
|
|
|
+ query.include('creator');
|
|
|
+ query.descending('createdAt');
|
|
|
+ const issues = await query.find();
|
|
|
+ console.log(`✅ 收集项目问题: ${issues.length} 条`);
|
|
|
+ return issues.map(issue => ({
|
|
|
+ id: issue.id,
|
|
|
+ title: issue.get('title') || '',
|
|
|
+ type: issue.get('type') || 'task',
|
|
|
+ priority: issue.get('priority') || 'medium',
|
|
|
+ status: issue.get('status') || '待处理',
|
|
|
+ createdAt: issue.get('createdAt'),
|
|
|
+ updatedAt: issue.get('updatedAt'),
|
|
|
+ resolvedAt: issue.get('resolvedAt'),
|
|
|
+ relatedStage: issue.get('relatedStage') || '',
|
|
|
+ assignee: issue.get('assignee')?.get('name') || '未分配',
|
|
|
+ creator: issue.get('creator')?.get('name') || '未知'
|
|
|
+ }));
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 收集项目问题失败:', error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 收集项目文件数据(ProjectFile表)
|
|
|
+ */
|
|
|
+ private async collectProjectFiles(): Promise<any[]> {
|
|
|
+ if (!this.projectId) return [];
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ProjectFile');
|
|
|
+ const project = new Parse.Object('Project');
|
|
|
+ project.id = this.projectId;
|
|
|
+ query.equalTo('project', project);
|
|
|
+ query.notEqualTo('isDeleted', true);
|
|
|
+ query.include('uploadedBy');
|
|
|
+ query.include('attach');
|
|
|
+ query.descending('createdAt');
|
|
|
+ const files = await query.find();
|
|
|
+ console.log(`✅ 收集项目文件: ${files.length} 条`);
|
|
|
+
|
|
|
+ return files.map(file => {
|
|
|
+ const data = file.get('data') || {};
|
|
|
+ const attach = file.get('attach');
|
|
|
+
|
|
|
+ // 从data字段中提取审批状态、版本等信息
|
|
|
+ const approvalStatus = data.approvalStatus || '待审';
|
|
|
+ const version = data.version || 1;
|
|
|
+ const spaceId = data.spaceId || '';
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: file.id,
|
|
|
+ name: file.get('fileName') || attach?.get('name') || '',
|
|
|
+ type: file.get('fileType') || data.fileType || '',
|
|
|
+ size: file.get('fileSize') || attach?.get('size') || 0,
|
|
|
+ stage: file.get('stage') || '',
|
|
|
+ version: version,
|
|
|
+ status: approvalStatus,
|
|
|
+ uploadedAt: data.uploadedAt || file.get('createdAt'),
|
|
|
+ uploadedBy: file.get('uploadedBy')?.get('name') || data.uploadedByName || '未知',
|
|
|
+ uploadedById: file.get('uploadedBy')?.id || data.uploadedById || '',
|
|
|
+ productId: spaceId,
|
|
|
+ deliverableId: data.deliverableId || ''
|
|
|
+ };
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 收集项目文件失败:', error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 收集团队成员数据(ProjectTeam表)
|
|
|
+ */
|
|
|
+ private async collectTeamMembers(): Promise<any[]> {
|
|
|
+ if (!this.projectId) return [];
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ProjectTeam');
|
|
|
+ const project = new Parse.Object('Project');
|
|
|
+ project.id = this.projectId;
|
|
|
+ query.equalTo('project', project);
|
|
|
+ query.notEqualTo('isDeleted', true);
|
|
|
+ query.include('profile');
|
|
|
+ query.descending('joinedAt');
|
|
|
+ const members = await query.find();
|
|
|
+ console.log(`✅ 收集团队成员: ${members.length} 人`);
|
|
|
+ return members.map(member => ({
|
|
|
+ id: member.id,
|
|
|
+ profileId: member.get('profile')?.id,
|
|
|
+ name: member.get('profile')?.get('name') || '未知',
|
|
|
+ role: member.get('role') || '组员',
|
|
|
+ joinedAt: member.get('joinedAt') || member.get('createdAt'),
|
|
|
+ contribution: member.get('contribution') || 0,
|
|
|
+ status: member.get('status') || 'active'
|
|
|
+ }));
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 收集团队成员失败:', error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 收集活动日志数据(ActivityLog表,如果存在)
|
|
|
+ */
|
|
|
+ private async collectActivityLogs(): Promise<any[]> {
|
|
|
+ if (!this.projectId) return [];
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ActivityLog');
|
|
|
+ const project = new Parse.Object('Project');
|
|
|
+ project.id = this.projectId;
|
|
|
+ query.equalTo('project', project);
|
|
|
+ query.include('actor');
|
|
|
+ query.descending('createdAt');
|
|
|
+ query.limit(100); // 限制数量
|
|
|
+ const activities = await query.find();
|
|
|
+ console.log(`✅ 收集活动日志: ${activities.length} 条`);
|
|
|
+ return activities.map(activity => ({
|
|
|
+ id: activity.id,
|
|
|
+ action: activity.get('action') || '',
|
|
|
+ actor: activity.get('actor')?.get('name') || '系统',
|
|
|
+ createdAt: activity.get('createdAt'),
|
|
|
+ description: activity.get('description') || ''
|
|
|
+ }));
|
|
|
+ } catch (error) {
|
|
|
+ // ActivityLog表可能不存在,静默失败
|
|
|
+ console.log('ℹ️ 活动日志表不存在或查询失败,跳过');
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 生成项目摘要
|
|
|
*/
|
|
|
@@ -1564,226 +1758,710 @@ export class StageAftercareComponent implements OnInit, OnChanges {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 分析效率
|
|
|
+ * 分析效率(基于真实数据)
|
|
|
*/
|
|
|
analyzeEfficiency(data: any): ProjectRetrospective['efficiencyAnalysis'] {
|
|
|
- // 模拟效率分析
|
|
|
- const overallScore = 82;
|
|
|
+ const project = data.project;
|
|
|
+ const projectData = data.projectData || {};
|
|
|
+ const issues = data.issues || [];
|
|
|
+ const files = data.files || [];
|
|
|
+ const teamMembers = data.teamMembers || [];
|
|
|
+
|
|
|
+ // 1. 时间效率分析
|
|
|
+ const createdAt = project.get('createdAt') ? new Date(project.get('createdAt')) : new Date();
|
|
|
+ const deadline = project.get('deadline') ? new Date(project.get('deadline')) : null;
|
|
|
+ const updatedAt = project.get('updatedAt') ? new Date(project.get('updatedAt')) : new Date();
|
|
|
+
|
|
|
+ const actualDuration = Math.ceil((updatedAt.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
+ const plannedDuration = deadline ? Math.ceil((deadline.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24)) : actualDuration;
|
|
|
+ const variance = plannedDuration > 0 ? ((actualDuration - plannedDuration) / plannedDuration) * 100 : 0;
|
|
|
+ const timeEfficiencyScore = plannedDuration > 0 ? Math.max(0, Math.min(100, (plannedDuration / actualDuration) * 100)) : 100;
|
|
|
+
|
|
|
+ // 2. 质量效率分析
|
|
|
+ const totalFiles = files.length;
|
|
|
+ // 首次通过:版本为1且审批状态为已通过、已审核、已审批等
|
|
|
+ const approvedStatuses = ['已通过', '已审核', '已审批', '已审', 'approved', 'passed'];
|
|
|
+ const firstPassFiles = files.filter(f =>
|
|
|
+ f.version === 1 && approvedStatuses.some(status =>
|
|
|
+ f.status?.toLowerCase().includes(status.toLowerCase())
|
|
|
+ )
|
|
|
+ ).length;
|
|
|
+ const firstPassYield = totalFiles > 0 ? (firstPassFiles / totalFiles) * 100 : 100;
|
|
|
+
|
|
|
+ const revisionFiles = files.filter(f => f.version > 1).length;
|
|
|
+ const revisionRate = totalFiles > 0 ? (revisionFiles / totalFiles) * 100 : 0;
|
|
|
+
|
|
|
+ console.log('✨ 质量分析:', {
|
|
|
+ totalFiles,
|
|
|
+ firstPassFiles,
|
|
|
+ firstPassYield: firstPassYield.toFixed(1) + '%',
|
|
|
+ revisionFiles,
|
|
|
+ revisionRate: revisionRate.toFixed(1) + '%'
|
|
|
+ });
|
|
|
+
|
|
|
+ const issueCount = issues.length;
|
|
|
+ const criticalIssues = issues.filter(i => i.priority === 'critical' || i.priority === 'urgent').length;
|
|
|
+ const qualityEfficiencyScore = Math.max(0, 100 - (revisionRate * 0.5) - (criticalIssues * 5) - (issueCount * 2));
|
|
|
+
|
|
|
+ // 3. 资源利用率分析
|
|
|
+ const teamSize = teamMembers.length || 1;
|
|
|
+ const activeMembers = teamMembers.filter(m => m.status === 'active').length;
|
|
|
+ const workload = activeMembers > 0 ? (activeMembers / teamSize) * 100 : 100;
|
|
|
+ const idleRate = Math.max(0, 100 - workload);
|
|
|
+ const resourceUtilizationScore = workload * 0.8 + (100 - idleRate) * 0.2;
|
|
|
+
|
|
|
+ // 4. 阶段效率分析
|
|
|
+ const stageMetrics = this.calculateStageMetrics(projectData, createdAt, updatedAt);
|
|
|
+
|
|
|
+ // 5. 瓶颈识别
|
|
|
+ const bottlenecks = this.identifyBottlenecks(stageMetrics, issues, files);
|
|
|
+
|
|
|
+ // 6. 综合效率得分
|
|
|
+ const overallScore = Math.round((timeEfficiencyScore * 0.4 + qualityEfficiencyScore * 0.4 + resourceUtilizationScore * 0.2));
|
|
|
const grade = overallScore >= 90 ? 'A' : overallScore >= 80 ? 'B' : overallScore >= 70 ? 'C' : overallScore >= 60 ? 'D' : 'F';
|
|
|
|
|
|
return {
|
|
|
overallScore,
|
|
|
grade,
|
|
|
timeEfficiency: {
|
|
|
- score: 85,
|
|
|
- plannedDuration: 30,
|
|
|
- actualDuration: 32,
|
|
|
- variance: 6.7
|
|
|
+ score: Math.round(timeEfficiencyScore),
|
|
|
+ plannedDuration,
|
|
|
+ actualDuration,
|
|
|
+ variance: Math.round(variance * 10) / 10
|
|
|
},
|
|
|
qualityEfficiency: {
|
|
|
- score: 90,
|
|
|
- firstPassYield: 90,
|
|
|
- revisionRate: 10,
|
|
|
- issueCount: 2
|
|
|
+ score: Math.round(qualityEfficiencyScore),
|
|
|
+ firstPassYield: Math.round(firstPassYield * 10) / 10,
|
|
|
+ revisionRate: Math.round(revisionRate * 10) / 10,
|
|
|
+ issueCount
|
|
|
},
|
|
|
resourceUtilization: {
|
|
|
- score: 80,
|
|
|
- teamSize: 3,
|
|
|
- workload: 85,
|
|
|
- idleRate: 5
|
|
|
+ score: Math.round(resourceUtilizationScore),
|
|
|
+ teamSize,
|
|
|
+ workload: Math.round(workload),
|
|
|
+ idleRate: Math.round(idleRate)
|
|
|
},
|
|
|
- stageMetrics: [
|
|
|
- {
|
|
|
- stage: '需求确认',
|
|
|
- plannedDays: 5,
|
|
|
- actualDays: 5,
|
|
|
- efficiency: 100,
|
|
|
- status: 'on-time'
|
|
|
- },
|
|
|
- {
|
|
|
- stage: '下单洽谈',
|
|
|
- plannedDays: 3,
|
|
|
- actualDays: 3,
|
|
|
- efficiency: 100,
|
|
|
- status: 'on-time'
|
|
|
- },
|
|
|
- {
|
|
|
- stage: '交付执行',
|
|
|
- plannedDays: 15,
|
|
|
- actualDays: 17,
|
|
|
- efficiency: 88,
|
|
|
- status: 'delayed',
|
|
|
- delayReason: '客户需求调整'
|
|
|
- },
|
|
|
- {
|
|
|
- stage: '售后归档',
|
|
|
- plannedDays: 7,
|
|
|
- actualDays: 7,
|
|
|
- efficiency: 100,
|
|
|
- status: 'on-time'
|
|
|
- }
|
|
|
- ],
|
|
|
- bottlenecks: [
|
|
|
- {
|
|
|
- stage: '交付执行',
|
|
|
- issue: '实际耗时17天,超出计划13%',
|
|
|
- severity: 'medium',
|
|
|
- suggestion: '建立更严格的需求变更管理流程'
|
|
|
- }
|
|
|
- ]
|
|
|
+ stageMetrics,
|
|
|
+ bottlenecks
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 分析团队绩效
|
|
|
+ * 计算各阶段效率指标
|
|
|
+ */
|
|
|
+ private calculateStageMetrics(projectData: any, startDate: Date, endDate: Date): Array<{
|
|
|
+ stage: string;
|
|
|
+ plannedDays: number;
|
|
|
+ actualDays: number;
|
|
|
+ efficiency: number;
|
|
|
+ status: 'on-time' | 'delayed' | 'ahead';
|
|
|
+ delayReason?: string;
|
|
|
+ }> {
|
|
|
+ const stages = ['订单分配', '需求确认', '交付执行', '售后归档'];
|
|
|
+ const stageStandards: Record<string, number> = {
|
|
|
+ '订单分配': 2,
|
|
|
+ '需求确认': 5,
|
|
|
+ '交付执行': 15,
|
|
|
+ '售后归档': 3
|
|
|
+ };
|
|
|
+
|
|
|
+ const metrics: any[] = [];
|
|
|
+
|
|
|
+ for (const stage of stages) {
|
|
|
+ const stageData = projectData[stage.toLowerCase().replace('订单分配', 'order').replace('需求确认', 'requirements').replace('交付执行', 'delivery').replace('售后归档', 'aftercare')] || {};
|
|
|
+ const startTime = stageData.startTime ? new Date(stageData.startTime) : null;
|
|
|
+ const endTime = stageData.endTime ? new Date(stageData.endTime) : null;
|
|
|
+
|
|
|
+ const plannedDays = stageStandards[stage] || 5;
|
|
|
+ const actualDays = startTime && endTime ? Math.ceil((endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60 * 24)) : plannedDays;
|
|
|
+ const efficiency = plannedDays > 0 ? Math.min(100, (plannedDays / actualDays) * 100) : 100;
|
|
|
+
|
|
|
+ let status: 'on-time' | 'delayed' | 'ahead' = 'on-time';
|
|
|
+ if (actualDays > plannedDays * 1.1) {
|
|
|
+ status = 'delayed';
|
|
|
+ } else if (actualDays < plannedDays * 0.9) {
|
|
|
+ status = 'ahead';
|
|
|
+ }
|
|
|
+
|
|
|
+ metrics.push({
|
|
|
+ stage,
|
|
|
+ plannedDays,
|
|
|
+ actualDays,
|
|
|
+ efficiency: Math.round(efficiency),
|
|
|
+ status,
|
|
|
+ delayReason: status === 'delayed' ? '实际耗时超出计划' : undefined
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return metrics;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 识别瓶颈
|
|
|
+ */
|
|
|
+ private identifyBottlenecks(
|
|
|
+ stageMetrics: any[],
|
|
|
+ issues: any[],
|
|
|
+ files: any[]
|
|
|
+ ): Array<{
|
|
|
+ stage: string;
|
|
|
+ issue: string;
|
|
|
+ severity: 'high' | 'medium' | 'low';
|
|
|
+ suggestion: string;
|
|
|
+ }> {
|
|
|
+ const bottlenecks: any[] = [];
|
|
|
+
|
|
|
+ // 基于阶段延期识别瓶颈
|
|
|
+ for (const stage of stageMetrics) {
|
|
|
+ if (stage.status === 'delayed') {
|
|
|
+ const delayDays = stage.actualDays - stage.plannedDays;
|
|
|
+ const severity = delayDays > 5 ? 'high' : delayDays > 2 ? 'medium' : 'low';
|
|
|
+ bottlenecks.push({
|
|
|
+ stage: stage.stage,
|
|
|
+ issue: `实际耗时${stage.actualDays}天,超出计划${delayDays}天`,
|
|
|
+ severity,
|
|
|
+ suggestion: severity === 'high' ? '需要优化流程,加强进度管控' : '建议提前预警,预留缓冲时间'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 基于问题数量识别瓶颈
|
|
|
+ const criticalIssues = issues.filter(i => i.priority === 'critical' || i.priority === 'urgent');
|
|
|
+ if (criticalIssues.length > 0) {
|
|
|
+ bottlenecks.push({
|
|
|
+ stage: '项目整体',
|
|
|
+ issue: `存在${criticalIssues.length}个严重问题`,
|
|
|
+ severity: criticalIssues.length > 3 ? 'high' : 'medium',
|
|
|
+ suggestion: '建立问题快速响应机制,优先处理严重问题'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 基于修改率识别瓶颈
|
|
|
+ const revisionRate = files.length > 0 ? (files.filter(f => f.version > 1).length / files.length) * 100 : 0;
|
|
|
+ if (revisionRate > 30) {
|
|
|
+ bottlenecks.push({
|
|
|
+ stage: '交付执行',
|
|
|
+ issue: `文件修改率${Math.round(revisionRate)}%,超过30%`,
|
|
|
+ severity: revisionRate > 50 ? 'high' : 'medium',
|
|
|
+ suggestion: '加强需求确认,提升首次通过率'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return bottlenecks;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分析团队绩效(基于真实数据)
|
|
|
*/
|
|
|
analyzeTeamPerformance(data: any): ProjectRetrospective['teamPerformance'] {
|
|
|
+ const teamMembers = data.teamMembers || [];
|
|
|
+ const files = data.files || [];
|
|
|
+ const issues = data.issues || [];
|
|
|
+ const activities = data.activities || [];
|
|
|
+
|
|
|
+ console.log('👥 开始分析团队绩效:', {
|
|
|
+ teamMembers: teamMembers.length,
|
|
|
+ files: files.length,
|
|
|
+ issues: issues.length,
|
|
|
+ activities: activities.length
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算每个成员的绩效
|
|
|
+ const memberPerformance = teamMembers.map((member: any, index: number) => {
|
|
|
+ // 通过profileId或name匹配成员的文件
|
|
|
+ const memberFiles = files.filter((f: any) =>
|
|
|
+ f.uploadedById === member.profileId ||
|
|
|
+ f.uploadedBy === member.name
|
|
|
+ );
|
|
|
+ const memberIssues = issues.filter((i: any) => i.assignee === member.name || i.creator === member.name);
|
|
|
+ const memberActivities = activities.filter((a: any) => a.actor === member.name);
|
|
|
+
|
|
|
+ console.log(`👤 成员 ${member.name}:`, {
|
|
|
+ files: memberFiles.length,
|
|
|
+ issues: memberIssues.length,
|
|
|
+ activities: memberActivities.length
|
|
|
+ });
|
|
|
+
|
|
|
+ // 工作量得分:基于文件数量、问题处理数、活动数
|
|
|
+ const fileCount = memberFiles.length;
|
|
|
+ const issueResolved = memberIssues.filter((i: any) => i.status === '已解决' || i.status === '已关闭').length;
|
|
|
+ const activityCount = memberActivities.length;
|
|
|
+ const workloadScore = Math.min(100, (fileCount * 5 + issueResolved * 10 + activityCount * 2));
|
|
|
+
|
|
|
+ // 质量得分:基于首次通过率、问题数量
|
|
|
+ const approvedStatuses = ['已通过', '已审核', '已审批', '已审', 'approved', 'passed'];
|
|
|
+ const firstPassFiles = memberFiles.filter((f: any) =>
|
|
|
+ f.version === 1 && approvedStatuses.some(status =>
|
|
|
+ f.status?.toLowerCase().includes(status.toLowerCase())
|
|
|
+ )
|
|
|
+ ).length;
|
|
|
+ const firstPassYield = memberFiles.length > 0 ? (firstPassFiles / memberFiles.length) * 100 : 100;
|
|
|
+ const issueCount = memberIssues.length;
|
|
|
+ const qualityScore = Math.max(0, firstPassYield - (issueCount * 5));
|
|
|
+
|
|
|
+ console.log(` 质量: 首次通过${firstPassFiles}/${memberFiles.length}, 通过率${firstPassYield.toFixed(1)}%, 问题${issueCount}个`);
|
|
|
+
|
|
|
+ // 计算修改文件数(用于时间分布)
|
|
|
+ const revisionFiles = memberFiles.filter((f: any) => f.version > 1);
|
|
|
+
|
|
|
+ // 效率得分:基于响应时间、解决时间
|
|
|
+ const resolvedIssues = memberIssues.filter((i: any) => i.resolvedAt);
|
|
|
+ const avgResolutionTime = resolvedIssues.length > 0
|
|
|
+ ? resolvedIssues.reduce((sum: number, i: any) => {
|
|
|
+ const created = i.createdAt ? new Date(i.createdAt).getTime() : 0;
|
|
|
+ const resolved = i.resolvedAt ? new Date(i.resolvedAt).getTime() : 0;
|
|
|
+ return sum + (resolved - created);
|
|
|
+ }, 0) / resolvedIssues.length / (1000 * 60 * 60 * 24) // 转换为天数
|
|
|
+ : 0;
|
|
|
+ const efficiencyScore = Math.max(0, 100 - (avgResolutionTime * 10));
|
|
|
+
|
|
|
+ // 协作得分:基于协助他人、沟通频率
|
|
|
+ const helpedOthers = issues.filter((i: any) => i.assignee === member.name && i.creator !== member.name).length;
|
|
|
+ const communicationScore = Math.min(100, (helpedOthers * 15 + activityCount * 2));
|
|
|
+
|
|
|
+ // 创新得分:基于提出的改进建议
|
|
|
+ const featureIssues = memberIssues.filter((i: any) => i.type === 'feature').length;
|
|
|
+ const innovationScore = Math.min(100, featureIssues * 20);
|
|
|
+
|
|
|
+ // 综合得分
|
|
|
+ const overallScore = Math.round(
|
|
|
+ workloadScore * 0.3 +
|
|
|
+ qualityScore * 0.3 +
|
|
|
+ efficiencyScore * 0.2 +
|
|
|
+ communicationScore * 0.15 +
|
|
|
+ innovationScore * 0.05
|
|
|
+ );
|
|
|
+
|
|
|
+ return {
|
|
|
+ memberId: member.profileId || member.id,
|
|
|
+ memberName: member.name,
|
|
|
+ role: member.role,
|
|
|
+ scores: {
|
|
|
+ workload: Math.round(workloadScore),
|
|
|
+ quality: Math.round(qualityScore),
|
|
|
+ efficiency: Math.round(efficiencyScore),
|
|
|
+ collaboration: Math.round(communicationScore),
|
|
|
+ innovation: Math.round(innovationScore),
|
|
|
+ overall: overallScore
|
|
|
+ },
|
|
|
+ timeDistribution: {
|
|
|
+ // 基于真实数据动态计算时间分布
|
|
|
+ design: Math.max(40, 70 - (revisionFiles.length / memberFiles.length) * 30),
|
|
|
+ communication: Math.min(30, 15 + (issueCount * 2)),
|
|
|
+ revision: Math.min(25, (revisionFiles.length / Math.max(1, memberFiles.length)) * 100 * 0.3),
|
|
|
+ admin: 5
|
|
|
+ },
|
|
|
+ contributions: [
|
|
|
+ `上传${fileCount}个文件`,
|
|
|
+ `解决${issueResolved}个问题`,
|
|
|
+ `参与${activityCount}次活动`
|
|
|
+ ],
|
|
|
+ strengths: [
|
|
|
+ qualityScore >= 80 ? '质量把控优秀' : '',
|
|
|
+ efficiencyScore >= 80 ? '响应效率高' : '',
|
|
|
+ communicationScore >= 80 ? '协作能力强' : ''
|
|
|
+ ].filter(s => s),
|
|
|
+ improvements: [
|
|
|
+ qualityScore < 70 ? '需要提升工作质量' : '',
|
|
|
+ efficiencyScore < 70 ? '需要提升响应速度' : '',
|
|
|
+ communicationScore < 70 ? '需要加强团队协作' : ''
|
|
|
+ ].filter(s => s),
|
|
|
+ ranking: 0 // 将在下面计算
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // 按综合得分排序并设置排名
|
|
|
+ memberPerformance.sort((a, b) => b.scores.overall - a.scores.overall);
|
|
|
+ memberPerformance.forEach((member, index) => {
|
|
|
+ member.ranking = index + 1;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算团队总体得分
|
|
|
+ const overallScore = memberPerformance.length > 0
|
|
|
+ ? Math.round(memberPerformance.reduce((sum, m) => sum + m.scores.overall, 0) / memberPerformance.length)
|
|
|
+ : 0;
|
|
|
+
|
|
|
return {
|
|
|
- overallScore: 85,
|
|
|
- members: [
|
|
|
- {
|
|
|
- memberId: '1',
|
|
|
- memberName: '设计师A',
|
|
|
- role: '主设计师',
|
|
|
- scores: {
|
|
|
- workload: 90,
|
|
|
- quality: 88,
|
|
|
- efficiency: 85,
|
|
|
- collaboration: 92,
|
|
|
- innovation: 80,
|
|
|
- overall: 87
|
|
|
- },
|
|
|
- timeDistribution: {
|
|
|
- design: 60,
|
|
|
- communication: 20,
|
|
|
- revision: 15,
|
|
|
- admin: 5
|
|
|
- },
|
|
|
- contributions: ['完成主要设计方案', '协调客户沟通'],
|
|
|
- strengths: ['设计能力出色', '沟通协作良好'],
|
|
|
- improvements: ['可提升时间管理能力'],
|
|
|
- ranking: 1
|
|
|
- }
|
|
|
- ]
|
|
|
+ overallScore,
|
|
|
+ members: memberPerformance
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 分析财务
|
|
|
+ * 分析财务(基于真实数据)
|
|
|
*/
|
|
|
analyzeFinancial(data: any): ProjectRetrospective['financialAnalysis'] {
|
|
|
+ const finalPayment = data.finalPayment || {};
|
|
|
+ const projectData = data.projectData || {};
|
|
|
+ const quotation = projectData.quotation || {};
|
|
|
+ const files = data.files || [];
|
|
|
+ const issues = data.issues || [];
|
|
|
+ const teamMembers = data.teamMembers || [];
|
|
|
+
|
|
|
+ // 收入分析
|
|
|
+ const contracted = quotation.total || finalPayment.totalAmount || 0;
|
|
|
+ const received = finalPayment.paidAmount || 0;
|
|
|
+ const pending = contracted - received;
|
|
|
+
|
|
|
+ // 成本估算(基于团队规模和项目时长)
|
|
|
+ const project = data.project;
|
|
|
+ const createdAt = project.get('createdAt') ? new Date(project.get('createdAt')) : new Date();
|
|
|
+ const updatedAt = project.get('updatedAt') ? new Date(project.get('updatedAt')) : new Date();
|
|
|
+ const projectDays = Math.ceil((updatedAt.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
+
|
|
|
+ // 人力成本估算(假设人均日薪500元)
|
|
|
+ const avgDailySalary = 500;
|
|
|
+ const laborCost = teamMembers.length * projectDays * avgDailySalary;
|
|
|
+
|
|
|
+ // 修改成本(基于修改次数)
|
|
|
+ const revisionCount = files.filter((f: any) => f.version > 1).length;
|
|
|
+ const revisionCost = revisionCount * 200; // 每次修改成本200元
|
|
|
+
|
|
|
+ // 管理成本(基于沟通轮次和问题数)
|
|
|
+ const issueCount = issues.length;
|
|
|
+ const communicationCost = issueCount * 50; // 每个问题沟通成本50元
|
|
|
+
|
|
|
+ const totalCost = laborCost + revisionCost + communicationCost;
|
|
|
+
|
|
|
+ // 预算偏差(假设预算为合同金额的80%)
|
|
|
+ const budget = contracted * 0.8;
|
|
|
+ const budgetVariance = budget > 0 ? ((totalCost - budget) / budget) * 100 : 0;
|
|
|
+
|
|
|
+ // 利润率
|
|
|
+ const profitMargin = contracted > 0 ? ((contracted - totalCost) / contracted) * 100 : 0;
|
|
|
+
|
|
|
+ // 成本构成
|
|
|
+ const costBreakdown = {
|
|
|
+ labor: contracted > 0 ? (laborCost / contracted) * 100 : 0,
|
|
|
+ materials: 0, // 材料成本通常为0(设计项目)
|
|
|
+ overhead: contracted > 0 ? (communicationCost / contracted) * 100 : 0,
|
|
|
+ revisions: contracted > 0 ? (revisionCost / contracted) * 100 : 0
|
|
|
+ };
|
|
|
+
|
|
|
return {
|
|
|
- budgetVariance: 5,
|
|
|
- profitMargin: 25,
|
|
|
+ budgetVariance: Math.round(budgetVariance * 10) / 10,
|
|
|
+ profitMargin: Math.max(0, Math.round(profitMargin * 10) / 10),
|
|
|
costBreakdown: {
|
|
|
- labor: 60,
|
|
|
- materials: 20,
|
|
|
- overhead: 15,
|
|
|
- revisions: 5
|
|
|
+ labor: Math.round(costBreakdown.labor * 10) / 10,
|
|
|
+ materials: 0,
|
|
|
+ overhead: Math.round(costBreakdown.overhead * 10) / 10,
|
|
|
+ revisions: Math.round(costBreakdown.revisions * 10) / 10
|
|
|
},
|
|
|
revenueAnalysis: {
|
|
|
- contracted: 100000,
|
|
|
- received: 80000,
|
|
|
- pending: 20000
|
|
|
+ contracted: Math.round(contracted),
|
|
|
+ received: Math.round(received),
|
|
|
+ pending: Math.round(pending)
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 分析满意度
|
|
|
+ * 分析满意度(基于真实数据)
|
|
|
*/
|
|
|
analyzeSatisfaction(data: any): ProjectRetrospective['satisfactionAnalysis'] {
|
|
|
- const feedback = data.customerFeedback;
|
|
|
- const overallScore = feedback?.overallRating ? feedback.overallRating * 20 : 0;
|
|
|
-
|
|
|
+ const feedback = data.customerFeedback || {};
|
|
|
+ const overallRating = feedback.overallRating || 0;
|
|
|
+ const overallScore = overallRating * 20;
|
|
|
+
|
|
|
+ // NPS计算
|
|
|
+ const wouldRecommend = feedback.wouldRecommend || false;
|
|
|
+ const recommendationScore = feedback.recommendationWillingness?.score || 0;
|
|
|
+ const nps = wouldRecommend ? (recommendationScore - 5) * 20 : (overallRating - 3) * 20;
|
|
|
+
|
|
|
+ // 维度评分
|
|
|
+ const dimensionRatings = feedback.dimensionRatings || {};
|
|
|
+ const dimensions = [
|
|
|
+ {
|
|
|
+ name: 'designQuality',
|
|
|
+ label: '设计质量',
|
|
|
+ score: (dimensionRatings.designQuality || 0) * 20,
|
|
|
+ benchmark: 75,
|
|
|
+ variance: ((dimensionRatings.designQuality || 0) * 20) - 75
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'serviceAttitude',
|
|
|
+ label: '服务态度',
|
|
|
+ score: (dimensionRatings.serviceAttitude || 0) * 20,
|
|
|
+ benchmark: 80,
|
|
|
+ variance: ((dimensionRatings.serviceAttitude || 0) * 20) - 80
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'deliveryTimeliness',
|
|
|
+ label: '交付及时性',
|
|
|
+ score: (dimensionRatings.deliveryTimeliness || 0) * 20,
|
|
|
+ benchmark: 70,
|
|
|
+ variance: ((dimensionRatings.deliveryTimeliness || 0) * 20) - 70
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'valueForMoney',
|
|
|
+ label: '性价比',
|
|
|
+ score: (dimensionRatings.valueForMoney || 0) * 20,
|
|
|
+ benchmark: 75,
|
|
|
+ variance: ((dimensionRatings.valueForMoney || 0) * 20) - 75
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'communication',
|
|
|
+ label: '沟通协作',
|
|
|
+ score: (dimensionRatings.communication || 0) * 20,
|
|
|
+ benchmark: 80,
|
|
|
+ variance: ((dimensionRatings.communication || 0) * 20) - 80
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 识别改进领域
|
|
|
+ const improvementAreas = dimensions
|
|
|
+ .filter(d => d.score < d.benchmark)
|
|
|
+ .map(d => ({
|
|
|
+ area: d.label,
|
|
|
+ currentScore: Math.round(d.score),
|
|
|
+ targetScore: d.benchmark,
|
|
|
+ priority: d.variance < -20 ? 'high' as const : d.variance < -10 ? 'medium' as const : 'low' as const,
|
|
|
+ actionPlan: d.variance < -20
|
|
|
+ ? `需要重点改进${d.label},当前得分${Math.round(d.score)}分,低于基准${Math.abs(Math.round(d.variance))}分`
|
|
|
+ : `建议提升${d.label},当前得分${Math.round(d.score)}分`
|
|
|
+ }));
|
|
|
+
|
|
|
return {
|
|
|
- overallScore,
|
|
|
- nps: feedback?.recommendationWillingness?.score ? (feedback.recommendationWillingness.score - 5) * 20 : 0,
|
|
|
- dimensions: [
|
|
|
- {
|
|
|
- name: 'designQuality',
|
|
|
- label: '设计质量',
|
|
|
- score: feedback?.dimensionRatings?.designQuality * 20 || 0,
|
|
|
- benchmark: 75,
|
|
|
- variance: (feedback?.dimensionRatings?.designQuality * 20 || 0) - 75
|
|
|
- },
|
|
|
- {
|
|
|
- name: 'serviceAttitude',
|
|
|
- label: '服务态度',
|
|
|
- score: feedback?.dimensionRatings?.serviceAttitude * 20 || 0,
|
|
|
- benchmark: 80,
|
|
|
- variance: (feedback?.dimensionRatings?.serviceAttitude * 20 || 0) - 80
|
|
|
- },
|
|
|
- {
|
|
|
- name: 'deliveryTimeliness',
|
|
|
- label: '交付及时性',
|
|
|
- score: feedback?.dimensionRatings?.deliveryTimeliness * 20 || 0,
|
|
|
- benchmark: 70,
|
|
|
- variance: (feedback?.dimensionRatings?.deliveryTimeliness * 20 || 0) - 70
|
|
|
- }
|
|
|
- ],
|
|
|
- improvementAreas: []
|
|
|
+ overallScore: Math.round(overallScore),
|
|
|
+ nps: Math.round(nps),
|
|
|
+ dimensions: dimensions.map(d => ({
|
|
|
+ ...d,
|
|
|
+ score: Math.round(d.score),
|
|
|
+ variance: Math.round(d.variance)
|
|
|
+ })),
|
|
|
+ improvementAreas
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 识别风险和机会
|
|
|
+ * 识别风险和机会(基于真实数据)
|
|
|
*/
|
|
|
identifyRisksAndOpportunities(data: any): ProjectRetrospective['risksAndOpportunities'] {
|
|
|
+ const project = data.project;
|
|
|
+ const issues = data.issues || [];
|
|
|
+ const files = data.files || [];
|
|
|
+ const finalPayment = data.finalPayment || {};
|
|
|
+ const feedback = data.customerFeedback || {};
|
|
|
+ const projectData = data.projectData || {};
|
|
|
+
|
|
|
+ const risks: any[] = [];
|
|
|
+ const opportunities: any[] = [];
|
|
|
+
|
|
|
+ // 时间风险
|
|
|
+ const deadline = project.get('deadline') ? new Date(project.get('deadline')) : null;
|
|
|
+ const updatedAt = project.get('updatedAt') ? new Date(project.get('updatedAt')) : new Date();
|
|
|
+ if (deadline && updatedAt > deadline) {
|
|
|
+ const delayDays = Math.ceil((updatedAt.getTime() - deadline.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
+ risks.push({
|
|
|
+ type: 'timeline' as const,
|
|
|
+ description: `项目延期${delayDays}天,可能影响后续项目交付`,
|
|
|
+ likelihood: delayDays > 10 ? 5 : delayDays > 5 ? 4 : 3,
|
|
|
+ impact: delayDays > 10 ? 5 : delayDays > 5 ? 4 : 3,
|
|
|
+ severity: delayDays > 10 ? 'high' as const : delayDays > 5 ? 'medium' as const : 'low' as const,
|
|
|
+ mitigation: '建立更严格的时间管控机制,提前预警延期风险'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 质量风险
|
|
|
+ const criticalIssues = issues.filter((i: any) => i.priority === 'critical' || i.priority === 'urgent');
|
|
|
+ if (criticalIssues.length > 0) {
|
|
|
+ risks.push({
|
|
|
+ type: 'quality' as const,
|
|
|
+ description: `存在${criticalIssues.length}个严重问题,可能影响项目质量`,
|
|
|
+ likelihood: criticalIssues.length > 3 ? 5 : 4,
|
|
|
+ impact: 4,
|
|
|
+ severity: criticalIssues.length > 3 ? 'high' as const : 'medium' as const,
|
|
|
+ mitigation: '建立问题快速响应机制,优先处理严重问题'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 财务风险
|
|
|
+ if (finalPayment.status === 'pending' || finalPayment.status === 'overdue') {
|
|
|
+ risks.push({
|
|
|
+ type: 'budget' as const,
|
|
|
+ description: `尾款回收存在风险,待收金额${this.formatCurrency(finalPayment.remainingAmount || 0)}`,
|
|
|
+ likelihood: finalPayment.status === 'overdue' ? 5 : 3,
|
|
|
+ impact: 4,
|
|
|
+ severity: finalPayment.status === 'overdue' ? 'high' as const : 'medium' as const,
|
|
|
+ mitigation: '加强客户沟通,及时跟进尾款回收'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 资源风险
|
|
|
+ const teamMembers = data.teamMembers || [];
|
|
|
+ if (teamMembers.length < 2) {
|
|
|
+ risks.push({
|
|
|
+ type: 'resource' as const,
|
|
|
+ description: '团队规模较小,可能存在资源不足风险',
|
|
|
+ likelihood: 3,
|
|
|
+ impact: 3,
|
|
|
+ severity: 'medium' as const,
|
|
|
+ mitigation: '合理配置团队资源,避免过度依赖单一成员'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 客户推荐机会
|
|
|
+ if (feedback.overallRating >= 4 && feedback.wouldRecommend) {
|
|
|
+ opportunities.push({
|
|
|
+ area: '客户推荐',
|
|
|
+ description: `客户满意度${feedback.overallRating}/5.0,愿意推荐,可争取转介绍`,
|
|
|
+ potential: 5,
|
|
|
+ effort: 2,
|
|
|
+ priority: 'high' as const,
|
|
|
+ actionPlan: '主动询问客户推荐意向,提供推荐激励'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 复购机会
|
|
|
+ if (feedback.overallRating >= 4.5) {
|
|
|
+ opportunities.push({
|
|
|
+ area: '客户复购',
|
|
|
+ description: '客户满意度极高,存在复购或追加项目机会',
|
|
|
+ potential: 4,
|
|
|
+ effort: 3,
|
|
|
+ priority: 'high' as const,
|
|
|
+ actionPlan: '定期跟进客户需求,主动推荐相关服务'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 流程优化机会
|
|
|
+ const revisionRate = files.length > 0 ? (files.filter((f: any) => f.version > 1).length / files.length) * 100 : 0;
|
|
|
+ if (revisionRate < 20) {
|
|
|
+ opportunities.push({
|
|
|
+ area: '流程优化',
|
|
|
+ description: `首次通过率${100 - revisionRate}%,流程效率较高,可作为最佳实践推广`,
|
|
|
+ potential: 4,
|
|
|
+ effort: 2,
|
|
|
+ priority: 'medium' as const,
|
|
|
+ actionPlan: '总结成功经验,形成标准化流程'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
return {
|
|
|
- risks: [
|
|
|
- {
|
|
|
- type: 'timeline',
|
|
|
- description: '后续项目可能面临交付延期风险',
|
|
|
- likelihood: 30,
|
|
|
- impact: 60,
|
|
|
- severity: 'medium',
|
|
|
- mitigation: '加强需求确认和进度管理'
|
|
|
- }
|
|
|
- ],
|
|
|
- opportunities: [
|
|
|
- {
|
|
|
- area: '客户推荐',
|
|
|
- description: '客户满意度高,可争取转介绍',
|
|
|
- potential: 80,
|
|
|
- effort: 20,
|
|
|
- priority: 'high',
|
|
|
- actionPlan: '主动询问客户推荐意向'
|
|
|
- }
|
|
|
- ]
|
|
|
+ risks,
|
|
|
+ opportunities
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 生成Product复盘
|
|
|
+ * 生成Product复盘(基于真实数据)
|
|
|
*/
|
|
|
generateProductRetrospectives(data: any): ProjectRetrospective['productRetrospectives'] {
|
|
|
- return this.projectProducts.map(product => ({
|
|
|
- productId: product.id,
|
|
|
- productName: product.name || this.productSpaceService.getProductTypeName(product.type),
|
|
|
- performance: 85,
|
|
|
- plannedDays: 10,
|
|
|
- actualDays: 11,
|
|
|
- issues: ['部分细节需要调整'],
|
|
|
- recommendations: ['加强前期规划']
|
|
|
- }));
|
|
|
+ const products = data.products || [];
|
|
|
+ const files = data.files || [];
|
|
|
+ const issues = data.issues || [];
|
|
|
+ const feedback = data.customerFeedback || {};
|
|
|
+ const productFeedbacks = feedback.productFeedbacks || [];
|
|
|
+
|
|
|
+ return products.map((product: any) => {
|
|
|
+ const productFiles = files.filter((f: any) => f.productId === product.id);
|
|
|
+ const productIssues = issues.filter((i: any) => i.relatedStage?.includes(product.name) || false);
|
|
|
+ const productFeedback = productFeedbacks.find((pf: any) => pf.productId === product.id);
|
|
|
+
|
|
|
+ // 计算性能得分
|
|
|
+ const fileCount = productFiles.length;
|
|
|
+ const firstPassFiles = productFiles.filter((f: any) => f.version === 1 && (f.status === '已审' || f.status === 'approved')).length;
|
|
|
+ const firstPassYield = fileCount > 0 ? (firstPassFiles / fileCount) * 100 : 100;
|
|
|
+ const issueCount = productIssues.length;
|
|
|
+ const rating = productFeedback?.rating || 0;
|
|
|
+ const performance = Math.round(
|
|
|
+ firstPassYield * 0.4 +
|
|
|
+ (rating * 20) * 0.4 +
|
|
|
+ Math.max(0, 100 - issueCount * 10) * 0.2
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算实际天数(基于文件上传时间)
|
|
|
+ let actualDays = 10;
|
|
|
+ if (productFiles.length > 0) {
|
|
|
+ const firstFile = productFiles[productFiles.length - 1];
|
|
|
+ const lastFile = productFiles[0];
|
|
|
+ if (firstFile.uploadedAt && lastFile.uploadedAt) {
|
|
|
+ const start = new Date(firstFile.uploadedAt).getTime();
|
|
|
+ const end = new Date(lastFile.uploadedAt).getTime();
|
|
|
+ actualDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) || 10;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 识别问题和建议
|
|
|
+ const productIssuesList: string[] = [];
|
|
|
+ if (issueCount > 0) {
|
|
|
+ productIssuesList.push(`发现${issueCount}个相关问题`);
|
|
|
+ }
|
|
|
+ if (firstPassYield < 70) {
|
|
|
+ productIssuesList.push('首次通过率较低,需要加强质量把控');
|
|
|
+ }
|
|
|
+ if (productIssuesList.length === 0) {
|
|
|
+ productIssuesList.push('执行顺利,无明显问题');
|
|
|
+ }
|
|
|
+
|
|
|
+ const recommendations: string[] = [];
|
|
|
+ if (firstPassYield < 80) {
|
|
|
+ recommendations.push('加强需求确认,提升首次通过率');
|
|
|
+ }
|
|
|
+ if (issueCount > 3) {
|
|
|
+ recommendations.push('建立问题预防机制,减少问题发生');
|
|
|
+ }
|
|
|
+ if (rating < 4 && rating > 0) {
|
|
|
+ recommendations.push('关注客户反馈,持续改进产品质量');
|
|
|
+ }
|
|
|
+ if (recommendations.length === 0) {
|
|
|
+ recommendations.push('保持当前执行标准,持续优化');
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ productId: product.id,
|
|
|
+ productName: product.name || this.productSpaceService.getProductTypeName(product.type),
|
|
|
+ performance,
|
|
|
+ plannedDays: 10,
|
|
|
+ actualDays,
|
|
|
+ issues: productIssuesList,
|
|
|
+ recommendations
|
|
|
+ };
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 生成对比分析
|
|
|
+ * 生成对比分析(基于真实数据)
|
|
|
*/
|
|
|
generateBenchmarking(data: any): ProjectRetrospective['benchmarking'] {
|
|
|
+ // 获取当前项目的效率得分(从效率分析中获取)
|
|
|
+ const currentEfficiency = data.efficiencyAnalysis?.overallScore || 82;
|
|
|
+
|
|
|
+ // 历史平均效率(可以从历史项目数据计算,这里使用合理默认值)
|
|
|
+ // 实际应用中应该查询所有已归档项目的平均效率
|
|
|
+ const averageEfficiency = 78; // 行业平均水平
|
|
|
+
|
|
|
+ // 计算排名和百分位(基于当前效率与历史平均的对比)
|
|
|
+ const efficiencyDiff = currentEfficiency - averageEfficiency;
|
|
|
+ const ranking = efficiencyDiff > 20 ? 5 : efficiencyDiff > 10 ? 10 : efficiencyDiff > 0 ? 15 : 20;
|
|
|
+ const percentile = Math.min(100, Math.max(0, 50 + (efficiencyDiff * 2)));
|
|
|
+
|
|
|
+ // 行业基准(基于项目数据计算)
|
|
|
+ const project = data.project;
|
|
|
+ const deadline = project.get('deadline') ? new Date(project.get('deadline')) : null;
|
|
|
+ const createdAt = project.get('createdAt') ? new Date(project.get('createdAt')) : new Date();
|
|
|
+ const updatedAt = project.get('updatedAt') ? new Date(project.get('updatedAt')) : new Date();
|
|
|
+ const actualDuration = Math.ceil((updatedAt.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
+ const plannedDuration = deadline ? Math.ceil((deadline.getTime() - createdAt.getTime()) / (1000 * 60 * 60 * 24)) : actualDuration;
|
|
|
+ const timelineVariance = plannedDuration > 0 ? ((actualDuration - plannedDuration) / plannedDuration) * 100 : 0;
|
|
|
+
|
|
|
+ const feedback = data.customerFeedback || {};
|
|
|
+ const satisfactionScore = feedback.overallRating ? feedback.overallRating * 20 : 85;
|
|
|
+
|
|
|
+ const financialAnalysis = data.financialAnalysis || {};
|
|
|
+ const profitMargin = financialAnalysis.profitMargin || 25;
|
|
|
+
|
|
|
return {
|
|
|
comparisonToHistory: {
|
|
|
- averageEfficiency: 78,
|
|
|
- currentEfficiency: 82,
|
|
|
- ranking: 12,
|
|
|
- percentile: 75
|
|
|
+ averageEfficiency,
|
|
|
+ currentEfficiency,
|
|
|
+ ranking,
|
|
|
+ percentile: Math.round(percentile)
|
|
|
},
|
|
|
industryBenchmark: {
|
|
|
- timelineVariance: 5,
|
|
|
- satisfactionScore: 85,
|
|
|
- profitMargin: 25
|
|
|
+ timelineVariance: Math.round(timelineVariance * 10) / 10,
|
|
|
+ satisfactionScore: Math.round(satisfactionScore),
|
|
|
+ profitMargin: Math.round(profitMargin * 10) / 10
|
|
|
}
|
|
|
};
|
|
|
}
|