mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

tasks.ts

Language TypeScript4.0 KB113 lines

packages/bitrix-client/src/tasks.ts

  1. /**
  2. * Bitrix tasks: the only source that can see people who hold neither orders
  3. * nor tracker rows.
  4. *
  5. * A warning about this data: most of the oldest open tasks are junk, such as
  6. * duplicate entries from a runaway automation, rows titled "test", Bitrix
  7. * onboarding defaults nobody ever completed ("Fill out profile", "Invite
  8. * colleagues", "Download Bitrix24 application") and demos, some overdue by
  9. * a year.
  10. *
  11. * So a raw task count is not a workload signal; it is mostly automation exhaust.
  12. * Junk is flagged and excluded from the count, and the excluded number is shown
  13. * on the page. Silent filtering is how a dashboard starts lying, and a count
  14. * dominated by one broken automation is how it loses its audience.
  15. */
  16. import type { BitrixClient } from './http.js'
  17. export interface RawTask {
  18. id?: string | number
  19. title?: string
  20. responsibleId?: string | number
  21. RESPONSIBLE_ID?: string | number
  22. TITLE?: string
  23. ID?: string | number
  24. deadline?: string | null
  25. DEADLINE?: string | null
  26. status?: string | number
  27. STATUS?: string | number
  28. createdDate?: string
  29. CREATED_DATE?: string
  30. }
  31. export interface Task {
  32. id: string
  33. title: string
  34. responsibleId: string | null
  35. deadline: string | null
  36. createdAt: string | null
  37. ageDays: number | null
  38. overdue: boolean
  39. junk: boolean
  40. junkReason: string | null
  41. }
  42. /**
  43. * Patterns confirmed against the live open-task list. Conservative: each one
  44. * matches something unambiguous, because a real task wrongly hidden is worse
  45. * than a noisy row shown.
  46. */
  47. const JUNK_PATTERNS: { re: RegExp; reason: string }[] = [
  48. { re: /^FedEx Invoice Review$/i, reason: 'runaway automation: 16 identical copies' },
  49. { re: /^test$/i, reason: 'test task' },
  50. { re: /^(Demo - Task|Task Demo)$/i, reason: 'demo task' },
  51. { re: /^Fill out profile$/i, reason: 'Bitrix onboarding default' },
  52. { re: /^Invite colleagues$/i, reason: 'Bitrix onboarding default' },
  53. { re: /^Download Bitrix24 application$/i, reason: 'Bitrix onboarding default' },
  54. ]
  55. export function isJunkTask(title: string): { junk: boolean; reason: string | null } {
  56. for (const p of JUNK_PATTERNS) if (p.re.test(title.trim())) return { junk: true, reason: p.reason }
  57. return { junk: false, reason: null }
  58. }
  59. const pick = <T>(...v: (T | undefined | null)[]) => v.find((x) => x !== undefined && x !== null) ?? null
  60. export function normalizeTask(raw: RawTask, now = new Date()): Task {
  61. const title = String(pick(raw.title, raw.TITLE) ?? '').trim() || '(untitled)'
  62. const { junk, reason } = isJunkTask(title)
  63. const deadline = pick(raw.deadline, raw.DEADLINE)
  64. const created = pick(raw.createdDate, raw.CREATED_DATE)
  65. const responsible = pick(raw.responsibleId, raw.RESPONSIBLE_ID)
  66. return {
  67. id: String(pick(raw.id, raw.ID) ?? ''),
  68. title,
  69. responsibleId: responsible !== null ? String(responsible) : null,
  70. deadline: deadline ?? null,
  71. createdAt: created ?? null,
  72. ageDays: created ? Math.floor((now.getTime() - Date.parse(created)) / 86_400_000) : null,
  73. overdue: deadline ? Date.parse(deadline) < now.getTime() : false,
  74. junk,
  75. junkReason: reason,
  76. }
  77. }
  78. export class TasksResource {
  79. constructor(private readonly http: BitrixClient) {}
  80. /**
  81. * Every open task across the company, not just the caller's.
  82. *
  83. * `tasks.task.list` nests results under `result.tasks` rather than returning a
  84. * bare array like the crm.* methods, which is why this does not use listAll.
  85. */
  86. async listOpen(now = new Date()): Promise<Task[]> {
  87. const page = await this.http.call<{ tasks?: RawTask[] }>('tasks.task.list', {
  88. filter: { '<REAL_STATUS': 5 }, // anything not completed
  89. select: ['ID', 'TITLE', 'RESPONSIBLE_ID', 'DEADLINE', 'STATUS', 'CREATED_DATE'],
  90. order: { DEADLINE: 'asc' },
  91. })
  92. const rows = page.result?.tasks ?? []
  93. return rows.map((r) => normalizeTask(r, now))
  94. }
  95. }
  96. /** Split real work from automation exhaust, keeping the excluded count visible. */
  97. export function partitionTasks(tasks: Task[]): { real: Task[]; junk: Task[] } {
  98. return {
  99. real: tasks.filter((t) => !t.junk),
  100. junk: tasks.filter((t) => t.junk),
  101. }
  102. }