|
@@ -1,4 +1,4 @@
|
|
|
-import { Component, Input, Output, EventEmitter, OnInit, ChangeDetectorRef } from '@angular/core';
|
|
|
|
|
|
|
+import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, ChangeDetectorRef } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { DesignerCalendarComponent } from '../../../../customer-service/consultation-order/components/designer-calendar/designer-calendar.component';
|
|
import { DesignerCalendarComponent } from '../../../../customer-service/consultation-order/components/designer-calendar/designer-calendar.component';
|
|
@@ -71,7 +71,7 @@ export interface DesignerAssignmentResult {
|
|
|
templateUrl: './designer-team-assignment-modal.component.html',
|
|
templateUrl: './designer-team-assignment-modal.component.html',
|
|
|
styleUrls: ['./designer-team-assignment-modal.component.scss']
|
|
styleUrls: ['./designer-team-assignment-modal.component.scss']
|
|
|
})
|
|
})
|
|
|
-export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
|
|
|
|
+export class DesignerTeamAssignmentModalComponent implements OnInit, OnChanges {
|
|
|
@Input() isVisible = false;
|
|
@Input() isVisible = false;
|
|
|
@Input() visible = false; // 添加visible属性以兼容父组件
|
|
@Input() visible = false; // 添加visible属性以兼容父组件
|
|
|
@Input() quotationItems: any[] = [];
|
|
@Input() quotationItems: any[] = [];
|
|
@@ -98,6 +98,11 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
loadingSpaces = false;
|
|
loadingSpaces = false;
|
|
|
loadError = '';
|
|
loadError = '';
|
|
|
spaceLoadError = '';
|
|
spaceLoadError = '';
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 新增:项目数据缓存(避免重复查询)
|
|
|
|
|
+ private projectDataCache: Map<string, any> = new Map();
|
|
|
|
|
+ private lastCacheTime: number = 0;
|
|
|
|
|
+ private CACHE_DURATION = 30000; // 30秒缓存
|
|
|
|
|
|
|
|
// 项目组数据(作为默认数据,如果没有通过@Input传入)
|
|
// 项目组数据(作为默认数据,如果没有通过@Input传入)
|
|
|
defaultProjectTeams: ProjectTeam[] = [
|
|
defaultProjectTeams: ProjectTeam[] = [
|
|
@@ -279,6 +284,8 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
) {}
|
|
) {}
|
|
|
|
|
|
|
|
async ngOnInit() {
|
|
async ngOnInit() {
|
|
|
|
|
+ console.log('🚀 [设计师分配弹窗] 初始化,loadRealData:', this.loadRealData);
|
|
|
|
|
+
|
|
|
// 如果需要加载真实数据
|
|
// 如果需要加载真实数据
|
|
|
if (this.loadRealData) {
|
|
if (this.loadRealData) {
|
|
|
await this.loadRealProjectTeams();
|
|
await this.loadRealProjectTeams();
|
|
@@ -301,6 +308,23 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
this.filterStagnantDesigners();
|
|
this.filterStagnantDesigners();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 🔥 监听输入变化,弹窗打开时重新加载数据
|
|
|
|
|
+ */
|
|
|
|
|
+ async ngOnChanges(changes: SimpleChanges) {
|
|
|
|
|
+ // 当弹窗从关闭变为打开时,重新加载项目数据
|
|
|
|
|
+ if (changes['visible'] || changes['isVisible']) {
|
|
|
|
|
+ const currentVisible = this.visible || this.isVisible;
|
|
|
|
|
+ const previousVisible = changes['visible']?.previousValue || changes['isVisible']?.previousValue;
|
|
|
|
|
+
|
|
|
|
|
+ if (currentVisible && !previousVisible && this.loadRealData) {
|
|
|
|
|
+ console.log('🔄 [设计师分配弹窗] 弹窗打开,重新加载项目数据...');
|
|
|
|
|
+ await this.enrichMembersWithProjectAssignments();
|
|
|
|
|
+ this.cdr.markForCheck();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 从Parse Server加载真实的项目组和成员数据
|
|
* 从Parse Server加载真实的项目组和成员数据
|
|
|
*/
|
|
*/
|
|
@@ -451,52 +475,353 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
* 为所有成员加载其真实项目分配,填充 currentProjects / workload / reviewDates / projectEvents 等
|
|
* 为所有成员加载其真实项目分配,填充 currentProjects / workload / reviewDates / projectEvents 等
|
|
|
*/
|
|
*/
|
|
|
private async enrichMembersWithProjectAssignments(): Promise<void> {
|
|
private async enrichMembersWithProjectAssignments(): Promise<void> {
|
|
|
|
|
+ console.log('\n'.repeat(3) + '═'.repeat(80));
|
|
|
|
|
+ console.log('%c🚀🚀🚀 [项目数据加载] ===== 开始加载所有成员的项目分配 =====', 'background: #E91E63; color: white; font-size: 20px; font-weight: bold; padding: 10px;');
|
|
|
|
|
+ console.log('═'.repeat(80) + '\n');
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
const allMembers: Designer[] = this.projectTeams.flatMap(t => t.members);
|
|
const allMembers: Designer[] = this.projectTeams.flatMap(t => t.members);
|
|
|
- if (allMembers.length === 0) return;
|
|
|
|
|
|
|
+ console.log('%c📊 [项目数据加载] 总成员数: ' + allMembers.length, 'background: #3F51B5; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+ console.log('%c📊 [项目数据加载] 成员列表: ' + allMembers.map(m => m.name).join(', '), 'background: #3F51B5; color: white; font-size: 14px; padding: 5px;');
|
|
|
|
|
+
|
|
|
|
|
+ if (allMembers.length === 0) {
|
|
|
|
|
+ console.error('❌ [项目数据加载] 没有成员,跳过');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const Profile = Parse.Object.extend('Profile');
|
|
|
|
|
- const profilePointers = allMembers.map(m => {
|
|
|
|
|
- const p = new Profile();
|
|
|
|
|
- p.id = m.id;
|
|
|
|
|
- return p;
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // 🔥 检查缓存,避免频繁查询
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ if (now - this.lastCacheTime < this.CACHE_DURATION && this.projectDataCache.size > 0) {
|
|
|
|
|
+ console.log('%c⚡ [项目数据加载] 使用缓存数据,跳过查询', 'background: #00BCD4; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+ this.applyProjectDataFromCache(allMembers);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const ptQuery = new Parse.Query('ProjectTeam');
|
|
|
|
|
- ptQuery.containedIn('profile', profilePointers);
|
|
|
|
|
- ptQuery.notEqualTo('isDeleted', true);
|
|
|
|
|
- ptQuery.include('project');
|
|
|
|
|
- ptQuery.limit(1000);
|
|
|
|
|
|
|
+ console.log('%c🔍 [项目数据加载] 目标成员 IDs:', 'background: #673AB7; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+ allMembers.forEach(m => {
|
|
|
|
|
+ console.log(` - ${m.name} (ID: ${m.id})`);
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- const rows = await ptQuery.find();
|
|
|
|
|
|
|
+ // 🔥 完全对齐团队组长端:查询所有公司的 ProjectTeam,不限制 profile 范围
|
|
|
|
|
+ console.log('\n' + '─'.repeat(80));
|
|
|
|
|
+ console.log('🔍 [项目数据加载] 方案1:开始查询 ProjectTeam 表(对齐组长端逻辑)...');
|
|
|
|
|
+ console.log('─'.repeat(80));
|
|
|
|
|
+
|
|
|
|
|
+ const companyId = localStorage.getItem('company');
|
|
|
|
|
+ console.log('📋 [项目数据加载] 公司 ID:', companyId);
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 对齐团队组长端:先查询当前公司的所有项目
|
|
|
|
|
+ const companyProjectQuery = new Parse.Query('Project');
|
|
|
|
|
+ companyProjectQuery.equalTo('company', companyId);
|
|
|
|
|
+ companyProjectQuery.notEqualTo('isDeleted', true);
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 对齐团队组长端:查询当前公司项目的所有 ProjectTeam(不限制 profile)
|
|
|
|
|
+ const teamQuery = new Parse.Query('ProjectTeam');
|
|
|
|
|
+ teamQuery.matchesQuery('project', companyProjectQuery);
|
|
|
|
|
+ teamQuery.notEqualTo('isDeleted', true);
|
|
|
|
|
+ teamQuery.include('project');
|
|
|
|
|
+ teamQuery.include('profile');
|
|
|
|
|
+ teamQuery.limit(1000);
|
|
|
|
|
+
|
|
|
|
|
+ let teamRecords: any[] = [];
|
|
|
|
|
+ try {
|
|
|
|
|
+ teamRecords = await teamQuery.find();
|
|
|
|
|
+ console.log('%c✅ [项目数据加载] ProjectTeam 查询成功!', 'background: #4CAF50; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+ console.log('%c📊 [项目数据加载] 查询结果: ' + teamRecords.length + ' 条记录', 'background: #2196F3; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 详细输出每条记录
|
|
|
|
|
+ if (teamRecords.length > 0) {
|
|
|
|
|
+ console.log('%c📋 [ProjectTeam 记录详情]', 'background: #FF9800; color: white; font-size: 14px; padding: 5px;');
|
|
|
|
|
+ teamRecords.forEach((record, index) => {
|
|
|
|
|
+ const profile = record.get('profile');
|
|
|
|
|
+ const project = record.get('project');
|
|
|
|
|
+ console.log(` ${index + 1}. Profile: ${profile?.get('name') || '未知'} (${profile?.id || 'N/A'})`);
|
|
|
|
|
+ console.log(` Project: ${project?.get('title') || project?.get('name') || '未命名'} (${project?.id || 'N/A'})`);
|
|
|
|
|
+ console.log(` 状态: ${project?.get('status') || 'N/A'}, 阶段: ${project?.get('currentStage') || project?.get('stage') || 'N/A'}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('%c⚠️ [ProjectTeam 查询] 返回 0 条记录!将使用降级方案查询 Project 表', 'background: #FF5722; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (teamQueryError) {
|
|
|
|
|
+ console.error('%c❌ [项目数据加载] ProjectTeam 查询失败', 'background: #F44336; color: white; font-size: 16px; padding: 5px;', teamQueryError);
|
|
|
|
|
+ console.error('错误详情:', {
|
|
|
|
|
+ message: (teamQueryError as any).message,
|
|
|
|
|
+ code: (teamQueryError as any).code
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let projects: any[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 如果 ProjectTeam 表为空,使用降级方案:直接查询 Project 表
|
|
|
|
|
+ if (teamRecords.length === 0) {
|
|
|
|
|
+ console.log('\n' + '─'.repeat(80));
|
|
|
|
|
+ console.log('%c⚠️ [项目数据加载] ProjectTeam 表无数据,启用降级方案', 'background: #FF9800; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+ console.log('%c🔍 [项目数据加载] 方案2:直接查询 Project 表(通过 assignee 字段)...', 'background: #9C27B0; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+ console.log('─'.repeat(80));
|
|
|
|
|
+
|
|
|
|
|
+ const projectQuery = new Parse.Query('Project');
|
|
|
|
|
+ projectQuery.equalTo('company', companyId);
|
|
|
|
|
+ projectQuery.equalTo('isDeleted', false);
|
|
|
|
|
+ projectQuery.include('assignee');
|
|
|
|
|
+ projectQuery.include('department');
|
|
|
|
|
+ projectQuery.limit(1000);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const allProjects = await projectQuery.find();
|
|
|
|
|
+ console.log('%c✅ [项目数据加载] Project 查询成功!', 'background: #4CAF50; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+ console.log('%c📊 [项目数据加载] 查询结果: ' + allProjects.length + ' 个项目', 'background: #2196F3; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+
|
|
|
|
|
+ // 过滤出分配给这些设计师的项目
|
|
|
|
|
+ const profileIds = new Set(allMembers.map(m => m.id));
|
|
|
|
|
+ projects = allProjects.filter(p => {
|
|
|
|
|
+ const assignee = p.get('assignee');
|
|
|
|
|
+ return assignee && profileIds.has(assignee.id);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.log('📊 [项目数据加载] 过滤后:', projects.length, '个项目分配给当前设计师');
|
|
|
|
|
+
|
|
|
|
|
+ if (projects.length > 0) {
|
|
|
|
|
+ console.log('📋 [项目数据加载] 项目列表:');
|
|
|
|
|
+ projects.forEach((p, i) => {
|
|
|
|
|
+ const assignee = p.get('assignee');
|
|
|
|
|
+ const title = p.get('title') || p.get('name') || '未命名';
|
|
|
|
|
+ const status = p.get('status');
|
|
|
|
|
+ const currentStage = p.get('currentStage') || p.get('stage');
|
|
|
|
|
+ console.log(` ${i + 1}. ${title} (负责人: ${assignee?.get('name') || '未知'}, 状态: ${status}, 阶段: ${currentStage})`);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (projectQueryError) {
|
|
|
|
|
+ console.error('❌ [项目数据加载] Project 查询失败:', projectQueryError);
|
|
|
|
|
+ console.error('错误详情:', {
|
|
|
|
|
+ message: (projectQueryError as any).message,
|
|
|
|
|
+ code: (projectQueryError as any).code
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 从 ProjectTeam 记录中提取项目
|
|
|
|
|
+ console.log('📋 [项目数据加载] 从 ProjectTeam 记录中提取项目...');
|
|
|
|
|
+ projects = teamRecords
|
|
|
|
|
+ .map(record => record.get('project'))
|
|
|
|
|
+ .filter(p => p != null);
|
|
|
|
|
+
|
|
|
|
|
+ console.log('📊 [项目数据加载] 提取到', projects.length, '个项目');
|
|
|
|
|
+
|
|
|
|
|
+ if (projects.length > 0) {
|
|
|
|
|
+ console.log('📋 [项目数据加载] 项目列表:');
|
|
|
|
|
+ projects.forEach((p, i) => {
|
|
|
|
|
+ const title = p.get('title') || p.get('name') || '未命名';
|
|
|
|
|
+ const status = p.get('status');
|
|
|
|
|
+ const currentStage = p.get('currentStage') || p.get('stage');
|
|
|
|
|
+ console.log(` ${i + 1}. ${title} (状态: ${status}, 阶段: ${currentStage})`);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('\n' + '═'.repeat(80));
|
|
|
|
|
+ console.log('📊 [项目数据加载] 查询阶段完成,开始处理数据...');
|
|
|
|
|
+ console.log('═'.repeat(80) + '\n');
|
|
|
|
|
|
|
|
- // 聚合为 profileId -> 项目数组
|
|
|
|
|
|
|
+ // 🔥 聚合为 profileId -> 项目数组(对齐团队组长端:统计所有项目,不过滤状态)
|
|
|
const profileIdToProjects = new Map<string, any[]>();
|
|
const profileIdToProjects = new Map<string, any[]>();
|
|
|
- for (const row of rows) {
|
|
|
|
|
- const profile = row.get('profile');
|
|
|
|
|
- const project = row.get('project');
|
|
|
|
|
- if (!profile || !project) continue;
|
|
|
|
|
|
|
+ let noAssigneeCount = 0;
|
|
|
|
|
+ let nonMemberRoleCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是从 ProjectTeam 表获取的数据,需要通过 teamRecords 来关联
|
|
|
|
|
+ if (teamRecords.length > 0) {
|
|
|
|
|
+ console.log('📊 [项目数据加载] 使用 ProjectTeam 表数据...');
|
|
|
|
|
+
|
|
|
|
|
+ for (const record of teamRecords) {
|
|
|
|
|
+ const profile = record.get('profile');
|
|
|
|
|
+ const project = record.get('project');
|
|
|
|
|
+
|
|
|
|
|
+ if (!profile || !project) {
|
|
|
|
|
+ noAssigneeCount++;
|
|
|
|
|
+ console.warn('⚠️ [项目数据加载] ProjectTeam 记录缺少 profile 或 project');
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 获取项目信息
|
|
|
|
|
+ const projectStatus = project.get('status');
|
|
|
|
|
+ const currentStage = project.get('currentStage') || project.get('stage');
|
|
|
|
|
+ const projectTitle = project.get('title') || project.get('name') || '未命名';
|
|
|
|
|
+ const profileName = profile.get('name') || '未知';
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`📋 [项目统计] ${projectTitle} (状态: ${projectStatus || '无'}, 阶段: ${currentStage || '无'}, 负责人: ${profileName})`);
|
|
|
|
|
+ console.log(` Profile ID: ${profile.id}, 是否在成员列表: ${allMembers.some(m => m.id === profile.id)}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 对齐团队组长端:不过滤项目状态,统计所有项目
|
|
|
const arr = profileIdToProjects.get(profile.id) || [];
|
|
const arr = profileIdToProjects.get(profile.id) || [];
|
|
|
arr.push(project);
|
|
arr.push(project);
|
|
|
profileIdToProjects.set(profile.id, arr);
|
|
profileIdToProjects.set(profile.id, arr);
|
|
|
|
|
+ console.log(` ✅ 已添加到 profileIdToProjects,当前该成员项目数: ${arr.length}`)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 使用降级方案:从 Project 表获取的数据
|
|
|
|
|
+ console.log('📊 [项目数据加载] 使用 Project 表数据(降级方案)...');
|
|
|
|
|
+
|
|
|
|
|
+ for (const project of projects) {
|
|
|
|
|
+ const assignee = project.get('assignee');
|
|
|
|
|
+
|
|
|
|
|
+ if (!assignee) {
|
|
|
|
|
+ noAssigneeCount++;
|
|
|
|
|
+ console.warn('⚠️ [项目数据加载] 项目缺少 assignee:', {
|
|
|
|
|
+ projectId: project.id,
|
|
|
|
|
+ projectTitle: project.get('title') || project.get('name') || '未命名'
|
|
|
|
|
+ });
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 获取项目信息
|
|
|
|
|
+ const projectStatus = project.get('status');
|
|
|
|
|
+ const currentStage = project.get('currentStage') || project.get('stage');
|
|
|
|
|
+ const projectTitle = project.get('title') || project.get('name') || '未命名';
|
|
|
|
|
+ const assigneeName = assignee.get('name') || '未知';
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`📋 [项目统计] ${projectTitle} (状态: ${projectStatus || '无'}, 阶段: ${currentStage || '无'}, 负责人: ${assigneeName})`);
|
|
|
|
|
+ console.log(` Assignee ID: ${assignee.id}, 是否在成员列表: ${allMembers.some(m => m.id === assignee.id)}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 对齐团队组长端:不过滤项目状态,统计所有项目
|
|
|
|
|
+ // 但只统计组员角色的项目
|
|
|
|
|
+ const assigneeRole = assignee.get('roleName');
|
|
|
|
|
+ if (assigneeRole === '组员') {
|
|
|
|
|
+ const arr = profileIdToProjects.get(assignee.id) || [];
|
|
|
|
|
+ arr.push(project);
|
|
|
|
|
+ profileIdToProjects.set(assignee.id, arr);
|
|
|
|
|
+ console.log(` ✅ 已添加到 profileIdToProjects,当前该成员项目数: ${arr.length}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ nonMemberRoleCount++;
|
|
|
|
|
+ console.log(` ⏭️ 跳过非组员角色的项目 (角色: ${assigneeRole || '未知'})`)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ console.log('📊 [项目数据加载] 统计结果:', {
|
|
|
|
|
+ '数据源': teamRecords.length > 0 ? 'ProjectTeam 表' : 'Project 表(降级)',
|
|
|
|
|
+ '总记录数': teamRecords.length > 0 ? teamRecords.length : projects.length,
|
|
|
|
|
+ '缺少assignee': noAssigneeCount,
|
|
|
|
|
+ '非组员角色': nonMemberRoleCount,
|
|
|
|
|
+ '有效项目数': Array.from(profileIdToProjects.values()).reduce((sum, arr) => sum + arr.length, 0)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.log('📋 [项目数据加载] 成员项目分布(所有项目):',
|
|
|
|
|
+ Array.from(profileIdToProjects.entries()).map(([id, projects]) => {
|
|
|
|
|
+ const member = allMembers.find(m => m.id === id);
|
|
|
|
|
+ return {
|
|
|
|
|
+ name: member?.name || id,
|
|
|
|
|
+ projectCount: projects.length
|
|
|
|
|
+ };
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
// 填充到成员信息
|
|
// 填充到成员信息
|
|
|
|
|
+ console.log('\n' + '═'.repeat(80));
|
|
|
|
|
+ console.log('%c📝 [项目数据加载] 开始填充成员项目数据...', 'background: #FF5722; color: white; font-size: 16px; padding: 5px;');
|
|
|
|
|
+ console.log('═'.repeat(80) + '\n');
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 DEBUG: 输出完整的 profileIdToProjects Map
|
|
|
|
|
+ console.log('%c🔍 [DEBUG] profileIdToProjects 完整内容:', 'background: #E91E63; color: white; font-size: 14px; padding: 5px;');
|
|
|
|
|
+ console.table(Array.from(profileIdToProjects.entries()).map(([profileId, projs]) => {
|
|
|
|
|
+ const member = allMembers.find(m => m.id === profileId);
|
|
|
|
|
+ return {
|
|
|
|
|
+ '成员名称': member?.name || '未知',
|
|
|
|
|
+ 'Profile ID': profileId,
|
|
|
|
|
+ '项目数量': projs.length,
|
|
|
|
|
+ '项目列表': projs.map(p => p.get('title') || p.get('name')).join(', ')
|
|
|
|
|
+ };
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
for (const member of allMembers) {
|
|
for (const member of allMembers) {
|
|
|
const projects = profileIdToProjects.get(member.id) || [];
|
|
const projects = profileIdToProjects.get(member.id) || [];
|
|
|
- const activeProjects = projects.filter((p: any) => p.get('isDeleted') !== true);
|
|
|
|
|
- member.currentProjects = activeProjects.length;
|
|
|
|
|
|
|
+ console.log(`\n🔍 [${member.name}] Member ID: ${member.id}`);
|
|
|
|
|
+ console.log(` 从 profileIdToProjects 获取到 ${projects.length} 个项目`);
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 DEBUG: 详细输出每个项目
|
|
|
|
|
+ if (projects.length > 0) {
|
|
|
|
|
+ console.log(` 📋 项目详情:`);
|
|
|
|
|
+ projects.forEach((p, i) => {
|
|
|
|
|
+ console.log(` ${i + 1}. ${p.get('title') || p.get('name')} (状态: ${p.get('status')}, 阶段: ${p.get('currentStage')})`);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log(` ⚠️ 没有找到项目!可能原因:`);
|
|
|
|
|
+ console.log(` 1. ProjectTeam 表中没有此成员的记录`);
|
|
|
|
|
+ console.log(` 2. Project 表的 assignee 字段未指向此成员`);
|
|
|
|
|
+ console.log(` 3. 所有项目都被状态过滤器过滤掉了`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 对齐团队组长端:统计所有项目数量,不过滤状态
|
|
|
|
|
+ member.currentProjects = projects.length;
|
|
|
|
|
+ console.log(` ✅ 已设置 currentProjects(所有项目) = ${member.currentProjects}`);
|
|
|
|
|
|
|
|
- // 粗略计算工作量(可按实际规则替换)
|
|
|
|
|
- member.workload = Math.min(100, member.currentProjects * 25);
|
|
|
|
|
- member.status = member.currentProjects >= 3 ? 'busy' : (member.currentProjects === 0 ? 'idle' : 'reviewing');
|
|
|
|
|
|
|
+ // 🔥 根据项目数量计算工作量和状态
|
|
|
|
|
+ member.workload = Math.min(100, member.currentProjects * 20);
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 计算闲置天数(如果有项目,闲置天数为0;如果没有项目,计算最后接单日期)
|
|
|
|
|
+ if (member.currentProjects > 0) {
|
|
|
|
|
+ member.idleDays = 0;
|
|
|
|
|
+ // 近30天接单数(以项目创建时间近30天为准)
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
|
|
|
|
|
+ const recentProjects = projects.filter(p => {
|
|
|
|
|
+ const created = p.get?.('createdAt') ?? p.createdAt;
|
|
|
|
|
+ if (!created) return false;
|
|
|
|
|
+ const ts = (created instanceof Date) ? created.getTime() : new Date(created).getTime();
|
|
|
|
|
+ return !isNaN(ts) && (now.getTime() - ts) <= THIRTY_DAYS;
|
|
|
|
|
+ });
|
|
|
|
|
+ member.recentOrders = recentProjects.length;
|
|
|
|
|
+
|
|
|
|
|
+ // 找到最近的项目创建日期
|
|
|
|
|
+ const recentProject = projects.reduce((latest: any, p: any) => {
|
|
|
|
|
+ const pCreatedAt = p.get('createdAt') || p.createdAt;
|
|
|
|
|
+ const latestCreatedAt = latest?.get?.('createdAt') || latest?.createdAt;
|
|
|
|
|
+ if (!latest || (pCreatedAt && (!latestCreatedAt || pCreatedAt > latestCreatedAt))) {
|
|
|
|
|
+ return p;
|
|
|
|
|
+ }
|
|
|
|
|
+ return latest;
|
|
|
|
|
+ }, null);
|
|
|
|
|
+
|
|
|
|
|
+ if (recentProject) {
|
|
|
|
|
+ const lastDate = recentProject.get('createdAt') || recentProject.createdAt;
|
|
|
|
|
+ if (lastDate) {
|
|
|
|
|
+ member.lastOrderDate = this.formatDateString(lastDate);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 没有项目,设置为空闲状态
|
|
|
|
|
+ member.recentOrders = 0;
|
|
|
|
|
+ // 如果有 lastOrderDate,计算闲置天数
|
|
|
|
|
+ if (member.lastOrderDate) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const lastDate = new Date(member.lastOrderDate);
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const diffTime = Math.abs(now.getTime() - lastDate.getTime());
|
|
|
|
|
+ member.idleDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ member.idleDays = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果没有历史记录,默认闲置0天
|
|
|
|
|
+ member.idleDays = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 根据项目数量设置状态(按用户需求)
|
|
|
|
|
+ // 0个项目 = 空闲(idle) → 绿色
|
|
|
|
|
+ // 1-5个项目 = 有项目(reviewing) → 橙色
|
|
|
|
|
+ // 超过5个项目 = 繁忙(stagnant) → 红色
|
|
|
|
|
+ if (member.currentProjects === 0) {
|
|
|
|
|
+ member.status = 'idle'; // 绿色 - 空闲
|
|
|
|
|
+ } else if (member.currentProjects >= 1 && member.currentProjects <= 5) {
|
|
|
|
|
+ member.status = 'reviewing'; // 橙色 - 有项目
|
|
|
|
|
+ } else {
|
|
|
|
|
+ member.status = 'stagnant'; // 红色 - 繁忙(超过5个项目)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 构建对图日期与日历事件
|
|
// 构建对图日期与日历事件
|
|
|
const projectEvents: Designer['projectEvents'] = [];
|
|
const projectEvents: Designer['projectEvents'] = [];
|
|
|
const reviewDates: string[] = [];
|
|
const reviewDates: string[] = [];
|
|
|
|
|
|
|
|
- for (const p of activeProjects) {
|
|
|
|
|
- const title = p.get('title') || '未命名项目';
|
|
|
|
|
|
|
+ for (const p of projects) {
|
|
|
|
|
+ const title = p.get('title') || p.get('name') || '未命名项目';
|
|
|
const projectId = p.id;
|
|
const projectId = p.id;
|
|
|
const demoday = p.get('demoday');
|
|
const demoday = p.get('demoday');
|
|
|
const deadline = p.get('deadline');
|
|
const deadline = p.get('deadline');
|
|
@@ -514,14 +839,57 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
|
|
|
|
|
member.reviewDates = reviewDates;
|
|
member.reviewDates = reviewDates;
|
|
|
member.projectEvents = projectEvents;
|
|
member.projectEvents = projectEvents;
|
|
|
|
|
+
|
|
|
|
|
+ // 根据状态显示不同的图标和颜色
|
|
|
|
|
+ const statusIcon = member.status === 'idle' ? '🟢' : member.status === 'reviewing' ? '🟠' : '🔴';
|
|
|
|
|
+ const statusText = member.status === 'idle' ? '空闲' : member.status === 'reviewing' ? '有项目' : '繁忙';
|
|
|
|
|
+ console.log(`${statusIcon} [${member.name}] ${member.currentProjects}个项目 - ${statusText} (工作量:${member.workload}%, 闲置:${member.idleDays}天)`);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ console.log('✅ [项目数据加载] 所有成员项目数据加载完成');
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 更新缓存
|
|
|
|
|
+ this.lastCacheTime = Date.now();
|
|
|
|
|
+ this.projectDataCache.clear();
|
|
|
|
|
+ allMembers.forEach(member => {
|
|
|
|
|
+ this.projectDataCache.set(member.id, {
|
|
|
|
|
+ currentProjects: member.currentProjects,
|
|
|
|
|
+ workload: member.workload,
|
|
|
|
|
+ status: member.status,
|
|
|
|
|
+ idleDays: member.idleDays,
|
|
|
|
|
+ recentOrders: member.recentOrders,
|
|
|
|
|
+ lastOrderDate: member.lastOrderDate,
|
|
|
|
|
+ projectEvents: member.projectEvents
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ console.log('%c💾 [项目数据加载] 已更新缓存,有效期30秒', 'background: #9C27B0; color: white; font-size: 14px; padding: 5px;');
|
|
|
|
|
|
|
|
this.cdr.markForCheck();
|
|
this.cdr.markForCheck();
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- console.error('为成员加载项目分配失败:', err);
|
|
|
|
|
|
|
+ console.error('❌ [项目数据加载] 加载项目分配失败:', err);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 🔥 从缓存应用项目数据(快速加载)
|
|
|
|
|
+ */
|
|
|
|
|
+ private applyProjectDataFromCache(allMembers: Designer[]): void {
|
|
|
|
|
+ allMembers.forEach(member => {
|
|
|
|
|
+ const cached = this.projectDataCache.get(member.id);
|
|
|
|
|
+ if (cached) {
|
|
|
|
|
+ member.currentProjects = cached.currentProjects;
|
|
|
|
|
+ member.workload = cached.workload;
|
|
|
|
|
+ member.status = cached.status;
|
|
|
|
|
+ member.idleDays = cached.idleDays;
|
|
|
|
|
+ member.recentOrders = cached.recentOrders;
|
|
|
|
|
+ member.lastOrderDate = cached.lastOrderDate;
|
|
|
|
|
+ member.projectEvents = cached.projectEvents;
|
|
|
|
|
+ console.log(` ✓ ${member.name}: ${member.currentProjects}个项目 (来自缓存)`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ this.cdr.markForCheck();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private formatDateString(date: Date): string {
|
|
private formatDateString(date: Date): string {
|
|
|
const d = new Date(date);
|
|
const d = new Date(date);
|
|
|
const y = d.getFullYear();
|
|
const y = d.getFullYear();
|
|
@@ -530,6 +898,26 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
return `${y}-${m}-${dd}`;
|
|
return `${y}-${m}-${dd}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 统一的活跃项目判定逻辑,供成员列表与员工详情共用
|
|
|
|
|
+ private isActiveProject(proj: any): boolean {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const statusRaw = (proj?.get?.('status') ?? proj?.status ?? '').toString().trim();
|
|
|
|
|
+ const stageRaw = (proj?.get?.('currentStage') ?? proj?.get?.('stage') ?? proj?.currentStage ?? proj?.stage ?? '').toString().trim();
|
|
|
|
|
+ const status = statusRaw.toLowerCase();
|
|
|
|
|
+ const stage = stageRaw.toLowerCase();
|
|
|
|
|
+ const allowedStatuses = new Set(['进行中','待分配','pendingassignment','pendingapproval']);
|
|
|
|
|
+ const excludedStatuses = new Set(['已完成','已交付','已取消','已归档','归档','archived','cancelled','canceled','done','delivered']);
|
|
|
|
|
+ const excludedStages = new Set(['delivery','已完成']);
|
|
|
|
|
+ if (excludedStatuses.has(statusRaw) || excludedStatuses.has(status)) return false;
|
|
|
|
|
+ if (excludedStages.has(stageRaw) || excludedStages.has(stage)) return false;
|
|
|
|
|
+ const progressStages = new Set(['requirement','planning','modeling','rendering','postproduction','review','revision','方案深化','需求确认','建模','渲染','施工图','复核','返修']);
|
|
|
|
|
+ if (!status && stage && progressStages.has(stage)) return true;
|
|
|
|
|
+ return allowedStatuses.has(statusRaw) || allowedStatuses.has(status);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 获取设计师状态
|
|
* 获取设计师状态
|
|
|
*/
|
|
*/
|
|
@@ -688,24 +1076,24 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
this.internalCrossTeamCollaborators.some(d => d.id === designer.id);
|
|
this.internalCrossTeamCollaborators.some(d => d.id === designer.id);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 获取设计师状态颜色
|
|
|
|
|
|
|
+ // 🔥 获取设计师状态颜色(根据项目数量)
|
|
|
getDesignerStatusColor(status: string): string {
|
|
getDesignerStatusColor(status: string): string {
|
|
|
switch (status) {
|
|
switch (status) {
|
|
|
- case 'idle': return '#52c41a';
|
|
|
|
|
- case 'busy': return '#faad14';
|
|
|
|
|
- case 'reviewing': return '#1890ff';
|
|
|
|
|
- case 'stagnant': return '#ff4d4f';
|
|
|
|
|
|
|
+ case 'idle': return '#52c41a'; // 绿色:0个项目(空闲)
|
|
|
|
|
+ case 'reviewing': return '#faad14'; // 橙色:1-5个项目(有项目)
|
|
|
|
|
+ case 'stagnant': return '#ff4d4f'; // 红色:超过5个项目(繁忙)
|
|
|
|
|
+ case 'busy': return '#faad14'; // 橙色:兼容旧状态
|
|
|
default: return '#d9d9d9';
|
|
default: return '#d9d9d9';
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 获取设计师状态文本
|
|
|
|
|
|
|
+ // 🔥 获取设计师状态文本(根据项目数量)
|
|
|
getDesignerStatusText(status: string): string {
|
|
getDesignerStatusText(status: string): string {
|
|
|
switch (status) {
|
|
switch (status) {
|
|
|
- case 'idle': return '空闲';
|
|
|
|
|
- case 'busy': return '忙碌';
|
|
|
|
|
- case 'reviewing': return '对图中';
|
|
|
|
|
- case 'stagnant': return '停滞期';
|
|
|
|
|
|
|
+ case 'idle': return '空闲'; // 0个项目
|
|
|
|
|
+ case 'reviewing': return '有项目'; // 1-5个项目
|
|
|
|
|
+ case 'stagnant': return '繁忙'; // 超过5个项目
|
|
|
|
|
+ case 'busy': return '有项目'; // 兼容旧状态
|
|
|
default: return '未知';
|
|
default: return '未知';
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -923,43 +1311,147 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
* 显示设计师详情面板(复用组长端员工详情面板)
|
|
* 显示设计师详情面板(复用组长端员工详情面板)
|
|
|
*/
|
|
*/
|
|
|
async showDesignerEmployeeDetail(designer: Designer): Promise<void> {
|
|
async showDesignerEmployeeDetail(designer: Designer): Promise<void> {
|
|
|
- // 查询该设计师的项目数据
|
|
|
|
|
|
|
+ // 🔥 参考组长端 dashboard.ts,优先使用 ProjectTeam 表,降级使用 Project 表
|
|
|
const Profile = Parse.Object.extend('Profile');
|
|
const Profile = Parse.Object.extend('Profile');
|
|
|
const profilePointer = new Profile();
|
|
const profilePointer = new Profile();
|
|
|
profilePointer.id = designer.id;
|
|
profilePointer.id = designer.id;
|
|
|
|
|
|
|
|
- const ptQuery = new Parse.Query('ProjectTeam');
|
|
|
|
|
- ptQuery.equalTo('profile', profilePointer);
|
|
|
|
|
- ptQuery.notEqualTo('isDeleted', true);
|
|
|
|
|
- ptQuery.include('project');
|
|
|
|
|
- ptQuery.limit(100);
|
|
|
|
|
|
|
+ const companyId = localStorage.getItem('company');
|
|
|
|
|
+
|
|
|
|
|
+ // 方案1:尝试从 ProjectTeam 表查询
|
|
|
|
|
+ const companyProjectQuery = new Parse.Query('Project');
|
|
|
|
|
+ companyProjectQuery.equalTo('company', companyId);
|
|
|
|
|
+ companyProjectQuery.notEqualTo('isDeleted', true);
|
|
|
|
|
+
|
|
|
|
|
+ const teamQuery = new Parse.Query('ProjectTeam');
|
|
|
|
|
+ teamQuery.matchesQuery('project', companyProjectQuery);
|
|
|
|
|
+ teamQuery.equalTo('profile', profilePointer);
|
|
|
|
|
+ teamQuery.notEqualTo('isDeleted', true);
|
|
|
|
|
+ teamQuery.include('project');
|
|
|
|
|
+ teamQuery.include('profile');
|
|
|
|
|
+ teamQuery.limit(100);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const rows = await ptQuery.find();
|
|
|
|
|
- const projects = rows.map(r => r.get('project')).filter(p => p && p.get('isDeleted') !== true);
|
|
|
|
|
|
|
+ let allProjects: any[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ const teamRecords = await teamQuery.find();
|
|
|
|
|
+ console.log(`📊 [员工详情] ${designer.name} ProjectTeam 查询到 ${teamRecords.length} 条记录`);
|
|
|
|
|
+
|
|
|
|
|
+ if (teamRecords.length > 0) {
|
|
|
|
|
+ // 从 ProjectTeam 记录中提取项目
|
|
|
|
|
+ allProjects = teamRecords
|
|
|
|
|
+ .map(record => record.get('project'))
|
|
|
|
|
+ .filter(p => p != null);
|
|
|
|
|
+ console.log(`📊 [员工详情] ${designer.name} 从 ProjectTeam 提取到 ${allProjects.length} 个项目`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 降级方案:直接查询 Project 表
|
|
|
|
|
+ console.log(`📊 [员工详情] ${designer.name} ProjectTeam 无数据,使用降级方案查询 Project 表`);
|
|
|
|
|
+ const projectQuery = new Parse.Query('Project');
|
|
|
|
|
+ projectQuery.equalTo('company', companyId);
|
|
|
|
|
+ projectQuery.equalTo('assignee', profilePointer);
|
|
|
|
|
+ projectQuery.equalTo('isDeleted', false);
|
|
|
|
|
+ projectQuery.include('assignee');
|
|
|
|
|
+ projectQuery.limit(100);
|
|
|
|
|
+
|
|
|
|
|
+ allProjects = await projectQuery.find();
|
|
|
|
|
+ console.log(`📊 [员工详情] ${designer.name} Project 表查询到 ${allProjects.length} 个项目`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 🔥 对齐团队组长端:不过滤项目状态,统计所有项目
|
|
|
|
|
+ const projects = allProjects;
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`📊 [员工详情] ${designer.name} 的项目总数:`, projects.length);
|
|
|
|
|
+
|
|
|
|
|
+ // 输出每个项目的状态和阶段
|
|
|
|
|
+ projects.forEach((p: any) => {
|
|
|
|
|
+ const projectStatus = p.get('status');
|
|
|
|
|
+ const currentStage = p.get('currentStage') || p.get('stage');
|
|
|
|
|
+ const projectTitle = p.get('title') || p.get('name') || '未命名';
|
|
|
|
|
+ console.log(` - ${projectTitle} (状态: ${projectStatus || '无'}, 阶段: ${currentStage || '无'})`);
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
// 构建项目数据
|
|
// 构建项目数据
|
|
|
const projectData = projects.map((p: any) => ({
|
|
const projectData = projects.map((p: any) => ({
|
|
|
id: p.id,
|
|
id: p.id,
|
|
|
- name: p.get('title') || '未命名项目'
|
|
|
|
|
|
|
+ name: p.get('title') || p.get('name') || '未命名项目'
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
|
// 构建日历数据(当月)
|
|
// 构建日历数据(当月)
|
|
|
|
|
+ console.log(`📅 [员工详情] 开始构建 ${designer.name} 的日历数据,项目数: ${projects.length}`);
|
|
|
const calendarData = this.buildEmployeeCalendarData(projects);
|
|
const calendarData = this.buildEmployeeCalendarData(projects);
|
|
|
|
|
+ console.log(`📅 [员工详情] 日历数据构建完成,天数: ${calendarData.days.length}`);
|
|
|
|
|
+ console.log(`📅 [员工详情] 有项目的日期数:`, calendarData.days.filter(d => d.projectCount > 0).length);
|
|
|
|
|
+
|
|
|
|
|
+ // === 加载员工能力问卷(Profile & SurveyLog) ===
|
|
|
|
|
+ let surveyCompleted = false;
|
|
|
|
|
+ let surveyData: any = null;
|
|
|
|
|
+ let profileForSurvey: any = null;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 优先按 ID 查询 Profile
|
|
|
|
|
+ const profileByIdQuery = new Parse.Query('Profile');
|
|
|
|
|
+ profileByIdQuery.equalTo('objectId', designer.id);
|
|
|
|
|
+ profileByIdQuery.limit(1);
|
|
|
|
|
+ profileForSurvey = await profileByIdQuery.first();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果按ID未找到,则按姓名(realname/name)兜底
|
|
|
|
|
+ if (!profileForSurvey) {
|
|
|
|
|
+ const realnameQuery = new Parse.Query('Profile');
|
|
|
|
|
+ realnameQuery.equalTo('realname', designer.name);
|
|
|
|
|
+
|
|
|
|
|
+ const nameQuery = new Parse.Query('Profile');
|
|
|
|
|
+ nameQuery.equalTo('name', designer.name);
|
|
|
|
|
+
|
|
|
|
|
+ const orQuery = Parse.Query.or(realnameQuery, nameQuery);
|
|
|
|
|
+ orQuery.limit(1);
|
|
|
|
|
+ const results = await orQuery.find();
|
|
|
|
|
+ if (results.length > 0) {
|
|
|
|
|
+ profileForSurvey = results[0];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 映射为 EmployeeDetail 格式
|
|
|
|
|
|
|
+ if (profileForSurvey) {
|
|
|
|
|
+ surveyCompleted = profileForSurvey.get('surveyCompleted') || false;
|
|
|
|
|
+
|
|
|
|
|
+ if (surveyCompleted) {
|
|
|
|
|
+ const surveyQuery = new Parse.Query('SurveyLog');
|
|
|
|
|
+ surveyQuery.equalTo('profile', profileForSurvey.toPointer());
|
|
|
|
|
+ surveyQuery.equalTo('type', 'survey-profile');
|
|
|
|
|
+ surveyQuery.descending('createdAt');
|
|
|
|
|
+ surveyQuery.limit(1);
|
|
|
|
|
+
|
|
|
|
|
+ const surveyResults = await surveyQuery.find();
|
|
|
|
|
+ if (surveyResults.length > 0) {
|
|
|
|
|
+ const survey = surveyResults[0];
|
|
|
|
|
+ surveyData = {
|
|
|
|
|
+ answers: survey.get('answers') || [],
|
|
|
|
|
+ createdAt: survey.get('createdAt'),
|
|
|
|
|
+ updatedAt: survey.get('updatedAt')
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn(`⚠️ 未找到设计师的 Profile:id=${designer.id} name=${designer.name}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('❌ 加载员工问卷数据失败:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 映射为 EmployeeDetail 格式(包含问卷字段)
|
|
|
this.employeeDetailData = {
|
|
this.employeeDetailData = {
|
|
|
name: designer.name,
|
|
name: designer.name,
|
|
|
currentProjects: projects.length,
|
|
currentProjects: projects.length,
|
|
|
projectNames: projectData.map(p => p.name),
|
|
projectNames: projectData.map(p => p.name),
|
|
|
projectData,
|
|
projectData,
|
|
|
leaveRecords: [], // 暂无请假数据
|
|
leaveRecords: [], // 暂无请假数据
|
|
|
- redMarkExplanation: designer.status === 'busy' ? '当前工作量较高,建议谨慎分配新项目' :
|
|
|
|
|
- designer.status === 'stagnant' ? '处于停滞期项目,需要跟进' :
|
|
|
|
|
- designer.idleDays >= 10 ? `已闲置 ${designer.idleDays} 天,优先推荐分配` : '工作状态正常',
|
|
|
|
|
|
|
+ redMarkExplanation: designer.status === 'stagnant' ? `当前负责 ${designer.currentProjects} 个项目,工作量饱和,不建议分配新项目` :
|
|
|
|
|
+ designer.status === 'reviewing' ? `当前负责 ${designer.currentProjects} 个项目,工作量适中` :
|
|
|
|
|
+ designer.status === 'idle' && designer.idleDays >= 10 ? `已闲置 ${designer.idleDays} 天,优先推荐分配` :
|
|
|
|
|
+ designer.status === 'idle' ? '空闲状态,可以分配新项目' : '工作状态正常',
|
|
|
calendarData,
|
|
calendarData,
|
|
|
- profileId: designer.id,
|
|
|
|
|
- surveyCompleted: false // 暂无问卷数据
|
|
|
|
|
|
|
+ profileId: profileForSurvey?.id || designer.id,
|
|
|
|
|
+ surveyCompleted,
|
|
|
|
|
+ surveyData
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
this.showEmployeeDetailPanel = true;
|
|
this.showEmployeeDetailPanel = true;
|
|
@@ -971,68 +1463,168 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 构建员工日历数据(当月视图)
|
|
|
|
|
|
|
+ * 刷新员工问卷(仅刷新问卷相关字段)
|
|
|
*/
|
|
*/
|
|
|
- private buildEmployeeCalendarData(projects: any[]): EmployeeCalendarData {
|
|
|
|
|
- const currentMonth = new Date();
|
|
|
|
|
- currentMonth.setDate(1);
|
|
|
|
|
- currentMonth.setHours(0, 0, 0, 0);
|
|
|
|
|
|
|
+ async refreshEmployeeSurvey(): Promise<void> {
|
|
|
|
|
+ if (!this.employeeDetailData) return;
|
|
|
|
|
|
|
|
- // 生成当月的所有日期(包含上月末和下月初补齐)
|
|
|
|
|
- const firstDayOfMonth = currentMonth.getDay();
|
|
|
|
|
- const daysFromPrevMonth = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ const profileId = this.employeeDetailData.profileId || '';
|
|
|
|
|
+ let profileForSurvey: any = null;
|
|
|
|
|
+
|
|
|
|
|
+ if (profileId) {
|
|
|
|
|
+ const profileByIdQuery = new Parse.Query('Profile');
|
|
|
|
|
+ profileByIdQuery.equalTo('objectId', profileId);
|
|
|
|
|
+ profileByIdQuery.limit(1);
|
|
|
|
|
+ profileForSurvey = await profileByIdQuery.first();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果按ID未找到,则按姓名(realname/name)兜底
|
|
|
|
|
+ if (!profileForSurvey) {
|
|
|
|
|
+ const realnameQuery = new Parse.Query('Profile');
|
|
|
|
|
+ realnameQuery.equalTo('realname', this.employeeDetailData.name);
|
|
|
|
|
|
|
|
- const startDate = new Date(currentMonth);
|
|
|
|
|
- startDate.setDate(1 - daysFromPrevMonth);
|
|
|
|
|
|
|
+ const nameQuery = new Parse.Query('Profile');
|
|
|
|
|
+ nameQuery.equalTo('name', this.employeeDetailData.name);
|
|
|
|
|
+
|
|
|
|
|
+ const orQuery = Parse.Query.or(realnameQuery, nameQuery);
|
|
|
|
|
+ orQuery.limit(1);
|
|
|
|
|
+ const results = await orQuery.find();
|
|
|
|
|
+ if (results.length > 0) {
|
|
|
|
|
+ profileForSurvey = results[0];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const days: EmployeeCalendarDay[] = [];
|
|
|
|
|
- const dateToProjects = new Map<string, Array<{ id: string; name: string; deadline?: Date }>>();
|
|
|
|
|
|
|
+ let surveyCompleted = false;
|
|
|
|
|
+ let surveyData: any = null;
|
|
|
|
|
+
|
|
|
|
|
+ if (profileForSurvey) {
|
|
|
|
|
+ surveyCompleted = profileForSurvey.get('surveyCompleted') || false;
|
|
|
|
|
+ if (surveyCompleted) {
|
|
|
|
|
+ const surveyQuery = new Parse.Query('SurveyLog');
|
|
|
|
|
+ surveyQuery.equalTo('profile', profileForSurvey.toPointer());
|
|
|
|
|
+ surveyQuery.equalTo('type', 'survey-profile');
|
|
|
|
|
+ surveyQuery.descending('createdAt');
|
|
|
|
|
+ surveyQuery.limit(1);
|
|
|
|
|
+ const surveyResults = await surveyQuery.find();
|
|
|
|
|
+ if (surveyResults.length > 0) {
|
|
|
|
|
+ const survey = surveyResults[0];
|
|
|
|
|
+ surveyData = {
|
|
|
|
|
+ answers: survey.get('answers') || [],
|
|
|
|
|
+ createdAt: survey.get('createdAt'),
|
|
|
|
|
+ updatedAt: survey.get('updatedAt')
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 映射项目到日期
|
|
|
|
|
- for (const p of projects) {
|
|
|
|
|
- const demoday = p.get('demoday');
|
|
|
|
|
- const deadline = p.get('deadline');
|
|
|
|
|
- const projectInfo = {
|
|
|
|
|
- id: p.id,
|
|
|
|
|
- name: p.get('title') || '未命名项目',
|
|
|
|
|
- deadline: deadline ? new Date(deadline) : undefined
|
|
|
|
|
|
|
+ // 更新当前显示的员工详情中的问卷字段
|
|
|
|
|
+ this.employeeDetailData = {
|
|
|
|
|
+ ...this.employeeDetailData,
|
|
|
|
|
+ profileId: profileForSurvey?.id || this.employeeDetailData.profileId,
|
|
|
|
|
+ surveyCompleted,
|
|
|
|
|
+ surveyData
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- if (demoday) {
|
|
|
|
|
- const key = this.formatDateString(new Date(demoday));
|
|
|
|
|
- const arr = dateToProjects.get(key) || [];
|
|
|
|
|
- arr.push(projectInfo);
|
|
|
|
|
- dateToProjects.set(key, arr);
|
|
|
|
|
- }
|
|
|
|
|
- if (deadline) {
|
|
|
|
|
- const key = this.formatDateString(new Date(deadline));
|
|
|
|
|
- const arr = dateToProjects.get(key) || [];
|
|
|
|
|
- if (!arr.some(x => x.id === projectInfo.id)) {
|
|
|
|
|
- arr.push(projectInfo);
|
|
|
|
|
- }
|
|
|
|
|
- dateToProjects.set(key, arr);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ this.cdr.markForCheck();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('刷新员工问卷失败:', error);
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 生成42天(6周)
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建员工日历数据(当月视图)
|
|
|
|
|
+ * 🔥 完全参考组长端 dashboard.ts 的 generateEmployeeCalendar 方法
|
|
|
|
|
+ */
|
|
|
|
|
+ private buildEmployeeCalendarData(projects: any[]): EmployeeCalendarData {
|
|
|
|
|
+ console.log(`\n📅 [日历构建] 收到 ${projects.length} 个项目`);
|
|
|
|
|
+
|
|
|
|
|
+ const currentMonth = new Date();
|
|
|
|
|
+ const year = currentMonth.getFullYear();
|
|
|
|
|
+ const month = currentMonth.getMonth();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当月天数
|
|
|
|
|
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
|
|
|
+ const days: EmployeeCalendarDay[] = [];
|
|
|
const today = new Date();
|
|
const today = new Date();
|
|
|
today.setHours(0, 0, 0, 0);
|
|
today.setHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
- for (let i = 0; i < 42; i++) {
|
|
|
|
|
- const date = new Date(startDate);
|
|
|
|
|
- date.setDate(startDate.getDate() + i);
|
|
|
|
|
|
|
+ console.log(`📅 [日历构建] 当前月份: ${year}年${month + 1}月,天数: ${daysInMonth}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 生成当月每一天的数据
|
|
|
|
|
+ for (let day = 1; day <= daysInMonth; day++) {
|
|
|
|
|
+ const date = new Date(year, month, day);
|
|
|
|
|
+ const dateStr = date.toISOString().split('T')[0];
|
|
|
|
|
|
|
|
- const dateKey = this.formatDateString(date);
|
|
|
|
|
- const projectsOnDate = dateToProjects.get(dateKey) || [];
|
|
|
|
|
|
|
+ // 找出该日期相关的项目(项目进行中且在当天范围内)
|
|
|
|
|
+ const dayProjects = projects.filter(p => {
|
|
|
|
|
+ // 处理 Parse Date 对象:检查是否有 toDate 方法
|
|
|
|
|
+ const getDate = (dateValue: any) => {
|
|
|
|
|
+ if (!dateValue) return null;
|
|
|
|
|
+ if (dateValue.toDate && typeof dateValue.toDate === 'function') {
|
|
|
|
|
+ return dateValue.toDate(); // Parse Date对象
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dateValue instanceof Date) {
|
|
|
|
|
+ return dateValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ return new Date(dateValue); // 字符串或时间戳
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const deadlineDate = getDate(p.get('deadline'));
|
|
|
|
|
+ const createdDate = p.get('createdAt') ? getDate(p.get('createdAt')) : (p.createdAt ? getDate(p.createdAt) : null);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果项目既没有 deadline 也没有 createdAt,则跳过
|
|
|
|
|
+ if (!deadlineDate && !createdDate) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 智能处理日期范围
|
|
|
|
|
+ let startDate: Date;
|
|
|
|
|
+ let endDate: Date;
|
|
|
|
|
+
|
|
|
|
|
+ if (deadlineDate && createdDate) {
|
|
|
|
|
+ // 情况1:两个日期都有
|
|
|
|
|
+ startDate = createdDate;
|
|
|
|
|
+ endDate = deadlineDate;
|
|
|
|
|
+ } else if (deadlineDate) {
|
|
|
|
|
+ // 情况2:只有deadline,往前推30天
|
|
|
|
|
+ startDate = new Date(deadlineDate.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
|
|
|
+ endDate = deadlineDate;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 情况3:只有createdAt,往后推30天
|
|
|
|
|
+ startDate = createdDate!;
|
|
|
|
|
+ endDate = new Date(createdDate!.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ startDate.setHours(0, 0, 0, 0);
|
|
|
|
|
+ endDate.setHours(0, 0, 0, 0);
|
|
|
|
|
+
|
|
|
|
|
+ const inRange = date >= startDate && date <= endDate;
|
|
|
|
|
+
|
|
|
|
|
+ return inRange;
|
|
|
|
|
+ }).map(p => ({
|
|
|
|
|
+ id: p.id,
|
|
|
|
|
+ name: p.get('title') || p.get('name') || '未命名项目',
|
|
|
|
|
+ deadline: p.get('deadline') ? new Date(p.get('deadline')) : undefined
|
|
|
|
|
+ }));
|
|
|
|
|
|
|
|
days.push({
|
|
days.push({
|
|
|
date,
|
|
date,
|
|
|
- projectCount: projectsOnDate.length,
|
|
|
|
|
- projects: projectsOnDate,
|
|
|
|
|
|
|
+ projectCount: dayProjects.length,
|
|
|
|
|
+ projects: dayProjects,
|
|
|
isToday: date.getTime() === today.getTime(),
|
|
isToday: date.getTime() === today.getTime(),
|
|
|
- isCurrentMonth: date.getMonth() === currentMonth.getMonth()
|
|
|
|
|
|
|
+ isCurrentMonth: true // 当月的所有天都是当月
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+ // 如果这一天有项目,输出详细信息
|
|
|
|
|
+ if (dayProjects.length > 0) {
|
|
|
|
|
+ console.log(` ${year}-${(month + 1).toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}: ${dayProjects.length}个项目`,
|
|
|
|
|
+ dayProjects.map(p => p.name).join(', '));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const daysWithProjects = days.filter(d => d.projectCount > 0).length;
|
|
|
|
|
+ console.log(`\n📅 [日历构建] 完成!总天数: ${days.length}, 有项目的天数: ${daysWithProjects}`);
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
currentMonth,
|
|
currentMonth,
|