Files
kandev/apps/web/app/office/tasks/use-tasks-tree.ts
T
2026-05-16 20:07:57 +01:00

173 lines
4.6 KiB
TypeScript

import { useMemo } from "react";
import { useAppStore } from "@/components/state-provider";
import type {
OfficeTask,
OfficeTaskStatus,
TaskSortField,
TaskSortDir,
TaskFilterState,
} from "@/lib/state/slices/office/types";
const FALLBACK_STATUS_ORDER: Record<OfficeTaskStatus, number> = {
backlog: 0,
todo: 1,
in_progress: 2,
in_review: 3,
blocked: 4,
done: 5,
cancelled: 6,
};
const FALLBACK_PRIORITY_ORDER: Record<string, number> = {
critical: 0,
high: 1,
medium: 2,
low: 3,
none: 4,
};
function matchesFilters(task: OfficeTask, filters: TaskFilterState): boolean {
if (filters.search && !task.title.toLowerCase().includes(filters.search.toLowerCase())) {
return false;
}
if (filters.statuses.length > 0 && !filters.statuses.includes(task.status)) return false;
if (filters.priorities.length > 0 && !filters.priorities.includes(task.priority)) return false;
if (
filters.assigneeIds.length > 0 &&
(!task.assigneeAgentProfileId || !filters.assigneeIds.includes(task.assigneeAgentProfileId))
) {
return false;
}
if (
filters.projectIds.length > 0 &&
(!task.projectId || !filters.projectIds.includes(task.projectId))
) {
return false;
}
return true;
}
type SortContext = {
field: TaskSortField;
dir: TaskSortDir;
statusOrder: Record<string, number>;
priorityOrder: Record<string, number>;
};
function compareIssues(a: OfficeTask, b: OfficeTask, ctx: SortContext): number {
let cmp = 0;
switch (ctx.field) {
case "status":
cmp = (ctx.statusOrder[a.status] ?? 99) - (ctx.statusOrder[b.status] ?? 99);
break;
case "priority":
cmp = (ctx.priorityOrder[a.priority] ?? 4) - (ctx.priorityOrder[b.priority] ?? 4);
break;
case "title":
cmp = a.title.localeCompare(b.title);
break;
case "created":
cmp = a.createdAt.localeCompare(b.createdAt);
break;
case "updated":
default:
cmp = a.updatedAt.localeCompare(b.updatedAt);
break;
}
return ctx.dir === "asc" ? cmp : -cmp;
}
export type FlatTaskNode = {
task: OfficeTask;
level: number;
hasChildren: boolean;
};
export type UseIssuesTreeOptions = {
tasks: OfficeTask[];
filters: TaskFilterState;
sortField: TaskSortField;
sortDir: TaskSortDir;
nestingEnabled: boolean;
expandedIds: Set<string>;
};
export function useIssuesTree(opts: UseIssuesTreeOptions): FlatTaskNode[] {
const { tasks, filters, sortField, sortDir, nestingEnabled, expandedIds } = opts;
const meta = useAppStore((s) => s.office.meta);
const STATUS_ORDER = useMemo(() => {
if (!meta) return FALLBACK_STATUS_ORDER;
const map: Record<string, number> = {};
for (const s of meta.statuses) map[s.id] = s.order;
return map;
}, [meta]);
const PRIORITY_ORDER = useMemo(() => {
if (!meta) return FALLBACK_PRIORITY_ORDER;
const map: Record<string, number> = {};
for (const p of meta.priorities) map[p.id] = p.order;
return map;
}, [meta]);
return useMemo(() => {
const filtered = tasks.filter((i) => matchesFilters(i, filters));
const sortCtx: SortContext = {
field: sortField,
dir: sortDir,
statusOrder: STATUS_ORDER,
priorityOrder: PRIORITY_ORDER,
};
const sorted = [...filtered].sort((a, b) => compareIssues(a, b, sortCtx));
if (!nestingEnabled) {
return sorted.map((task) => ({ task, level: 0, hasChildren: false }));
}
const childrenMap = new Map<string | undefined, OfficeTask[]>();
for (const task of sorted) {
const key = task.parentId ?? "__root__";
const list = childrenMap.get(key);
if (list) {
list.push(task);
} else {
childrenMap.set(key, [task]);
}
}
const result: FlatTaskNode[] = [];
function walk(parentId: string | undefined, level: number) {
const key = parentId ?? "__root__";
const children = childrenMap.get(key) ?? [];
for (const task of children) {
const kids = childrenMap.get(task.id) ?? [];
const hasChildren = kids.length > 0;
result.push({ task, level, hasChildren });
if (hasChildren && expandedIds.has(task.id)) {
walk(task.id, level + 1);
}
}
}
walk(undefined, 0);
// Also include orphans (tasks whose parent is not in filtered set)
const renderedIds = new Set(result.map((n) => n.task.id));
for (const task of sorted) {
if (!renderedIds.has(task.id)) {
result.push({ task, level: 0, hasChildren: false });
}
}
return result;
}, [
tasks,
filters,
sortField,
sortDir,
nestingEnabled,
expandedIds,
STATUS_ORDER,
PRIORITY_ORDER,
]);
}