| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666 |
- 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<void>();
- @Output() projectClick = new EventEmitter<string>();
- // 兼容旧模式的输出事件
- @Output() calendarMonthChange = new EventEmitter<number>();
- @Output() calendarDayClick = new EventEmitter<EmployeeCalendarDay>();
- @Output() refreshSurvey = new EventEmitter<void>();
-
- // 组件内部状态
- 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<void> {
- 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<void> {
- 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<void> {
- // 如果是外部数据模式,发出事件让父组件处理
- 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<string, string> = {
- 'sick': '病假',
- 'personal': '事假',
- 'annual': '年假',
- 'other': '其他'
- };
- return typeMap[leaveType || ''] || '未知';
- }
-
- /**
- * 查看项目进度
- */
- async onViewProgress(projectId: string, event?: Event): Promise<void> {
- 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();
- }
- }
|