/** * Bidirectional mapping between rTasks and ClickUp fields. * * Pure functions — no side effects, no API calls. */ // ── Status mapping ── const DEFAULT_STATUS_TO_CLICKUP: Record = { TODO: 'to do', IN_PROGRESS: 'in progress', REVIEW: 'in review', DONE: 'complete', }; const DEFAULT_CLICKUP_TO_STATUS: Record = { 'to do': 'TODO', 'open': 'TODO', 'pending': 'TODO', 'in progress': 'IN_PROGRESS', 'in review': 'REVIEW', 'review': 'REVIEW', 'complete': 'DONE', 'closed': 'DONE', 'done': 'DONE', }; export function toClickUpStatus( rTasksStatus: string, customMap?: Record, ): string { if (customMap?.[rTasksStatus]) return customMap[rTasksStatus]; return DEFAULT_STATUS_TO_CLICKUP[rTasksStatus] || rTasksStatus.toLowerCase().replace(/_/g, ' '); } export function fromClickUpStatus( clickUpStatus: string, reverseMap?: Record, ): string { const lower = clickUpStatus.toLowerCase(); if (reverseMap?.[lower]) return reverseMap[lower]; if (DEFAULT_CLICKUP_TO_STATUS[lower]) return DEFAULT_CLICKUP_TO_STATUS[lower]; // Fuzzy: check if any known keyword is contained for (const [pattern, mapped] of Object.entries(DEFAULT_CLICKUP_TO_STATUS)) { if (lower.includes(pattern)) return mapped; } return 'TODO'; // fallback } // ── Priority mapping ── const PRIORITY_TO_CLICKUP: Record = { URGENT: 1, HIGH: 2, MEDIUM: 3, LOW: 4, }; const CLICKUP_TO_PRIORITY: Record = { 1: 'URGENT', 2: 'HIGH', 3: 'MEDIUM', 4: 'LOW', }; export function toClickUpPriority(priority: string | null): number | null { if (!priority) return null; return PRIORITY_TO_CLICKUP[priority] ?? 3; } export function fromClickUpPriority(cuPriority: number | null | undefined): string | null { if (cuPriority == null) return null; return CLICKUP_TO_PRIORITY[cuPriority] ?? 'MEDIUM'; } // ── Content hashing for change detection ── export async function contentHash(parts: { title: string; description: string; status: string; priority: string | null; }): Promise { const raw = `${parts.title}\x00${parts.description}\x00${parts.status}\x00${parts.priority ?? ''}`; const buf = new TextEncoder().encode(raw); const hash = await crypto.subtle.digest('SHA-256', buf); return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join(''); } // ── Conflict detection ── export interface FieldDiff { field: string; local: string; remote: string; } export function detectConflicts( localFields: { title: string; description: string; status: string; priority: string | null }, remoteFields: { title: string; description: string; status: string; priority: string | null }, baseFields: { title: string; description: string; status: string; priority: string | null }, ): { conflicts: FieldDiff[]; localOnly: string[]; remoteOnly: string[] } { const conflicts: FieldDiff[] = []; const localOnly: string[] = []; const remoteOnly: string[] = []; for (const field of ['title', 'description', 'status', 'priority'] as const) { const base = baseFields[field] ?? ''; const local = localFields[field] ?? ''; const remote = remoteFields[field] ?? ''; const localChanged = local !== base; const remoteChanged = remote !== base; if (localChanged && remoteChanged && local !== remote) { conflicts.push({ field, local, remote }); } else if (localChanged && !remoteChanged) { localOnly.push(field); } else if (!localChanged && remoteChanged) { remoteOnly.push(field); } } return { conflicts, localOnly, remoteOnly }; } // ── ClickUp task → rTasks fields ── export function mapClickUpTaskToRTasks(cuTask: any, reverseStatusMap?: Record): { title: string; description: string; status: string; priority: string | null; labels: string[]; clickUpMeta: { taskId: string; listId: string; url: string; }; } { return { title: cuTask.name || 'Untitled', description: cuTask.description || cuTask.text_content || '', status: fromClickUpStatus(cuTask.status?.status || 'to do', reverseStatusMap), priority: fromClickUpPriority(cuTask.priority?.id ? Number(cuTask.priority.id) : null), labels: (cuTask.tags || []).map((t: any) => t.name), clickUpMeta: { taskId: cuTask.id, listId: cuTask.list?.id || '', url: cuTask.url || `https://app.clickup.com/t/${cuTask.id}`, }, }; } // ── rTasks fields → ClickUp API body ── export function mapRTasksToClickUpBody( task: { title: string; description: string; status: string; priority: string | null; labels: string[] }, statusMap?: Record, ): Record { const body: Record = { name: task.title, description: task.description, status: toClickUpStatus(task.status, statusMap), }; const p = toClickUpPriority(task.priority); if (p != null) body.priority = p; if (task.labels?.length) body.tags = task.labels; return body; } // ── Build reverse status map from ClickUp list statuses ── export function buildStatusMaps(clickUpStatuses: Array<{ status: string; type: string }>): { statusMap: Record; reverseStatusMap: Record; } { const statusMap: Record = {}; const reverseStatusMap: Record = {}; for (const s of clickUpStatuses) { const mapped = fromClickUpStatus(s.status); reverseStatusMap[s.status.toLowerCase()] = mapped; // First ClickUp status for each rTasks status wins if (!statusMap[mapped]) statusMap[mapped] = s.status; } return { statusMap, reverseStatusMap }; }