tasks.ts
Language TypeScript4.0 KB113 lines
/*** Bitrix tasks: the only source that can see people who hold neither orders* nor tracker rows.** A warning about this data: most of the oldest open tasks are junk, such as* duplicate entries from a runaway automation, rows titled "test", Bitrix* onboarding defaults nobody ever completed ("Fill out profile", "Invite* colleagues", "Download Bitrix24 application") and demos, some overdue by* a year.** So a raw task count is not a workload signal; it is mostly automation exhaust.* Junk is flagged and excluded from the count, and the excluded number is shown* on the page. Silent filtering is how a dashboard starts lying, and a count* dominated by one broken automation is how it loses its audience.*/import type { BitrixClient } from './http.js'export interface RawTask {id?: string | numbertitle?: stringresponsibleId?: string | numberRESPONSIBLE_ID?: string | numberTITLE?: stringID?: string | numberdeadline?: string | nullDEADLINE?: string | nullstatus?: string | numberSTATUS?: string | numbercreatedDate?: stringCREATED_DATE?: string}export interface Task {id: stringtitle: stringresponsibleId: string | nulldeadline: string | nullcreatedAt: string | nullageDays: number | nulloverdue: booleanjunk: booleanjunkReason: string | null}/*** Patterns confirmed against the live open-task list. Conservative: each one* matches something unambiguous, because a real task wrongly hidden is worse* than a noisy row shown.*/const JUNK_PATTERNS: { re: RegExp; reason: string }[] = [{ re: /^FedEx Invoice Review$/i, reason: 'runaway automation: 16 identical copies' },{ re: /^test$/i, reason: 'test task' },{ re: /^(Demo - Task|Task Demo)$/i, reason: 'demo task' },{ re: /^Fill out profile$/i, reason: 'Bitrix onboarding default' },{ re: /^Invite colleagues$/i, reason: 'Bitrix onboarding default' },{ re: /^Download Bitrix24 application$/i, reason: 'Bitrix onboarding default' },]export function isJunkTask(title: string): { junk: boolean; reason: string | null } {for (const p of JUNK_PATTERNS) if (p.re.test(title.trim())) return { junk: true, reason: p.reason }return { junk: false, reason: null }}const pick = <T>(...v: (T | undefined | null)[]) => v.find((x) => x !== undefined && x !== null) ?? nullexport function normalizeTask(raw: RawTask, now = new Date()): Task {const title = String(pick(raw.title, raw.TITLE) ?? '').trim() || '(untitled)'const { junk, reason } = isJunkTask(title)const deadline = pick(raw.deadline, raw.DEADLINE)const created = pick(raw.createdDate, raw.CREATED_DATE)const responsible = pick(raw.responsibleId, raw.RESPONSIBLE_ID)return {id: String(pick(raw.id, raw.ID) ?? ''),title,responsibleId: responsible !== null ? String(responsible) : null,deadline: deadline ?? null,createdAt: created ?? null,ageDays: created ? Math.floor((now.getTime() - Date.parse(created)) / 86_400_000) : null,overdue: deadline ? Date.parse(deadline) < now.getTime() : false,junk,junkReason: reason,}}export class TasksResource {constructor(private readonly http: BitrixClient) {}/*** Every open task across the company, not just the caller's.** `tasks.task.list` nests results under `result.tasks` rather than returning a* bare array like the crm.* methods, which is why this does not use listAll.*/async listOpen(now = new Date()): Promise<Task[]> {const page = await this.http.call<{ tasks?: RawTask[] }>('tasks.task.list', {filter: { '<REAL_STATUS': 5 }, // anything not completedselect: ['ID', 'TITLE', 'RESPONSIBLE_ID', 'DEADLINE', 'STATUS', 'CREATED_DATE'],order: { DEADLINE: 'asc' },})const rows = page.result?.tasks ?? []return rows.map((r) => normalizeTask(r, now))}}/** Split real work from automation exhaust, keeping the excluded count visible. */export function partitionTasks(tasks: Task[]): { real: Task[]; junk: Task[] } {return {real: tasks.filter((t) => !t.junk),junk: tasks.filter((t) => t.junk),}}