task-timer-sync.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import { useEffect, useState } from 'react';
  2. import emitter, { EVENTS } from 'app/context/emitter';
  3. import { fetcher } from 'app/lib/fetcher';
  4. import { clockToSeconds } from 'app/lib/tasks-meta';
  5. export const TASK_TIMER_SYNC_EVENT = EVENTS.taskTimerSync;
  6. const sessions = new Map();
  7. /** `bx_tasks/process_timer/&params[]=reload&params[]={taskId}&params[]={context}` */
  8. export function parseTimerContentId(requestUrl: string | null | undefined) {
  9. const values = [...String(requestUrl || '').matchAll(/params\[\]=([^&]*)/g)]
  10. .map((match) => decodeURIComponent(match[1] || ''));
  11. const taskId = values[1];
  12. if (taskId == null || taskId === '' || taskId === 'reload') return null;
  13. return taskId;
  14. }
  15. export function getTaskTimerSession(taskId: number | string | null | undefined) {
  16. if (taskId == null || taskId === '') return null;
  17. return sessions.get(String(taskId)) || null;
  18. }
  19. function emitSession(session: any) {
  20. emitter.emit(TASK_TIMER_SYNC_EVENT, session);
  21. }
  22. export function setTaskTimerSession(partial: any) {
  23. const taskId = partial.taskId != null && partial.taskId !== ''
  24. ? String(partial.taskId)
  25. : parseTimerContentId(partial.requestUrl);
  26. if (!taskId) return null;
  27. const prev = sessions.get(taskId) || {};
  28. const next = {
  29. ...prev,
  30. ...partial,
  31. taskId,
  32. state: partial.state ?? prev.state ?? null,
  33. seconds: partial.seconds ?? prev.seconds ?? 0,
  34. };
  35. sessions.set(taskId, next);
  36. if (next.timerId != null) sessions.set(`timer:${next.timerId}`, next);
  37. emitSession(next);
  38. return next;
  39. }
  40. export function publishTimerSession({
  41. requestUrl,
  42. timerId,
  43. state,
  44. time,
  45. action,
  46. taskId,
  47. }: { requestUrl?: string; timerId?: any; state?: any; time?: any; action?: any; taskId?: any } = {}) {
  48. const contentId = taskId ?? parseTimerContentId(requestUrl);
  49. if (contentId == null) return null;
  50. const actionKey = String(action || '').toLowerCase();
  51. const nextState = state
  52. || (actionKey === 'start' || actionKey === 'resume' ? 'started' : null)
  53. || (['pause', 'stop', 'log', 'clear'].includes(actionKey) ? 'paused' : null);
  54. if (nextState === 'started') {
  55. for (const [key, session] of sessions) {
  56. if (key.startsWith('timer:')) continue;
  57. if (String(session.taskId) === String(contentId)) continue;
  58. if (session.state !== 'started') continue;
  59. const paused = { ...session, state: 'paused' };
  60. sessions.set(String(session.taskId), paused);
  61. emitSession(paused);
  62. }
  63. }
  64. return setTaskTimerSession({
  65. taskId: contentId,
  66. requestUrl,
  67. timerId,
  68. state: nextState,
  69. seconds: actionKey === 'log' || actionKey === 'clear'
  70. ? 0
  71. : (clockToSeconds(time) ?? 0),
  72. });
  73. }
  74. export function ingestTimersFromPage(payload: any) {
  75. const seen = new Set();
  76. const walk = (node: any) => {
  77. if (!node || typeof node !== 'object' || seen.has(node)) return;
  78. seen.add(node);
  79. if (Array.isArray(node.timers)) {
  80. for (const item of node.timers) {
  81. if (!item?.timer) continue;
  82. publishTimerSession({
  83. requestUrl: item.timer.request_url,
  84. timerId: item.timer.id,
  85. state: item.timer.state,
  86. time: item.timer.time,
  87. taskId: parseTimerContentId(item.timer.request_url),
  88. });
  89. }
  90. }
  91. const values = Array.isArray(node) ? node : Object.values(node);
  92. for (const value of values) walk(value);
  93. };
  94. walk(payload?.data || payload);
  95. }
  96. let loadPromise: Promise<any> | null = null;
  97. export async function loadTaskTimerSessions() {
  98. if (loadPromise) return loadPromise;
  99. loadPromise = (async () => {
  100. try {
  101. const response = await fetcher('/api.php?r=system/get_page_by_request/TemplServicePages&params[]=tasks-timers');
  102. ingestTimersFromPage(response);
  103. } catch {
  104. // Browse rows still render from the time string; live state stays unknown.
  105. } finally {
  106. loadPromise = null;
  107. }
  108. })();
  109. return loadPromise;
  110. }
  111. export function useTaskTimerSession(taskId: number | string | null | undefined) {
  112. const [session, setSession] = useState(() => getTaskTimerSession(taskId));
  113. useEffect(() => {
  114. const apply = (next: any) => {
  115. if (next == null || String(next.taskId) !== String(taskId)) return;
  116. setSession(next);
  117. };
  118. apply(getTaskTimerSession(taskId));
  119. const subscription = emitter.addListener(TASK_TIMER_SYNC_EVENT, apply);
  120. return () => subscription.remove();
  121. }, [taskId]);
  122. return taskId == null ? null : session;
  123. }