AidopWorkbenchService.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Entity;
  3. using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
  4. using Admin.NET.Plugin.AiDOP.Entity.S8;
  5. using Admin.NET.Plugin.AiDOP.Infrastructure;
  6. using Admin.NET.Plugin.AiDOP.SmartOps;
  7. using Admin.NET.Plugin.AiDOP.Workbench.Dto;
  8. using Admin.NET.Plugin.ApprovalFlow;
  9. using Admin.NET.Plugin.ApprovalFlow.Service;
  10. using Microsoft.Extensions.Logging;
  11. namespace Admin.NET.Plugin.AiDOP.Workbench;
  12. public sealed class AidopWorkbenchService : ITransient
  13. {
  14. private readonly ISqlSugarClient _db;
  15. private readonly SmartOpsTrustedScopeResolver _trustedScope;
  16. private readonly FlowTaskService _flowTasks;
  17. private readonly ILogger<AidopWorkbenchService> _logger;
  18. public AidopWorkbenchService(
  19. ISqlSugarClient db,
  20. SmartOpsTrustedScopeResolver trustedScope,
  21. FlowTaskService flowTasks,
  22. ILogger<AidopWorkbenchService> logger)
  23. {
  24. _db = db;
  25. _trustedScope = trustedScope;
  26. _flowTasks = flowTasks;
  27. _logger = logger;
  28. }
  29. public async Task<AidopWorkbenchSummaryDto> GetSummaryAsync(int top)
  30. {
  31. var scope = await _trustedScope.ResolveAsync();
  32. top = Math.Clamp(top <= 0 ? 6 : top, 1, 20);
  33. var dto = new AidopWorkbenchSummaryDto
  34. {
  35. GeneratedAt = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:sszzz"),
  36. };
  37. dto.ImprovementActions = await SafeSection("improvement", () => LoadImprovementAsync(scope, top, dto.Counts));
  38. dto.ApprovalTasks = await SafeSection("approval", () => LoadApprovalsAsync(top, dto.Counts));
  39. dto.S8Tasks = await SafeSection("s8", () => LoadS8Async(scope, top, dto.Counts));
  40. dto.UnreadNotices = await SafeSection("notice", () => LoadNoticesAsync(scope, top, dto.Counts));
  41. dto.Pending = MergePending(dto, top);
  42. dto.Counts.PendingTotal = dto.Pending.Total;
  43. dto.Initiated = await SafeSection("initiated", () => LoadInitiatedAsync(scope, top, dto.Counts));
  44. dto.Completed = await SafeSection("completed", () => LoadCompletedAsync(scope, top, dto.Counts));
  45. return dto;
  46. }
  47. private async Task<AidopWorkbenchSectionDto> SafeSection(string name, Func<Task<AidopWorkbenchSectionDto>> factory)
  48. {
  49. try
  50. {
  51. return await factory();
  52. }
  53. catch (Exception ex)
  54. {
  55. _logger.LogWarning(ex, "工作台分区加载失败 section={Section}", name);
  56. return new AidopWorkbenchSectionDto
  57. {
  58. Available = false,
  59. ErrorCode = "SECTION_FAILED",
  60. Message = "该分区暂时不可用",
  61. };
  62. }
  63. }
  64. private async Task<AidopWorkbenchSectionDto> LoadImprovementAsync(SmartOpsTrustedScope scope, int top, AidopWorkbenchCountsDto counts)
  65. {
  66. var today = DateTime.Today;
  67. var q = _db.Queryable<AdoSmartOpsImprovementAction>()
  68. .InnerJoin<AdoSmartOpsImprovementPlan>((a, p) => a.PlanId == p.Id && a.TenantId == p.TenantId)
  69. .Where((a, p) => a.TenantId == scope.TenantId && a.OwnerUserId == scope.UserId)
  70. .Where((a, p) => p.Status == "executing" || p.Status == "pending_verify")
  71. .Where((a, p) => a.Status == SmartOpsImprovementActionStatus.Pending
  72. || a.Status == SmartOpsImprovementActionStatus.InProgress);
  73. var open = await q.Clone().CountAsync();
  74. var dueSoon = await q.Clone()
  75. .Where((a, p) => a.DueDate != null && a.DueDate.Value.Date >= today && a.DueDate.Value.Date <= today.AddDays(1))
  76. .CountAsync();
  77. var overdue = await q.Clone()
  78. .Where((a, p) => a.DueDate != null && a.DueDate.Value.Date < today)
  79. .CountAsync();
  80. counts.OverdueTotal += overdue;
  81. counts.DueToday += await q.Clone()
  82. .Where((a, p) => a.DueDate != null && a.DueDate.Value >= today && a.DueDate.Value < today.AddDays(1))
  83. .CountAsync();
  84. counts.ImprovementOpen = open;
  85. counts.ImprovementDueSoon = dueSoon;
  86. counts.ImprovementOverdue = overdue;
  87. var rows = await q.Clone().OrderBy((a, p) => a.DueDate).Select((a, p) => a).Take(top).ToListAsync();
  88. var planIds = rows.Select(x => x.PlanId).Distinct().ToList();
  89. var plans = planIds.Count == 0
  90. ? new List<AdoSmartOpsImprovementPlan>()
  91. : await _db.Queryable<AdoSmartOpsImprovementPlan>()
  92. .Where(x => x.TenantId == scope.TenantId && planIds.Contains(x.Id))
  93. .ToListAsync();
  94. var planMap = plans.ToDictionary(x => x.Id, x => x);
  95. var kickoffs = await _db.Queryable<AdoSmartOpsImprovementPlan>()
  96. .Where(p => p.TenantId == scope.TenantId && p.Status == "approved" && p.OwnerUserId == scope.UserId)
  97. .OrderByDescending(p => p.UpdateTime)
  98. .Take(top)
  99. .ToListAsync();
  100. var kickoffCount = await _db.Queryable<AdoSmartOpsImprovementPlan>()
  101. .Where(p => p.TenantId == scope.TenantId && p.Status == "approved" && p.OwnerUserId == scope.UserId)
  102. .CountAsync();
  103. counts.ImprovementOpen = open + kickoffCount;
  104. var items = kickoffs.Select(p => new AidopWorkbenchItemDto
  105. {
  106. Id = $"start-{p.Id}",
  107. Source = "ImprovementPlanStart",
  108. Title = $"开始执行改善计划-{p.PlanNo}",
  109. Status = "pending",
  110. Extra = "审批已通过,请总负责人开始执行",
  111. CurrentHandler = "总负责人开始执行",
  112. PlanId = p.Id.ToString(),
  113. PlanNo = p.PlanNo,
  114. Category = "改善计划",
  115. CreatedAt = p.UpdateTime.ToString("yyyy-MM-dd HH:mm"),
  116. JumpUrl = $"/aidop/smart-ops/improvement-plans?id={p.Id}",
  117. })
  118. .Concat(rows.Select(x =>
  119. {
  120. planMap.TryGetValue(x.PlanId, out var plan);
  121. return new AidopWorkbenchItemDto
  122. {
  123. Id = x.Id.ToString(),
  124. Source = "SmartOpsAction",
  125. Title = x.Content,
  126. Status = SmartOpsImprovementActionStatus.DisplayStatus(x.Status, x.DueDate, today),
  127. DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
  128. Overdue = SmartOpsImprovementActionStatus.IsOverdue(x.Status, x.DueDate, today),
  129. PlanId = x.PlanId.ToString(),
  130. PlanNo = plan?.PlanNo,
  131. Category = "改善行动",
  132. CreatedAt = x.CreateTime.ToString("yyyy-MM-dd HH:mm"),
  133. JumpUrl = $"/aidop/smart-ops/improvement-plans?id={x.PlanId}&actionId={x.Id}",
  134. };
  135. }))
  136. .Take(top)
  137. .ToList();
  138. return new AidopWorkbenchSectionDto
  139. {
  140. Total = open + kickoffCount,
  141. Items = items,
  142. };
  143. }
  144. private async Task<AidopWorkbenchSectionDto> LoadApprovalsAsync(int top, AidopWorkbenchCountsDto counts)
  145. {
  146. counts.ApprovalPending = await _flowTasks.MyPendingCount();
  147. var page = await _flowTasks.MyPendingPage(new TaskPageInput { Page = 1, PageSize = top });
  148. return new AidopWorkbenchSectionDto
  149. {
  150. Total = counts.ApprovalPending,
  151. Items = (page.Items ?? new List<TaskPageOutput>()).Select(t => new AidopWorkbenchItemDto
  152. {
  153. Id = t.Id.ToString(),
  154. Source = "Approval",
  155. Title = string.IsNullOrWhiteSpace(t.Title) ? t.BizType : t.Title,
  156. Status = "pending",
  157. Extra = string.IsNullOrWhiteSpace(t.NodeName) ? t.BizType : $"当前节点:{t.NodeName}",
  158. PlanNo = t.BizNo,
  159. CurrentHandler = t.NodeName,
  160. Category = "审批",
  161. CreatedAt = t.CreateTime.ToString("yyyy-MM-dd HH:mm"),
  162. JumpUrl = ApprovalJumpUrl(t),
  163. }).ToList(),
  164. };
  165. }
  166. private async Task<AidopWorkbenchSectionDto> LoadS8Async(SmartOpsTrustedScope scope, int top, AidopWorkbenchCountsDto counts)
  167. {
  168. var recIds = await _db.Queryable<AdoS0EmployeeMaster>().ClearFilter()
  169. .Where(x => x.SysUserId == scope.UserId)
  170. .Select(x => x.Id)
  171. .ToListAsync();
  172. if (recIds.Count == 0)
  173. {
  174. return new AidopWorkbenchSectionDto
  175. {
  176. Available = false,
  177. ErrorCode = "EMPLOYEE_UNBOUND",
  178. Message = "当前账号未绑定员工主数据,无法列出个人 S8 事项",
  179. };
  180. }
  181. var assignedQ = _db.Queryable<AdoS8Exception>()
  182. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted && recIds.Contains(x.AssigneeId ?? 0))
  183. .Where(x => x.Status != "CLOSED" && x.Status != "REJECTED" && x.Status != "RECOVERED");
  184. var verifyQ = _db.Queryable<AdoS8Exception>()
  185. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted && recIds.Contains(x.VerifierId ?? 0))
  186. .Where(x => x.Status == "PENDING_VERIFICATION");
  187. counts.S8Assigned = await assignedQ.Clone().CountAsync();
  188. counts.S8Verification = await verifyQ.Clone().CountAsync();
  189. var today = DateTime.Today;
  190. counts.DueToday += await _db.Queryable<AdoS8Exception>()
  191. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted)
  192. .Where(x => x.Status != "CLOSED" && x.Status != "REJECTED" && x.Status != "RECOVERED")
  193. .Where(x => recIds.Contains(x.AssigneeId ?? 0) || recIds.Contains(x.VerifierId ?? 0))
  194. .Where(x => x.SlaDeadline != null && x.SlaDeadline >= today && x.SlaDeadline < today.AddDays(1))
  195. .CountAsync();
  196. counts.OverdueTotal += await _db.Queryable<AdoS8Exception>()
  197. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted)
  198. .Where(x => x.Status != "CLOSED" && x.Status != "REJECTED" && x.Status != "RECOVERED")
  199. .Where(x => recIds.Contains(x.AssigneeId ?? 0) || recIds.Contains(x.VerifierId ?? 0))
  200. .Where(x => x.TimeoutFlag || (x.SlaDeadline != null && x.SlaDeadline < DateTime.Now))
  201. .CountAsync();
  202. var assigned = await assignedQ.Clone().OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(top).ToListAsync();
  203. var verify = await verifyQ.Clone().OrderByDescending(x => x.UpdatedAt ?? x.CreatedAt).Take(top).ToListAsync();
  204. var merged = assigned.Concat(verify)
  205. .GroupBy(x => x.Id)
  206. .Select(g => g.First())
  207. .Take(top)
  208. .ToList();
  209. return new AidopWorkbenchSectionDto
  210. {
  211. Total = counts.S8Assigned + counts.S8Verification,
  212. Items = merged.Select(x => new AidopWorkbenchItemDto
  213. {
  214. Id = x.Id.ToString(),
  215. Source = "S8Exception",
  216. Title = string.IsNullOrWhiteSpace(x.Title) ? x.ExceptionCode : x.Title,
  217. Status = x.Status,
  218. DueDate = x.SlaDeadline?.ToString("yyyy-MM-dd"),
  219. Overdue = x.TimeoutFlag || (x.SlaDeadline.HasValue && x.SlaDeadline < DateTime.Now && x.Status != "CLOSED"),
  220. Extra = x.ExceptionCode,
  221. Category = "异常",
  222. CreatedAt = x.CreatedAt.ToString("yyyy-MM-dd HH:mm"),
  223. JumpUrl = $"/aidop/s8/exceptions/{x.Id}",
  224. }).ToList(),
  225. };
  226. }
  227. private async Task<AidopWorkbenchSectionDto> LoadNoticesAsync(SmartOpsTrustedScope scope, int top, AidopWorkbenchCountsDto counts)
  228. {
  229. var q = _db.Queryable<SysNoticeUser>()
  230. .Includes(u => u.SysNotice)
  231. .Where(u => u.UserId == scope.UserId && u.ReadStatus == NoticeUserStatusEnum.UNREAD);
  232. counts.NoticeUnread = await q.Clone().CountAsync();
  233. var rows = await q.Clone()
  234. .OrderByDescending(u => u.SysNotice.CreateTime)
  235. .Take(top)
  236. .ToListAsync();
  237. return new AidopWorkbenchSectionDto
  238. {
  239. Total = counts.NoticeUnread,
  240. Items = rows.Where(x => x.SysNotice != null).Select(x => new AidopWorkbenchItemDto
  241. {
  242. Id = x.SysNotice.Id.ToString(),
  243. Source = "SysNotice",
  244. Title = x.SysNotice.Title,
  245. Status = "unread",
  246. Extra = x.SysNotice.PublicTime?.ToString("yyyy-MM-dd HH:mm"),
  247. Category = "消息",
  248. CreatedAt = x.SysNotice.CreateTime.ToString("yyyy-MM-dd HH:mm"),
  249. JumpUrl = $"/dashboard/notice?noticeId={x.SysNotice.Id}",
  250. }).ToList(),
  251. };
  252. }
  253. private static AidopWorkbenchSectionDto MergePending(AidopWorkbenchSummaryDto dto, int top)
  254. {
  255. var sections = new[] { dto.ImprovementActions, dto.ApprovalTasks, dto.S8Tasks };
  256. var items = sections.SelectMany(x => x.Items)
  257. .OrderByDescending(x => x.Overdue)
  258. .ThenBy(x => ParseDate(x.DueDate) ?? DateTime.MaxValue)
  259. .ThenByDescending(x => ParseDate(x.CreatedAt) ?? DateTime.MinValue)
  260. .Take(top)
  261. .ToList();
  262. return new AidopWorkbenchSectionDto
  263. {
  264. Available = sections.Any(x => x.Available),
  265. Total = sections.Sum(x => x.Total),
  266. Items = items,
  267. Message = sections.All(x => !x.Available) ? "待办数据暂时不可用" : null,
  268. };
  269. }
  270. private async Task<AidopWorkbenchSectionDto> LoadInitiatedAsync(
  271. SmartOpsTrustedScope scope,
  272. int top,
  273. AidopWorkbenchCountsDto counts)
  274. {
  275. var actionPlanIds = await _db.Queryable<AdoSmartOpsImprovementAction>()
  276. .Where(x => x.TenantId == scope.TenantId && x.CreateUserId == scope.UserId)
  277. .Select(x => x.PlanId)
  278. .Distinct()
  279. .ToListAsync();
  280. var planCount = actionPlanIds.Count;
  281. var plans = actionPlanIds.Count == 0
  282. ? new List<AdoSmartOpsImprovementPlan>()
  283. : await _db.Queryable<AdoSmartOpsImprovementPlan>()
  284. .Where(x => x.TenantId == scope.TenantId && actionPlanIds.Contains(x.Id))
  285. .OrderByDescending(x => x.CreateTime)
  286. .Take(top)
  287. .ToListAsync();
  288. var recIds = await ResolveEmployeeIds(scope.UserId);
  289. var reportedQ = _db.Queryable<AdoS8Exception>()
  290. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted && recIds.Contains(x.ReporterId ?? 0));
  291. var reportedCount = recIds.Count == 0 ? 0 : await reportedQ.Clone().CountAsync();
  292. var reported = recIds.Count == 0
  293. ? new List<AdoS8Exception>()
  294. : await reportedQ.Clone().OrderByDescending(x => x.CreatedAt).Take(top).ToListAsync();
  295. var flowQ = _db.Queryable<ApprovalFlowInstance>()
  296. .Where(x => x.InitiatorId == scope.UserId)
  297. .Where(x => x.BizType != "SMART_OPS_IMPROVEMENT" && !x.BizType.StartsWith("EXCEPTION_"));
  298. var flowCount = await flowQ.Clone().CountAsync();
  299. var flows = await flowQ.Clone().OrderByDescending(x => x.StartTime).Take(top).ToListAsync();
  300. var items = plans.Select(x => new AidopWorkbenchItemDto
  301. {
  302. Id = x.Id.ToString(),
  303. Source = "ImprovementPlan",
  304. Category = "改善计划",
  305. Title = string.IsNullOrWhiteSpace(x.ProblemName) ? x.PlanNo : x.ProblemName,
  306. Status = x.Status,
  307. DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
  308. PlanId = x.Id.ToString(),
  309. PlanNo = x.PlanNo,
  310. CreatedAt = x.CreateTime.ToString("yyyy-MM-dd HH:mm"),
  311. JumpUrl = $"/aidop/smart-ops/improvement-plans?id={x.Id}",
  312. })
  313. .Concat(reported.Select(x => new AidopWorkbenchItemDto
  314. {
  315. Id = x.Id.ToString(),
  316. Source = "S8Reported",
  317. Category = "异常提报",
  318. Title = string.IsNullOrWhiteSpace(x.Title) ? x.ExceptionCode : x.Title,
  319. Status = x.Status,
  320. DueDate = x.SlaDeadline?.ToString("yyyy-MM-dd"),
  321. Extra = x.ExceptionCode,
  322. CreatedAt = x.CreatedAt.ToString("yyyy-MM-dd HH:mm"),
  323. JumpUrl = $"/aidop/s8/exceptions/{x.Id}",
  324. }))
  325. .Concat(flows.Select(x => new AidopWorkbenchItemDto
  326. {
  327. Id = x.Id.ToString(),
  328. Source = "ApprovalInitiated",
  329. Category = "审批发起",
  330. Title = x.Title,
  331. Status = x.Status.ToString(),
  332. Extra = x.BizType,
  333. CreatedAt = x.StartTime.ToString("yyyy-MM-dd HH:mm"),
  334. CompletedAt = x.EndTime?.ToString("yyyy-MM-dd HH:mm"),
  335. JumpUrl = "/aidop/flowManage/approvalFlowCenter",
  336. }))
  337. .OrderByDescending(x => ParseDate(x.CreatedAt) ?? DateTime.MinValue)
  338. .Take(top)
  339. .ToList();
  340. counts.InitiatedTotal = planCount + reportedCount + flowCount;
  341. return new AidopWorkbenchSectionDto { Total = counts.InitiatedTotal, Items = items };
  342. }
  343. private async Task<AidopWorkbenchSectionDto> LoadCompletedAsync(
  344. SmartOpsTrustedScope scope,
  345. int top,
  346. AidopWorkbenchCountsDto counts)
  347. {
  348. var actionsQ = _db.Queryable<AdoSmartOpsImprovementAction>()
  349. .Where(x => x.TenantId == scope.TenantId && x.OwnerUserId == scope.UserId)
  350. .Where(x => x.Status == SmartOpsImprovementActionStatus.Completed);
  351. var actionCount = await actionsQ.Clone().CountAsync();
  352. var actions = await actionsQ.Clone()
  353. .OrderByDescending(x => x.CompletedAt ?? x.UpdateTime)
  354. .Take(top)
  355. .ToListAsync();
  356. var recIds = await ResolveEmployeeIds(scope.UserId);
  357. var closedQ = _db.Queryable<AdoS8Exception>()
  358. .Where(x => x.TenantId == scope.TenantId && !x.IsDeleted)
  359. .Where(x => x.Status == "CLOSED" || x.Status == "REJECTED" || x.Status == "RECOVERED")
  360. .Where(x => recIds.Contains(x.AssigneeId ?? 0) || recIds.Contains(x.VerifierId ?? 0));
  361. var closedCount = recIds.Count == 0 ? 0 : await closedQ.Clone().CountAsync();
  362. var closed = recIds.Count == 0
  363. ? new List<AdoS8Exception>()
  364. : await closedQ.Clone().OrderByDescending(x => x.ClosedAt ?? x.UpdatedAt).Take(top).ToListAsync();
  365. var donePage = await _flowTasks.MyDonePage(new TaskPageInput { Page = 1, PageSize = top });
  366. var doneCount = donePage.Total;
  367. var done = donePage.Items ?? new List<TaskPageOutput>();
  368. var items = actions.Select(x => new AidopWorkbenchItemDto
  369. {
  370. Id = x.Id.ToString(),
  371. Source = "SmartOpsActionCompleted",
  372. Category = "改善行动",
  373. Title = x.Content,
  374. Status = x.Status,
  375. DueDate = x.DueDate?.ToString("yyyy-MM-dd"),
  376. PlanId = x.PlanId.ToString(),
  377. CompletedAt = (x.CompletedAt ?? x.UpdateTime).ToString("yyyy-MM-dd HH:mm"),
  378. JumpUrl = $"/aidop/smart-ops/improvement-plans?id={x.PlanId}&actionId={x.Id}",
  379. })
  380. .Concat(closed.Select(x => new AidopWorkbenchItemDto
  381. {
  382. Id = x.Id.ToString(),
  383. Source = "S8Completed",
  384. Category = "异常",
  385. Title = string.IsNullOrWhiteSpace(x.Title) ? x.ExceptionCode : x.Title,
  386. Status = x.Status,
  387. Extra = x.ExceptionCode,
  388. CompletedAt = (x.ClosedAt ?? x.UpdatedAt ?? x.CreatedAt).ToString("yyyy-MM-dd HH:mm"),
  389. JumpUrl = $"/aidop/s8/exceptions/{x.Id}",
  390. }))
  391. .Concat(done.Select(x => new AidopWorkbenchItemDto
  392. {
  393. Id = x.Id.ToString(),
  394. Source = "ApprovalCompleted",
  395. Category = "审批",
  396. Title = string.IsNullOrWhiteSpace(x.Title) ? x.BizType : x.Title,
  397. Status = x.Status.ToString(),
  398. Extra = x.BizType,
  399. CompletedAt = x.ActionTime?.ToString("yyyy-MM-dd HH:mm"),
  400. JumpUrl = ApprovalJumpUrl(x),
  401. }))
  402. .OrderByDescending(x => ParseDate(x.CompletedAt) ?? DateTime.MinValue)
  403. .Take(top)
  404. .ToList();
  405. counts.CompletedTotal = actionCount + closedCount + doneCount;
  406. return new AidopWorkbenchSectionDto { Total = counts.CompletedTotal, Items = items };
  407. }
  408. private async Task<List<long>> ResolveEmployeeIds(long userId)
  409. {
  410. return await _db.Queryable<AdoS0EmployeeMaster>().ClearFilter()
  411. .Where(x => x.SysUserId == userId)
  412. .Select(x => x.Id)
  413. .ToListAsync();
  414. }
  415. private static DateTime? ParseDate(string? value) =>
  416. DateTime.TryParse(value, out var parsed) ? parsed : null;
  417. private static string ApprovalJumpUrl(TaskPageOutput t)
  418. {
  419. var bizType = t.BizType ?? "";
  420. if (bizType.StartsWith("EXCEPTION", StringComparison.OrdinalIgnoreCase) && t.BizId > 0)
  421. return $"/aidop/s8/exceptions/{t.BizId}";
  422. if (string.Equals(bizType, "SMART_OPS_IMPROVEMENT", StringComparison.OrdinalIgnoreCase) && t.BizId > 0)
  423. return $"/aidop/smart-ops/improvement-plans?id={t.BizId}";
  424. return $"/aidop/flowManage/approvalFlowCenter?taskId={t.Id}";
  425. }
  426. }