|
@@ -1,12 +1,15 @@
|
|
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
import { CommonModule } from '@angular/common';
|
|
|
-import { RouterModule } from '@angular/router';
|
|
|
|
|
|
|
+import { RouterModule, ActivatedRoute, Router } from '@angular/router';
|
|
|
import { ProjectService } from '../../../services/project.service';
|
|
import { ProjectService } from '../../../services/project.service';
|
|
|
import { Task } from '../../../models/project.model';
|
|
import { Task } from '../../../models/project.model';
|
|
|
import { SkillRadarComponent } from './skill-radar/skill-radar.component';
|
|
import { SkillRadarComponent } from './skill-radar/skill-radar.component';
|
|
|
import { PersonalBoard } from '../personal-board/personal-board';
|
|
import { PersonalBoard } from '../personal-board/personal-board';
|
|
|
import { FmodeQuery, FmodeObject, FmodeUser } from 'fmode-ng/core';
|
|
import { FmodeQuery, FmodeObject, FmodeUser } from 'fmode-ng/core';
|
|
|
import { WxworkAuth } from 'fmode-ng/core';
|
|
import { WxworkAuth } from 'fmode-ng/core';
|
|
|
|
|
+import { DesignerTaskService } from '../../../services/designer-task.service';
|
|
|
|
|
+import { LeaveService, LeaveApplication } from '../../../services/leave.service';
|
|
|
|
|
+import { FormsModule } from '@angular/forms';
|
|
|
|
|
|
|
|
interface ShiftTask {
|
|
interface ShiftTask {
|
|
|
id: string;
|
|
id: string;
|
|
@@ -28,7 +31,7 @@ interface ProjectTimelineItem {
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-dashboard',
|
|
selector: 'app-dashboard',
|
|
|
standalone: true,
|
|
standalone: true,
|
|
|
- imports: [CommonModule, RouterModule, SkillRadarComponent, PersonalBoard],
|
|
|
|
|
|
|
+ imports: [CommonModule, RouterModule, FormsModule, SkillRadarComponent, PersonalBoard],
|
|
|
templateUrl: './dashboard.html',
|
|
templateUrl: './dashboard.html',
|
|
|
styleUrl: './dashboard.scss'
|
|
styleUrl: './dashboard.scss'
|
|
|
})
|
|
})
|
|
@@ -54,52 +57,122 @@ export class Dashboard implements OnInit {
|
|
|
projectTimeline: ProjectTimelineItem[] = [];
|
|
projectTimeline: ProjectTimelineItem[] = [];
|
|
|
private wxAuth: WxworkAuth | null = null;
|
|
private wxAuth: WxworkAuth | null = null;
|
|
|
private currentUser: FmodeUser | null = null;
|
|
private currentUser: FmodeUser | null = null;
|
|
|
|
|
+ private currentProfile: FmodeObject | null = null; // 当前Profile
|
|
|
|
|
+ private cid: string = '';
|
|
|
|
|
+
|
|
|
|
|
+ // 请假相关
|
|
|
|
|
+ showLeaveModal: boolean = false;
|
|
|
|
|
+ leaveApplications: LeaveApplication[] = [];
|
|
|
|
|
+ today: string = new Date().toISOString().split('T')[0];
|
|
|
|
|
+
|
|
|
|
|
+ // 请假表单
|
|
|
|
|
+ leaveForm = {
|
|
|
|
|
+ startDate: '',
|
|
|
|
|
+ endDate: '',
|
|
|
|
|
+ type: 'personal' as 'annual' | 'sick' | 'personal' | 'other',
|
|
|
|
|
+ reason: ''
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- constructor(private projectService: ProjectService) {
|
|
|
|
|
- this.initAuth();
|
|
|
|
|
|
|
+ constructor(
|
|
|
|
|
+ private projectService: ProjectService,
|
|
|
|
|
+ private route: ActivatedRoute,
|
|
|
|
|
+ private router: Router,
|
|
|
|
|
+ private taskService: DesignerTaskService,
|
|
|
|
|
+ private leaveService: LeaveService
|
|
|
|
|
+ ) {}
|
|
|
|
|
+
|
|
|
|
|
+ async ngOnInit(): Promise<void> {
|
|
|
|
|
+ // 1. 从URL获取cid
|
|
|
|
|
+ this.route.paramMap.subscribe(async params => {
|
|
|
|
|
+ this.cid = params.get('cid') || localStorage.getItem('company') || '';
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.cid) {
|
|
|
|
|
+ console.warn('⚠️ 未找到公司ID,尝试使用默认值');
|
|
|
|
|
+ this.cid = 'cDL6R1hgSi'; // 默认公司ID
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 初始化企微认证
|
|
|
|
|
+ this.initAuth();
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 执行认证并加载数据
|
|
|
|
|
+ await this.authenticateAndLoadData();
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 初始化企业微信认证
|
|
|
|
|
|
|
+ // 初始化企业微信认证(优化版)
|
|
|
private initAuth(): void {
|
|
private initAuth(): void {
|
|
|
try {
|
|
try {
|
|
|
this.wxAuth = new WxworkAuth({
|
|
this.wxAuth = new WxworkAuth({
|
|
|
- cid: 'cDL6R1hgSi' // 公司帐套ID
|
|
|
|
|
|
|
+ cid: this.cid, // 使用动态获取的cid
|
|
|
|
|
+ appId: 'crm'
|
|
|
});
|
|
});
|
|
|
- console.log('✅ 设计师仪表板企业微信认证初始化成功');
|
|
|
|
|
|
|
+ console.log('✅ 设计师端企微认证初始化成功,CID:', this.cid);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('❌ 设计师仪表板企业微信认证初始化失败:', error);
|
|
|
|
|
|
|
+ console.error('❌ 设计师端企微认证初始化失败:', error);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async ngOnInit(): Promise<void> {
|
|
|
|
|
- await this.authenticateAndLoadData();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 认证并加载数据
|
|
|
|
|
|
|
+ // 认证并加载数据(优化版)
|
|
|
private async authenticateAndLoadData(): Promise<void> {
|
|
private async authenticateAndLoadData(): Promise<void> {
|
|
|
try {
|
|
try {
|
|
|
// 执行企业微信认证和登录
|
|
// 执行企业微信认证和登录
|
|
|
- const { user } = await this.wxAuth!.authenticateAndLogin();
|
|
|
|
|
|
|
+ const { user, profile } = await this.wxAuth!.authenticateAndLogin();
|
|
|
this.currentUser = user;
|
|
this.currentUser = user;
|
|
|
|
|
+ this.currentProfile = profile;
|
|
|
|
|
|
|
|
- if (user) {
|
|
|
|
|
- console.log('✅ 设计师登录成功:', user.get('username'));
|
|
|
|
|
- await this.loadDashboardData();
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ if (!user || !profile) {
|
|
|
console.error('❌ 设计师登录失败');
|
|
console.error('❌ 设计师登录失败');
|
|
|
|
|
+ this.loadMockData();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('✅ 设计师登录成功:', user.get('username'));
|
|
|
|
|
+ console.log('✅ Profile ID:', profile.id);
|
|
|
|
|
+
|
|
|
|
|
+ // 验证角色是否为"组员"
|
|
|
|
|
+ if (!await this.validateDesignerRole()) {
|
|
|
|
|
+ alert('您不是设计师(组员),无权访问此页面');
|
|
|
|
|
+ this.router.navigate(['/']);
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 缓存Profile ID
|
|
|
|
|
+ localStorage.setItem('Parse/ProfileId', profile.id);
|
|
|
|
|
+
|
|
|
|
|
+ // 加载真实数据
|
|
|
|
|
+ await this.loadDashboardData();
|
|
|
|
|
+
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('❌ 设计师认证过程出错:', error);
|
|
console.error('❌ 设计师认证过程出错:', error);
|
|
|
// 降级到模拟数据
|
|
// 降级到模拟数据
|
|
|
this.loadMockData();
|
|
this.loadMockData();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 验证设计师(组员)角色
|
|
|
|
|
+ */
|
|
|
|
|
+ private async validateDesignerRole(): Promise<boolean> {
|
|
|
|
|
+ if (!this.currentProfile) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const roleName = this.currentProfile.get('roleName');
|
|
|
|
|
+
|
|
|
|
|
+ if (roleName !== '组员') {
|
|
|
|
|
+ console.warn(`⚠️ 用户角色为"${roleName}",不是"组员"`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('✅ 角色验证通过:组员(设计师)');
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 加载仪表板数据
|
|
// 加载仪表板数据
|
|
|
private async loadDashboardData(): Promise<void> {
|
|
private async loadDashboardData(): Promise<void> {
|
|
|
try {
|
|
try {
|
|
|
await Promise.all([
|
|
await Promise.all([
|
|
|
- this.loadTasks(),
|
|
|
|
|
|
|
+ this.loadRealTasks(), // 使用真实数据
|
|
|
this.loadShiftTasks(),
|
|
this.loadShiftTasks(),
|
|
|
this.calculateWorkloadPercentage(),
|
|
this.calculateWorkloadPercentage(),
|
|
|
this.loadProjectTimeline()
|
|
this.loadProjectTimeline()
|
|
@@ -111,6 +184,127 @@ export class Dashboard implements OnInit {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载真实任务数据
|
|
|
|
|
+ */
|
|
|
|
|
+ private async loadRealTasks(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.currentProfile) {
|
|
|
|
|
+ throw new Error('未找到当前Profile');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const designerTasks = await this.taskService.getMyTasks(this.currentProfile.id);
|
|
|
|
|
+
|
|
|
|
|
+ // 转换为组件所需格式
|
|
|
|
|
+ this.tasks = designerTasks.map(task => ({
|
|
|
|
|
+ id: task.id,
|
|
|
|
|
+ projectId: task.projectId,
|
|
|
|
|
+ projectName: task.projectName,
|
|
|
|
|
+ title: task.projectName,
|
|
|
|
|
+ stage: task.stage,
|
|
|
|
|
+ deadline: task.deadline,
|
|
|
|
|
+ isOverdue: task.isOverdue,
|
|
|
|
|
+ priority: task.priority,
|
|
|
|
|
+ isCompleted: false,
|
|
|
|
|
+ assignee: null,
|
|
|
|
|
+ description: `${task.stage} - ${task.customerName}`
|
|
|
|
|
+ })) as Task[];
|
|
|
|
|
+
|
|
|
|
|
+ // 筛选超期任务
|
|
|
|
|
+ this.overdueTasks = this.tasks.filter(task => task.isOverdue);
|
|
|
|
|
+
|
|
|
|
|
+ // 筛选紧急任务
|
|
|
|
|
+ this.urgentTasks = this.tasks.filter(task => {
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const diffHours = (task.deadline.getTime() - now.getTime()) / (1000 * 60 * 60);
|
|
|
|
|
+ return diffHours <= 3 && diffHours > 0 && task.stage === '渲染';
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 设置反馈项目ID
|
|
|
|
|
+ if (this.overdueTasks.length > 0) {
|
|
|
|
|
+ this.feedbackProjectId = this.overdueTasks[0].projectId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 加载待处理反馈
|
|
|
|
|
+ await this.loadRealPendingFeedbacks();
|
|
|
|
|
+
|
|
|
|
|
+ // 启动倒计时
|
|
|
|
|
+ this.startCountdowns();
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`✅ 成功加载 ${this.tasks.length} 个真实任务`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('❌ 加载真实任务失败:', error);
|
|
|
|
|
+ throw error;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载真实待处理反馈
|
|
|
|
|
+ */
|
|
|
|
|
+ private async loadRealPendingFeedbacks(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
|
|
|
|
|
+
|
|
|
|
|
+ // 查询该设计师相关项目的待处理反馈
|
|
|
|
|
+ const projectIds = this.tasks.map(t => t.projectId);
|
|
|
|
|
+
|
|
|
|
|
+ if (projectIds.length === 0) {
|
|
|
|
|
+ this.pendingFeedbacks = [];
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const query = new Parse.Query('ProjectFeedback');
|
|
|
|
|
+ const ProjectClass = Parse.Object.extend('Project');
|
|
|
|
|
+ query.containedIn('project', projectIds.map(id => {
|
|
|
|
|
+ const obj = new ProjectClass();
|
|
|
|
|
+ obj.id = id;
|
|
|
|
|
+ return obj;
|
|
|
|
|
+ }));
|
|
|
|
|
+ query.equalTo('status', '待处理');
|
|
|
|
|
+ query.notEqualTo('isDeleted', true);
|
|
|
|
|
+ query.include('project');
|
|
|
|
|
+ query.include('contact');
|
|
|
|
|
+ query.descending('createdAt');
|
|
|
|
|
+ query.limit(100);
|
|
|
|
|
+
|
|
|
|
|
+ const feedbacks = await query.find();
|
|
|
|
|
+
|
|
|
|
|
+ this.pendingFeedbacks = feedbacks.map((feedback: any) => {
|
|
|
|
|
+ const project = feedback.get('project');
|
|
|
|
|
+ const task = this.tasks.find(t => t.projectId === project?.id);
|
|
|
|
|
+
|
|
|
|
|
+ const fallbackTask: Task = {
|
|
|
|
|
+ id: project?.id || '',
|
|
|
|
|
+ projectId: project?.id || '',
|
|
|
|
|
+ projectName: project?.get('title') || '未知项目',
|
|
|
|
|
+ title: project?.get('title') || '未知项目',
|
|
|
|
|
+ stage: '投诉处理',
|
|
|
|
|
+ deadline: new Date(),
|
|
|
|
|
+ isOverdue: false,
|
|
|
|
|
+ isCompleted: false,
|
|
|
|
|
+ priority: 'high',
|
|
|
|
|
+ assignee: null,
|
|
|
|
|
+ description: '待处理反馈'
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ task: task || fallbackTask,
|
|
|
|
|
+ feedback: {
|
|
|
|
|
+ id: feedback.id,
|
|
|
|
|
+ content: feedback.get('content') || '',
|
|
|
|
|
+ rating: feedback.get('rating'),
|
|
|
|
|
+ createdAt: feedback.get('createdAt')
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`✅ 成功加载 ${this.pendingFeedbacks.length} 个待处理反馈`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('❌ 加载待处理反馈失败:', error);
|
|
|
|
|
+ this.pendingFeedbacks = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 降级到模拟数据
|
|
// 降级到模拟数据
|
|
|
private loadMockData(): void {
|
|
private loadMockData(): void {
|
|
|
console.warn('⚠️ 使用模拟数据');
|
|
console.warn('⚠️ 使用模拟数据');
|
|
@@ -511,5 +705,135 @@ export class Dashboard implements OnInit {
|
|
|
}
|
|
}
|
|
|
].sort((a, b) => new Date(a.deadline).getTime() - new Date(b.deadline).getTime());
|
|
].sort((a, b) => new Date(a.deadline).getTime() - new Date(b.deadline).getTime());
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开请假申请弹窗
|
|
|
|
|
+ */
|
|
|
|
|
+ openLeaveModal(): void {
|
|
|
|
|
+ this.showLeaveModal = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 关闭请假申请弹窗
|
|
|
|
|
+ */
|
|
|
|
|
+ closeLeaveModal(): void {
|
|
|
|
|
+ this.showLeaveModal = false;
|
|
|
|
|
+ this.resetLeaveForm();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 提交请假申请
|
|
|
|
|
+ */
|
|
|
|
|
+ async submitLeaveApplication(): Promise<void> {
|
|
|
|
|
+ if (!this.currentProfile) {
|
|
|
|
|
+ alert('未找到当前用户信息');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 验证表单
|
|
|
|
|
+ if (!this.leaveForm.startDate || !this.leaveForm.endDate) {
|
|
|
|
|
+ alert('请选择请假日期');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.leaveForm.reason.trim()) {
|
|
|
|
|
+ alert('请输入请假原因');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const startDate = new Date(this.leaveForm.startDate);
|
|
|
|
|
+ const endDate = new Date(this.leaveForm.endDate);
|
|
|
|
|
+
|
|
|
|
|
+ if (startDate > endDate) {
|
|
|
|
|
+ alert('结束日期不能早于开始日期');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算请假天数
|
|
|
|
|
+ const days = this.leaveService.calculateLeaveDays(startDate, endDate);
|
|
|
|
|
+
|
|
|
|
|
+ if (days === 0) {
|
|
|
|
|
+ alert('请假天数必须大于0(周末不计入)');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const success = await this.leaveService.submitLeaveApplication(
|
|
|
|
|
+ this.currentProfile.id,
|
|
|
|
|
+ {
|
|
|
|
|
+ startDate,
|
|
|
|
|
+ endDate,
|
|
|
|
|
+ type: this.leaveForm.type,
|
|
|
|
|
+ reason: this.leaveForm.reason,
|
|
|
|
|
+ days
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (success) {
|
|
|
|
|
+ alert(`请假申请已提交!共${days}天(已排除周末)`);
|
|
|
|
|
+ this.closeLeaveModal();
|
|
|
|
|
+ await this.loadMyLeaveRecords();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('请假申请提交失败,请重试');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('提交请假申请失败:', error);
|
|
|
|
|
+ alert('请假申请提交失败,请重试');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载我的请假记录
|
|
|
|
|
+ */
|
|
|
|
|
+ private async loadMyLeaveRecords(): Promise<void> {
|
|
|
|
|
+ if (!this.currentProfile) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.leaveApplications = await this.leaveService.getMyLeaveRecords(
|
|
|
|
|
+ this.currentProfile.id
|
|
|
|
|
+ );
|
|
|
|
|
+ console.log(`✅ 成功加载 ${this.leaveApplications.length} 条请假记录`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('❌ 加载请假记录失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 重置请假表单
|
|
|
|
|
+ */
|
|
|
|
|
+ private resetLeaveForm(): void {
|
|
|
|
|
+ this.leaveForm = {
|
|
|
|
|
+ startDate: '',
|
|
|
|
|
+ endDate: '',
|
|
|
|
|
+ type: 'personal',
|
|
|
|
|
+ reason: ''
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取请假类型文本
|
|
|
|
|
+ */
|
|
|
|
|
+ getLeaveTypeText(type: string): string {
|
|
|
|
|
+ const typeMap: Record<string, string> = {
|
|
|
|
|
+ 'annual': '年假',
|
|
|
|
|
+ 'sick': '病假',
|
|
|
|
|
+ 'personal': '事假',
|
|
|
|
|
+ 'other': '其他'
|
|
|
|
|
+ };
|
|
|
|
|
+ return typeMap[type] || type;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取请假状态文本
|
|
|
|
|
+ */
|
|
|
|
|
+ getLeaveStatusText(status: string): string {
|
|
|
|
|
+ const statusMap: Record<string, string> = {
|
|
|
|
|
+ 'pending': '待审批',
|
|
|
|
|
+ 'approved': '已批准',
|
|
|
|
|
+ 'rejected': '已拒绝'
|
|
|
|
|
+ };
|
|
|
|
|
+ return statusMap[status] || status;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+
|