import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, ChangeDetectorRef } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; import { DesignerCalendarComponent, Designer as CalendarDesigner } from '../../customer-service/consultation-order/components/designer-calendar/designer-calendar.component'; import { ProjectProgressModalComponent } from '../project-timeline/project-progress-modal'; import { ProjectSpaceDeliverableService, ProjectSpaceDeliverableSummary } from '../../../../modules/project/services/project-space-deliverable.service'; import { normalizeDateInput, addDays } from '../../../utils/date-utils'; // 员工详情面板数据接口 export interface EmployeeDetail { name: string; currentProjects: number; // 当前负责项目数 projectNames: string[]; // 项目名称列表(用于显示) projectData: Array<{ id: string; name: string }>; // 项目数据(包含ID和名称,用于跳转) leaveRecords: LeaveRecord[]; // 未来7天请假记录 redMarkExplanation: string; // 红色标记说明 calendarData?: EmployeeCalendarData; // 负载日历数据 // 问卷相关 surveyCompleted?: boolean; // 是否完成问卷 surveyData?: any; // 问卷答案数据 profileId?: string; // Profile ID } // 请假记录接口 export interface LeaveRecord { id: string; employeeName: string; date: string; // YYYY-MM-DD 格式 isLeave: boolean; leaveType?: 'sick' | 'personal' | 'annual' | 'other'; // 请假类型 reason?: string; // 请假原因 } // 员工日历数据接口 export interface EmployeeCalendarData { currentMonth: Date; days: EmployeeCalendarDay[]; } // 日历日期数据 export interface EmployeeCalendarDay { date: Date; projectCount: number; // 当天项目数量 projects: Array<{ id: string; name: string; deadline?: Date }>; // 项目列表 isToday: boolean; isCurrentMonth: boolean; } @Component({ selector: 'app-employee-detail-panel', standalone: true, imports: [CommonModule, DesignerCalendarComponent, ProjectProgressModalComponent], templateUrl: './employee-detail-panel.html', styleUrls: ['./employee-detail-panel.scss'] }) export class EmployeeDetailPanelComponent implements OnInit, OnChanges { // 暴露 Array 给模板使用 Array = Array; // 输入属性 @Input() visible: boolean = false; // 兼容旧模式:直接传入详情数据 @Input() employeeDetail: EmployeeDetail | null = null; // 🆕 替换直接传入 employeeDetail,改为传入基础数据自行计算 @Input() employeeName: string = ''; @Input() projects: any[] = []; // 该员工的项目列表 @Input() allLeaveRecords: LeaveRecord[] = []; // 所有请假记录(组件内过滤) @Input() embedMode: boolean = false; // 嵌入模式 // 输出事件 @Output() close = new EventEmitter(); @Output() projectClick = new EventEmitter(); // 兼容旧模式的输出事件 @Output() calendarMonthChange = new EventEmitter(); @Output() calendarDayClick = new EventEmitter(); @Output() refreshSurvey = new EventEmitter(); // 组件内部状态 internalEmployeeDetail: EmployeeDetail | null = null; // 内部生成的详情 showFullSurvey: boolean = false; refreshingSurvey: boolean = false; // 获取负载状态 get workloadStatus(): { label: string; class: string; color: string } { const count = this.currentEmployeeDetail?.currentProjects || 0; if (count >= 3) { return { label: '高负载', class: 'high', color: '#ef4444' }; // red } else if (count >= 1) { return { label: '正常', class: 'normal', color: '#3b82f6' }; // blue } return { label: '空闲', class: 'low', color: '#10b981' }; // green } // 获取能力标签 get strengthTags(): string[] { if (!this.currentEmployeeDetail?.surveyData?.answers) return []; const summary = this.getCapabilitySummary(this.currentEmployeeDetail.surveyData.answers); const tags: string[] = []; // 解析风格 if (summary.styles && summary.styles !== '未填写') { tags.push(...summary.styles.split('、').slice(0, 3)); } // 解析空间 if (summary.spaces && summary.spaces !== '未填写') { tags.push(...summary.spaces.split('、').slice(0, 2)); } // 技术优势 if (summary.advantages && summary.advantages !== '未填写') { tags.push(...summary.advantages.split('、').slice(0, 2)); } return tags.slice(0, 6); // 最多显示6个标签 } // 获取当前显示的 employeeDetail(优先使用 Input,否则使用内部生成的) get currentEmployeeDetail(): EmployeeDetail | null { return this.employeeDetail || this.internalEmployeeDetail; } // 日历项目列表弹窗状态 showCalendarProjectList: boolean = false; selectedDate: Date | null = null; selectedDayProjects: Array<{ id: string; name: string; deadline?: Date }> = []; // 设计师详细日历 showDesignerCalendar: boolean = false; calendarDesigners: CalendarDesigner[] = []; calendarViewMode: 'week' | 'month' | 'quarter' = 'month'; // 项目进度详情弹窗 showProgressModal: boolean = false; selectedProjectForProgress: string = ''; progressSummary: ProjectSpaceDeliverableSummary | null = null; loadingProgress: boolean = false; constructor( private router: Router, private cdr: ChangeDetectorRef, private projectSpaceDeliverableService: ProjectSpaceDeliverableService ) {} ngOnInit(): void { console.log('📋 EmployeeDetailPanelComponent 初始化'); } ngOnChanges(changes: SimpleChanges): void { // 优先处理直接传入的 employeeDetail if (changes['employeeDetail']) { // 如果外部传入了数据,不需要做额外操作,直接使用 return; } // 当 visible 变为 true,或员工姓名/项目数据改变时,重新生成详情(仅在未传入 employeeDetail 时) if (!this.employeeDetail && ( (changes['visible']?.currentValue === true) || (this.visible && (changes['employeeName'] || changes['projects'])) )) { if (this.employeeName) { this.generateEmployeeDetail(); } } } /** * 生成员工详情数据 */ async generateEmployeeDetail(): Promise { if (!this.employeeName) return; const employeeName = this.employeeName; // 过滤出当前活跃的项目(非已完成/已交付)用于显示"当前项目数" const activeProjects = this.projects.filter(p => p.status !== '已完成' && p.status !== '已交付'); const currentProjects = activeProjects.length; // 保存完整的项目数据(最多显示3个活跃项目) const projectData = activeProjects.slice(0, 3).map(p => ({ id: p.id, name: p.name })); const projectNames = projectData.map(p => p.name); // 项目名称列表 // 获取该员工的请假记录(未来7天) const today = new Date(); const next7Days = Array.from({ length: 7 }, (_, i) => { const date = new Date(today); date.setDate(today.getDate() + i); return date.toISOString().split('T')[0]; // YYYY-MM-DD 格式 }); const employeeLeaveRecords = this.allLeaveRecords.filter(record => record.employeeName === employeeName && next7Days.includes(record.date) ); // 生成红色标记说明 const redMarkExplanation = this.generateRedMarkExplanation(employeeName, employeeLeaveRecords, currentProjects); // 生成日历数据 (传入所有项目以显示历史负载) const calendarData = this.generateEmployeeCalendar(employeeName, this.projects); // 构建基础对象 this.internalEmployeeDetail = { name: employeeName, currentProjects, projectNames, projectData, leaveRecords: employeeLeaveRecords, redMarkExplanation, calendarData }; // 加载问卷数据 await this.loadSurveyData(employeeName); // 触发变更检测 this.cdr.markForCheck(); } /** * 加载问卷数据 */ async loadSurveyData(employeeName: string): Promise { if (!this.internalEmployeeDetail) return; try { const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova')); // 通过员工名字查找Profile(同时查询 realname 和 name 字段) const realnameQuery = new Parse.Query('Profile'); realnameQuery.equalTo('realname', employeeName); const nameQuery = new Parse.Query('Profile'); nameQuery.equalTo('name', employeeName); // 使用 or 查询 const profileQuery = Parse.Query.or(realnameQuery, nameQuery); profileQuery.limit(1); const profileResults = await profileQuery.find(); if (profileResults.length > 0) { const profile = profileResults[0]; this.internalEmployeeDetail.profileId = profile.id; this.internalEmployeeDetail.surveyCompleted = profile.get('surveyCompleted') || false; // 如果已完成问卷,加载问卷答案 if (this.internalEmployeeDetail.surveyCompleted) { const surveyQuery = new Parse.Query('SurveyLog'); surveyQuery.equalTo('profile', profile.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]; this.internalEmployeeDetail.surveyData = { answers: survey.get('answers') || [], createdAt: survey.get('createdAt'), updatedAt: survey.get('updatedAt') }; } } } this.cdr.markForCheck(); } catch (error) { console.error(`❌ 加载员工 ${employeeName} 问卷数据失败:`, error); } } /** * 生成员工日历数据(支持指定月份) */ private generateEmployeeCalendar(employeeName: string, employeeProjects: any[], targetMonth?: Date): EmployeeCalendarData { const currentMonth = targetMonth || 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(); today.setHours(0, 0, 0, 0); // 生成当月每一天的数据 for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day); const dateStr = date.toISOString().split('T')[0]; // 找出该日期相关的项目(项目进行中且在当天范围内) const dayProjects = employeeProjects.filter(p => { // 移除对已完成项目的过滤,以便在日历中显示历史负载 // if (p.status === '已完成' || p.status === '已交付') { // return false; // } const projectData = p.data || {}; // 2. 获取真实的项目开始时间 (逻辑与 DesignerWorkloadService 保持一致) const realStartDate = normalizeDateInput( projectData.phaseDeadlines?.modeling?.startDate || projectData.requirementsConfirmedAt || p.createdAt, new Date() ); // 3. 获取真实的交付日期 (逻辑与 DesignerWorkloadService 保持一致) let proposedEndDate = p.deadline || projectData.phaseDeadlines?.postProcessing?.deadline; let realEndDate: Date; if (proposedEndDate) { const proposed = normalizeDateInput(proposedEndDate, realStartDate); // fix: 只要有明确的deadline,就使用它 realEndDate = proposed; } else { realEndDate = addDays(realStartDate, 30); } // 归一化为当天0点,便于比较 const rangeStart = new Date(realStartDate); rangeStart.setHours(0, 0, 0, 0); const rangeEnd = new Date(realEndDate); rangeEnd.setHours(0, 0, 0, 0); const inRange = date >= rangeStart && date <= rangeEnd; return inRange; }).map(p => { const getDate = (dateValue: any) => { if (!dateValue) return undefined; if (dateValue.toDate && typeof dateValue.toDate === 'function') { return dateValue.toDate(); } if (dateValue instanceof Date) { return dateValue; } return new Date(dateValue); }; return { id: p.id, name: p.name, deadline: getDate(p.deadline) }; }); days.push({ date, projectCount: dayProjects.length, projects: dayProjects, isToday: date.getTime() === today.getTime(), isCurrentMonth: true }); } // 补齐前后的日期(保证从周日开始) const firstDay = new Date(year, month, 1); const firstDayOfWeek = firstDay.getDay(); // 0=周日 // 前置补齐(上个月的日期) for (let i = firstDayOfWeek - 1; i >= 0; i--) { const date = new Date(year, month, -i); days.unshift({ date, projectCount: 0, projects: [], isToday: false, isCurrentMonth: false }); } // 后置补齐(下个月的日期,保证总数是7的倍数) const remainder = days.length % 7; if (remainder !== 0) { const needed = 7 - remainder; for (let i = 1; i <= needed; i++) { const date = new Date(year, month + 1, i); days.push({ date, projectCount: 0, projects: [], isToday: false, isCurrentMonth: false }); } } return { currentMonth: new Date(year, month, 1), days }; } // 生成红色标记说明 private generateRedMarkExplanation(employeeName: string, leaveRecords: LeaveRecord[], projectCount: number): string { const explanations: string[] = []; // 检查请假情况 const leaveDays = leaveRecords.filter(record => record.isLeave); if (leaveDays.length > 0) { leaveDays.forEach(leave => { const date = new Date(leave.date); const dateStr = `${date.getMonth() + 1}月${date.getDate()}日`; explanations.push(`${dateStr}(${leave.reason || '请假'})`); }); } // 检查项目繁忙情况 if (projectCount >= 3) { const today = new Date(); const dateStr = `${today.getMonth() + 1}月${today.getDate()}日`; explanations.push(`${dateStr}(${projectCount}个项目繁忙)`); } if (explanations.length === 0) { return '当前无红色标记时段'; } return `甘特图中红色时段说明:${explanations.map((exp, index) => `${index + 1}${exp}`).join(';')}`; } /** * 关闭面板 */ onClose(): void { this.close.emit(); this.showFullSurvey = false; this.closeCalendarProjectList(); } /** * 切换月份 */ onChangeMonth(direction: number): void { // 如果是外部数据模式,发出事件让父组件处理 if (this.employeeDetail) { this.calendarMonthChange.emit(direction); return; } // 内部模式处理 if (!this.internalEmployeeDetail?.calendarData) { return; } const currentMonth = this.internalEmployeeDetail.calendarData.currentMonth; const newMonth = new Date(currentMonth); newMonth.setMonth(newMonth.getMonth() + direction); // 重新生成日历数据 const newCalendarData = this.generateEmployeeCalendar( this.employeeName, this.projects, newMonth ); // 更新员工详情中的日历数据 this.internalEmployeeDetail = { ...this.internalEmployeeDetail, calendarData: newCalendarData }; } /** * 日历日期点击 */ onCalendarDayClick(day: EmployeeCalendarDay): void { // 发出事件 this.calendarDayClick.emit(day); // 内部处理 if (!day.isCurrentMonth || day.projectCount === 0) { return; } this.selectedDate = day.date; this.selectedDayProjects = day.projects; this.showCalendarProjectList = true; } /** * 关闭项目列表弹窗 */ closeCalendarProjectList(): void { this.showCalendarProjectList = false; this.selectedDate = null; this.selectedDayProjects = []; } /** * 项目点击 */ onProjectClick(projectId: string): void { this.projectClick.emit(projectId); this.closeCalendarProjectList(); } /** * 打开“设计师详细日历”弹窗(复用订单分配页的日历) * 将当前员工信息适配为 DesignerCalendar 组件的数据结构 */ openDesignerCalendar(): void { const detail = this.currentEmployeeDetail; if (!detail) return; const name = detail.name || '设计师'; const currentProjects = detail.currentProjects || 0; // 将已有的 employeeDetail.calendarData 映射为日历事件(粗粒度:有项目视为当日有工作) const upcomingEvents: CalendarDesigner['upcomingEvents'] = []; const days = detail.calendarData?.days || []; for (const day of days) { if (day.projectCount > 0) { upcomingEvents.push({ id: `${day.date.getTime()}`, date: day.date, title: `${day.projectCount}个项目`, type: 'project', duration: 6 }); } } // 适配为日历组件的设计师数据(单人视图) this.calendarDesigners = [{ id: detail.profileId || name, name, groupId: '', groupName: '', isLeader: false, status: currentProjects >= 3 ? 'busy' : 'available', currentProjects, upcomingEvents, workload: Math.min(100, currentProjects * 30) }]; this.showDesignerCalendar = true; } closeDesignerCalendar(): void { this.showDesignerCalendar = false; this.calendarDesigners = []; } /** * 刷新问卷 */ async onRefreshSurvey(): Promise { // 如果是外部数据模式,发出事件让父组件处理 if (this.employeeDetail) { this.refreshSurvey.emit(); return; } if (this.refreshingSurvey || !this.internalEmployeeDetail) { return; } try { this.refreshingSurvey = true; console.log('🔄 刷新问卷状态...'); await this.loadSurveyData(this.employeeName); console.log('✅ 问卷状态刷新成功'); } catch (error) { console.error('❌ 刷新问卷状态失败:', error); } finally { this.refreshingSurvey = false; } } /** * 切换问卷显示模式 */ toggleSurveyDisplay(): void { this.showFullSurvey = !this.showFullSurvey; } /** * 获取能力画像摘要 */ getCapabilitySummary(answers: any[]): any { const findAnswer = (questionId: string) => { const item = answers.find((a: any) => a.questionId === questionId); return item?.answer; }; const formatArray = (value: any): string => { if (Array.isArray(value)) { return value.join('、'); } return value || '未填写'; }; return { styles: formatArray(findAnswer('q1_expertise_styles')), spaces: formatArray(findAnswer('q2_expertise_spaces')), advantages: formatArray(findAnswer('q3_technical_advantages')), difficulty: findAnswer('q5_project_difficulty') || '未填写', capacity: findAnswer('q7_weekly_capacity') || '未填写', urgent: findAnswer('q8_urgent_willingness') || '未填写', urgentLimit: findAnswer('q8_urgent_limit') || '', feedback: findAnswer('q9_progress_feedback') || '未填写', communication: formatArray(findAnswer('q12_communication_methods')) }; } /** * 获取请假类型显示文本 */ getLeaveTypeText(leaveType?: string): string { const typeMap: Record = { 'sick': '病假', 'personal': '事假', 'annual': '年假', 'other': '其他' }; return typeMap[leaveType || ''] || '未知'; } /** * 查看项目进度 */ async onViewProgress(projectId: string, event?: Event): Promise { if (event) { event.stopPropagation(); } this.selectedProjectForProgress = projectId; this.loadingProgress = true; this.showProgressModal = true; try { console.log('📊 加载项目进度数据:', projectId); this.progressSummary = await this.projectSpaceDeliverableService.getProjectSpaceDeliverableSummary(projectId); console.log('✅ 项目进度数据加载成功:', this.progressSummary); } catch (error) { console.error('❌ 加载项目进度数据失败:', error); this.progressSummary = null; } finally { this.loadingProgress = false; this.cdr.markForCheck(); } } /** * 关闭进度弹窗 */ closeProgressModal(): void { this.showProgressModal = false; this.selectedProjectForProgress = ''; this.progressSummary = null; } /** * 阻止事件冒泡 */ stopPropagation(event: Event): void { event.stopPropagation(); } }