import { normalizePath } from '@renderer/utils/pathNormalize'; import { splitPath } from '@shared/utils/platformPath'; import { differenceInDays, isToday, isYesterday } from 'date-fns'; import { DATE_CATEGORY_ORDER } from '../types/tabs'; import type { DateCategory } from '../types/tabs'; import type { GlobalTask } from '@shared/types'; export type DateGroupedTasks = Record; export interface ProjectTaskGroup { projectKey: string; projectLabel: string; tasks: GlobalTask[]; } /** Returns updatedAt if available, otherwise createdAt. */ function getEffectiveDate(task: GlobalTask): string | undefined { return task.updatedAt ?? task.createdAt; } function getEffectiveTs(task: GlobalTask): number { const d = getEffectiveDate(task); return d ? new Date(d).getTime() : 0; } /** * Build a map: teamName → max effective timestamp among its tasks. * Used to sort team sub-groups by most recent activity (not alphabetically). */ function buildTeamMaxTs(tasks: GlobalTask[]): Map { const m = new Map(); for (const t of tasks) { const ts = getEffectiveTs(t); const cur = m.get(t.teamName) ?? 0; if (ts > cur) m.set(t.teamName, ts); } return m; } /** * Sort comparator: teams ordered by most recent task (desc), * within the same team — by individual task date (desc). */ function compareByTeamFreshness( a: GlobalTask, b: GlobalTask, teamMaxTs: Map ): number { if (a.teamName !== b.teamName) { const teamTsA = teamMaxTs.get(a.teamName) ?? 0; const teamTsB = teamMaxTs.get(b.teamName) ?? 0; return teamTsB - teamTsA; } return getEffectiveTs(b) - getEffectiveTs(a); } function getDateCategory(dateStr: string | undefined): DateCategory { if (!dateStr) return 'Older'; const d = new Date(dateStr); if (isNaN(d.getTime())) return 'Older'; if (isToday(d)) return 'Today'; if (isYesterday(d)) return 'Yesterday'; if (differenceInDays(new Date(), d) <= 7) return 'Previous 7 Days'; return 'Older'; } export function groupTasksByDate(tasks: GlobalTask[]): DateGroupedTasks { const groups: DateGroupedTasks = { Today: [], Yesterday: [], 'Previous 7 Days': [], Older: [], }; for (const task of tasks) { const cat = getDateCategory(getEffectiveDate(task)); groups[cat].push(task); } for (const cat of DATE_CATEGORY_ORDER) { const teamTs = buildTeamMaxTs(groups[cat]); groups[cat].sort((a, b) => compareByTeamFreshness(a, b, teamTs)); } return groups; } export function getNonEmptyTaskCategories(groups: DateGroupedTasks): DateCategory[] { return DATE_CATEGORY_ORDER.filter((cat) => groups[cat].length > 0); } const NO_PROJECT_KEY = '__no_project__'; const NO_PROJECT_LABEL = 'Without project'; function trimTrailingPathSep(p: string): string { let s = p; while (s.length > 0 && (s.endsWith('/') || s.endsWith('\\'))) s = s.slice(0, -1); return s; } export function projectLabelFromPath(path: string): string { const normalized = trimTrailingPathSep(path); const segments = splitPath(normalized); return segments.length > 0 ? segments[segments.length - 1] : path || NO_PROJECT_LABEL; } /** * Flat sort: newest (by updatedAt/createdAt) first, no grouping. * Within the same team, tasks are ordered by freshness. * Teams with more recent activity appear first. */ export function sortTasksByFreshness(tasks: GlobalTask[]): GlobalTask[] { const teamTs = buildTeamMaxTs(tasks); return [...tasks].sort((a, b) => compareByTeamFreshness(a, b, teamTs)); } export function groupTasksByProject(tasks: GlobalTask[]): ProjectTaskGroup[] { const byKey = new Map(); for (const task of tasks) { const path = task.projectPath?.trim() ?? ''; const key = path ? normalizePath(trimTrailingPathSep(path)) : NO_PROJECT_KEY; let entry = byKey.get(key); if (!entry) { entry = { path: path || '', tasks: [] }; byKey.set(key, entry); } entry.tasks.push(task); } for (const entry of byKey.values()) { const teamTs = buildTeamMaxTs(entry.tasks); entry.tasks.sort((a, b) => compareByTeamFreshness(a, b, teamTs)); } const groups: ProjectTaskGroup[] = []; for (const [key, { path, tasks: list }] of byKey) { const projectLabel = key === NO_PROJECT_KEY ? NO_PROJECT_LABEL : projectLabelFromPath(path); groups.push({ projectKey: key, projectLabel, tasks: list }); } groups.sort((a, b) => { const tsA = Math.max(...a.tasks.map(getEffectiveTs)); const tsB = Math.max(...b.tasks.map(getEffectiveTs)); return tsB - tsA; }); return groups; }