Files
kandev/apps/web/lib/kanban/wip-queue.ts
T
2026-08-17 21:21:48 +00:00

110 lines
3.1 KiB
TypeScript

import type { TaskPriority } from "@/lib/types/http";
export type WipQueueTask = {
id: string;
workflowStepId: string;
queuedForStepId?: string | null;
wipAdmitted?: boolean | null;
position?: number | null;
priority?: TaskPriority | null;
queuedAt?: string | null;
createdAt?: string | null;
};
export type WipQueueEntry<T extends WipQueueTask = WipQueueTask> = {
task: T;
position: number;
total: number;
};
export type WipQueueStatus = {
position: number;
total: number;
destinationTitle: string;
};
function priorityRank(priority: WipQueueTask["priority"]): number {
switch (priority) {
case "critical":
return 0;
case "high":
return 1;
case "medium":
return 2;
case "low":
return 3;
default:
return 4;
}
}
function timestamp(value: string | null | undefined): number {
if (!value) return Number.POSITIVE_INFINITY;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
}
function compareNumbers(left: number, right: number): number {
if (left < right) return -1;
if (left > right) return 1;
return 0;
}
export function compareWipQueueTasks(left: WipQueueTask, right: WipQueueTask): number {
const position = (left.position ?? 0) - (right.position ?? 0);
if (position !== 0) return position;
const priority = priorityRank(left.priority) - priorityRank(right.priority);
if (priority !== 0) return priority;
const queuedAt = compareNumbers(timestamp(left.queuedAt), timestamp(right.queuedAt));
if (queuedAt !== 0) return queuedAt;
const createdAt = compareNumbers(timestamp(left.createdAt), timestamp(right.createdAt));
if (createdAt !== 0) return createdAt;
return left.id.localeCompare(right.id);
}
function isDestinationQueued(task: WipQueueTask, destinationStepId: string): boolean {
return (
task.workflowStepId === destinationStepId &&
task.queuedForStepId === destinationStepId &&
task.wipAdmitted !== true
);
}
export function getDestinationQueue<T extends WipQueueTask>(
tasks: T[],
destinationStepId: string,
): WipQueueEntry<T>[] {
const queued = tasks.filter((task) => isDestinationQueued(task, destinationStepId));
queued.sort(compareWipQueueTasks);
return queued.map((task, index) => ({ task, position: index + 1, total: queued.length }));
}
export function partitionWipTasks<T extends WipQueueTask>(
tasks: T[],
destinationStepId: string,
): { admitted: T[]; queued: T[] } {
const queuedEntries = getDestinationQueue(tasks, destinationStepId);
const queuedIds = new Set(queuedEntries.map(({ task }) => task.id));
return {
admitted: tasks.filter((task) => !queuedIds.has(task.id)),
queued: queuedEntries.map(({ task }) => task),
};
}
export function getWipQueueStatus<T extends WipQueueTask>(
task: T,
tasks: T[],
destinationStepId: string,
destinationTitle: string,
): WipQueueStatus | undefined {
const entry = getDestinationQueue(tasks, destinationStepId).find(
({ task: candidate }) => candidate.id === task.id,
);
if (!entry) return undefined;
return { position: entry.position, total: entry.total, destinationTitle };
}