dashboard-data.service.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import { Injectable } from '@angular/core';
  2. import { ProjectSpaceDeliverableService } from '../../../../modules/project/services/project-space-deliverable.service';
  3. /**
  4. * 仪表盘数据服务
  5. * 负责组长端仪表盘的统计数据获取
  6. */
  7. @Injectable({
  8. providedIn: 'root'
  9. })
  10. export class DashboardDataService {
  11. private cid: string = '';
  12. private Parse: any = null;
  13. constructor(
  14. private projectSpaceDeliverableService: ProjectSpaceDeliverableService
  15. ) {
  16. this.cid = localStorage.getItem('company') || '';
  17. console.log('🏢 DashboardDataService初始化,当前公司ID:', this.cid || '(未设置)');
  18. this.initParse();
  19. }
  20. /**
  21. * 延迟初始化Parse
  22. */
  23. private async initParse(): Promise<void> {
  24. try {
  25. // 🔥 尝试从 core 导入以获取完整功能的 FmodeParse
  26. const { FmodeParse } = await import('fmode-ng/core');
  27. this.Parse = FmodeParse.with("nova");
  28. console.log('✅ DashboardDataService: FmodeParse (core) 初始化成功');
  29. } catch (error) {
  30. console.warn('⚠️ 从 fmode-ng/core 导入失败,尝试 fmode-ng/parse', error);
  31. try {
  32. const { FmodeParse } = await import('fmode-ng/parse');
  33. this.Parse = FmodeParse.with("nova");
  34. console.log('✅ DashboardDataService: FmodeParse (parse) 初始化成功');
  35. } catch (err) {
  36. console.error('❌ DashboardDataService: FmodeParse 初始化失败:', err);
  37. }
  38. }
  39. }
  40. /**
  41. * 确保Parse已初始化
  42. */
  43. private async ensureParse(): Promise<any> {
  44. if (!this.Parse) {
  45. await this.initParse();
  46. }
  47. return this.Parse;
  48. }
  49. /**
  50. * 获取KPI统计数据
  51. * @returns KPI数据
  52. */
  53. async getKPIStats(): Promise<{
  54. totalProjects: number;
  55. inProgressProjects: number;
  56. completedProjects: number;
  57. overdueProjects: number;
  58. dueSoonProjects: number;
  59. totalDesigners: number;
  60. availableDesigners: number;
  61. busyDesigners: number;
  62. overloadedDesigners: number;
  63. }> {
  64. const Parse = await this.ensureParse();
  65. if (!Parse) {
  66. return this.getDefaultKPIStats();
  67. }
  68. try {
  69. // 项目统计
  70. const projectQuery = new Parse.Query('Project');
  71. projectQuery.equalTo('company', this.cid);
  72. projectQuery.notEqualTo('isDeleted', true);
  73. const totalProjects = await projectQuery.count();
  74. const inProgressQuery = new Parse.Query('Project');
  75. inProgressQuery.equalTo('company', this.cid);
  76. inProgressQuery.equalTo('status', '进行中');
  77. inProgressQuery.notEqualTo('isDeleted', true);
  78. const inProgressProjects = await inProgressQuery.count();
  79. const completedQuery = new Parse.Query('Project');
  80. completedQuery.equalTo('company', this.cid);
  81. completedQuery.equalTo('status', '已完成');
  82. completedQuery.notEqualTo('isDeleted', true);
  83. const completedProjects = await completedQuery.count();
  84. // 超期项目
  85. const now = new Date();
  86. const overdueQuery = new Parse.Query('Project');
  87. overdueQuery.equalTo('company', this.cid);
  88. overdueQuery.equalTo('status', '进行中');
  89. overdueQuery.lessThan('deadline', now);
  90. overdueQuery.notEqualTo('isDeleted', true);
  91. const overdueProjects = await overdueQuery.count();
  92. // 临期项目(7天内到期)
  93. const sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
  94. const dueSoonQuery = new Parse.Query('Project');
  95. dueSoonQuery.equalTo('company', this.cid);
  96. dueSoonQuery.equalTo('status', '进行中');
  97. dueSoonQuery.greaterThanOrEqualTo('deadline', now);
  98. dueSoonQuery.lessThanOrEqualTo('deadline', sevenDaysLater);
  99. dueSoonQuery.notEqualTo('isDeleted', true);
  100. const dueSoonProjects = await dueSoonQuery.count();
  101. // 设计师统计
  102. const designerQuery = new Parse.Query('Profile');
  103. designerQuery.equalTo('company', this.cid);
  104. designerQuery.equalTo('roleName', '组员');
  105. designerQuery.notEqualTo('isDeleted', true);
  106. const designers = await designerQuery.find();
  107. const totalDesigners = designers.length;
  108. // 统计设计师负载
  109. let availableDesigners = 0;
  110. let busyDesigners = 0;
  111. let overloadedDesigners = 0;
  112. for (const designer of designers) {
  113. const assignedQuery = new Parse.Query('Project');
  114. assignedQuery.equalTo('assignee', designer.id);
  115. assignedQuery.equalTo('status', '进行中');
  116. assignedQuery.notEqualTo('isDeleted', true);
  117. const projectCount = await assignedQuery.count();
  118. if (projectCount === 0) {
  119. availableDesigners++;
  120. } else if (projectCount >= 3) {
  121. overloadedDesigners++;
  122. } else {
  123. busyDesigners++;
  124. }
  125. }
  126. console.log('✅ KPI统计数据获取成功');
  127. return {
  128. totalProjects,
  129. inProgressProjects,
  130. completedProjects,
  131. overdueProjects,
  132. dueSoonProjects,
  133. totalDesigners,
  134. availableDesigners,
  135. busyDesigners,
  136. overloadedDesigners
  137. };
  138. } catch (error) {
  139. console.error('❌ 获取KPI统计失败:', error);
  140. return this.getDefaultKPIStats();
  141. }
  142. }
  143. /**
  144. * 获取项目阶段分布
  145. * @returns 阶段分布数据
  146. */
  147. async getStageDistribution(): Promise<Record<string, number>> {
  148. const Parse = await this.ensureParse();
  149. if (!Parse) return {};
  150. try {
  151. const stages = ['订单分配', '方案深化', '交付执行', '售后归档'];
  152. const distribution: Record<string, number> = {};
  153. for (const stage of stages) {
  154. const query = new Parse.Query('Project');
  155. query.equalTo('company', this.cid);
  156. query.equalTo('currentStage', stage);
  157. query.equalTo('status', '进行中');
  158. query.notEqualTo('isDeleted', true);
  159. const count = await query.count();
  160. distribution[stage] = count;
  161. }
  162. console.log('✅ 项目阶段分布获取成功:', distribution);
  163. return distribution;
  164. } catch (error) {
  165. console.error('❌ 获取项目阶段分布失败:', error);
  166. return {};
  167. }
  168. }
  169. /**
  170. * 获取设计师工作负载分布
  171. * @returns 负载分布数据
  172. */
  173. async getDesignerWorkloadDistribution(): Promise<{
  174. idle: number;
  175. busy: number;
  176. overload: number;
  177. }> {
  178. const Parse = await this.ensureParse();
  179. if (!Parse) {
  180. return { idle: 0, busy: 0, overload: 0 };
  181. }
  182. try {
  183. const designerQuery = new Parse.Query('Profile');
  184. designerQuery.equalTo('company', this.cid);
  185. designerQuery.equalTo('roleName', '组员');
  186. designerQuery.notEqualTo('isDeleted', true);
  187. const designers = await designerQuery.find();
  188. let idle = 0;
  189. let busy = 0;
  190. let overload = 0;
  191. for (const designer of designers) {
  192. const projectQuery = new Parse.Query('Project');
  193. projectQuery.equalTo('assignee', designer.id);
  194. projectQuery.equalTo('status', '进行中');
  195. projectQuery.notEqualTo('isDeleted', true);
  196. const projectCount = await projectQuery.count();
  197. if (projectCount === 0) {
  198. idle++;
  199. } else if (projectCount >= 3) {
  200. overload++;
  201. } else {
  202. busy++;
  203. }
  204. }
  205. console.log('✅ 设计师负载分布获取成功:', { idle, busy, overload });
  206. return { idle, busy, overload };
  207. } catch (error) {
  208. console.error('❌ 获取设计师负载分布失败:', error);
  209. return { idle: 0, busy: 0, overload: 0 };
  210. }
  211. }
  212. /**
  213. * 获取待办任务列表
  214. * @returns 待办任务
  215. */
  216. async getTodoTasks(): Promise<any[]> {
  217. const Parse = await this.ensureParse();
  218. if (!Parse) return [];
  219. try {
  220. const tasks: any[] = [];
  221. // 1. 待分配的项目
  222. const unassignedQuery = new Parse.Query('Project');
  223. unassignedQuery.equalTo('company', this.cid);
  224. unassignedQuery.equalTo('status', '待分配');
  225. unassignedQuery.notEqualTo('isDeleted', true);
  226. unassignedQuery.include('customer');
  227. unassignedQuery.descending('createdAt');
  228. unassignedQuery.limit(10);
  229. const unassignedProjects = await unassignedQuery.find();
  230. for (const project of unassignedProjects) {
  231. const customer = project.get('customer');
  232. tasks.push({
  233. id: `assign-${project.id}`,
  234. title: `分配项目: ${project.get('title')}`,
  235. description: `客户 ${customer?.get('name') || '未知'} 的项目等待分配设计师`,
  236. deadline: project.get('createdAt'),
  237. priority: 'high' as const,
  238. type: 'assign' as const,
  239. targetId: project.id
  240. });
  241. }
  242. // 2. 超期项目(需要跟进)
  243. const now = new Date();
  244. const overdueQuery = new Parse.Query('Project');
  245. overdueQuery.equalTo('company', this.cid);
  246. overdueQuery.equalTo('status', '进行中');
  247. overdueQuery.lessThan('deadline', now);
  248. overdueQuery.notEqualTo('isDeleted', true);
  249. overdueQuery.include('assignee', 'customer');
  250. overdueQuery.limit(10);
  251. const overdueProjects = await overdueQuery.find();
  252. for (const project of overdueProjects) {
  253. const assignee = project.get('assignee');
  254. const customer = project.get('customer');
  255. const deadline = project.get('deadline');
  256. const overdueDays = Math.ceil((now.getTime() - deadline.getTime()) / (1000 * 60 * 60 * 24));
  257. tasks.push({
  258. id: `overdue-${project.id}`,
  259. title: `超期项目: ${project.get('title')}`,
  260. description: `设计师 ${assignee?.get('name') || '未分配'} 负责的项目已超期 ${overdueDays} 天,客户:${customer?.get('name') || '未知'}`,
  261. deadline: project.get('deadline'),
  262. priority: 'high' as const,
  263. type: 'review' as const,
  264. targetId: project.id
  265. });
  266. }
  267. // 3. 临期项目(3天内到期)
  268. const threeDaysLater = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000);
  269. const dueSoonQuery = new Parse.Query('Project');
  270. dueSoonQuery.equalTo('company', this.cid);
  271. dueSoonQuery.equalTo('status', '进行中');
  272. dueSoonQuery.greaterThanOrEqualTo('deadline', now);
  273. dueSoonQuery.lessThanOrEqualTo('deadline', threeDaysLater);
  274. dueSoonQuery.notEqualTo('isDeleted', true);
  275. dueSoonQuery.include('assignee');
  276. dueSoonQuery.limit(10);
  277. const dueSoonProjects = await dueSoonQuery.find();
  278. for (const project of dueSoonProjects) {
  279. const assignee = project.get('assignee');
  280. const deadline = project.get('deadline');
  281. const remainingDays = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
  282. tasks.push({
  283. id: `duesoon-${project.id}`,
  284. title: `临期项目: ${project.get('title')}`,
  285. description: `设计师 ${assignee?.get('name') || '未分配'} 负责的项目还有 ${remainingDays} 天到期`,
  286. deadline: project.get('deadline'),
  287. priority: remainingDays <= 1 ? 'high' as const : 'medium' as const,
  288. type: 'review' as const,
  289. targetId: project.id
  290. });
  291. }
  292. console.log(`✅ 获取待办任务成功,共 ${tasks.length} 个任务`);
  293. return tasks;
  294. } catch (error) {
  295. console.error('❌ 获取待办任务失败:', error);
  296. return [];
  297. }
  298. }
  299. /**
  300. * 获取组长看板数据
  301. */
  302. async getTeamLeaderDataFromCloud(): Promise<any> {
  303. try {
  304. const { FmodeParse } = await import('fmode-ng/core');
  305. const Parse: any = FmodeParse.initialize({
  306. appId: "ncloudmaster",
  307. serverURL: "https://server.fmode.cn/parse",
  308. appName: 'NovaCloud'
  309. } as any);
  310. const startTime = Date.now();
  311. // 调用云函数 (ID: 8qJkylemKn)
  312. const result = await Parse.Cloud.function({
  313. id: '8qJkylemKn',
  314. companyId: this.cid
  315. });
  316. console.log(`✅ 云函数数据加载成功,耗时 ${Date.now() - startTime}ms`);
  317. // 🆕 处理空间统计数据注入(预加载缓存)
  318. if (result && result.data && result.data.spaceStats) {
  319. const spaceStats = result.data.spaceStats;
  320. const count = Object.keys(spaceStats).length;
  321. console.log(`📊 预加载 ${count} 个项目的空间统计数据`);
  322. Object.keys(spaceStats).forEach(projectId => {
  323. this.projectSpaceDeliverableService.injectSummary(projectId, spaceStats[projectId]);
  324. });
  325. } else if (result && result.spaceStats) {
  326. // 兼容直接返回的情况
  327. const spaceStats = result.spaceStats;
  328. Object.keys(spaceStats).forEach(projectId => {
  329. this.projectSpaceDeliverableService.injectSummary(projectId, spaceStats[projectId]);
  330. });
  331. }
  332. // 兼容返回格式
  333. if (result && (result.stats || result.workload)) return result;
  334. if (result && result.data) return result.data;
  335. if (result && result.result) return result.result;
  336. return result;
  337. } catch (error) {
  338. console.error('❌ 云函数调用失败:', error);
  339. return null;
  340. }
  341. }
  342. // 辅助方法:统一处理返回结果格式
  343. private normalizeResult(result: any) {
  344. if (!result) return null;
  345. if (result.stats || result.workload) return result;
  346. if (result.data) return result.data;
  347. if (result.result) return result.result;
  348. return result;
  349. }
  350. /**
  351. * 获取默认KPI统计(无数据时)
  352. */
  353. private getDefaultKPIStats() {
  354. return {
  355. totalProjects: 0,
  356. inProgressProjects: 0,
  357. completedProjects: 0,
  358. overdueProjects: 0,
  359. dueSoonProjects: 0,
  360. totalDesigners: 0,
  361. availableDesigners: 0,
  362. busyDesigners: 0,
  363. overloadedDesigners: 0
  364. };
  365. }
  366. }