Newer
Older
client_dwatch / lib / clean-logs / sort.ts
import {
  CLEAN_TASK_STATUS_SORT_RANK,
  CLEAN_TASK_STATUS_SORT_RANK_NOT_DEPARTED,
  CLEAN_TASK_STATUS_SORT_RANK_UNKNOWN,
} from "@/lib/config";
import { compareCleanLogItems } from "@/lib/db/clean-log-mapping";
import type { TableSortOrder } from "@/lib/table-sort";
import type { CleanLogItem, CleanLogSortKey, Grade } from "@/lib/types";

const GRADE_SORT_RANK: Readonly<Record<Grade, number>> = {
  "×": 1,
  "△": 2,
  "○": 3,
  "◎": 4,
};

function compareCodepoint(a: string, b: string): number {
  return a < b ? -1 : a > b ? 1 : 0;
}

function taskStatusSortRank(item: CleanLogItem): number {
  if (item.task_status === null) return CLEAN_TASK_STATUS_SORT_RANK_NOT_DEPARTED;
  return CLEAN_TASK_STATUS_SORT_RANK[item.task_status] ?? CLEAN_TASK_STATUS_SORT_RANK_UNKNOWN;
}

function departedSortRank(item: CleanLogItem): number {
  return item.departed ? 1 : 0;
}

/**
 * 完了率の固定段(向きの反転の外。指示書BH §1-1):
 * 0 = 通常行 / 1 = 完了率 null(出発失敗)/ 2 = 結果待ち(その下)
 */
function completionRatePinTier(item: CleanLogItem): number {
  if (item.result_pending) return 2;
  if (item.completion_rate === null) return 1;
  return 0;
}

/** ★desc は主キーの比較だけを反転する★。`reverse()` しない(§2-1) */
function applyDirection(primary: number, order: TableSortOrder): number {
  if (primary === 0) return 0;
  return order === "desc" ? -primary : primary;
}

/**
 * 下に固定する行(★向きの反転の外★)。
 * 結果待ちを grade / departed / task_status で先に見る理由: grade は結果待ちでも "×"(罠3)。
 */
function comparePinnedBottom(
  a: CleanLogItem,
  b: CleanLogItem,
  sort: CleanLogSortKey
): number | null {
  switch (sort) {
    case "model_name": {
      const aNull = a.model_name === null;
      const bNull = b.model_name === null;
      if (aNull && bNull) return 0;
      if (aNull) return 1;
      if (bNull) return -1;
      return null;
    }
    case "serial_number": {
      const aNull = a.serial_number === null;
      const bNull = b.serial_number === null;
      if (aNull && bNull) return 0;
      if (aNull) return 1;
      if (bNull) return -1;
      return null;
    }
    case "departed":
    case "grade":
    case "task_status": {
      const aPin = a.result_pending;
      const bPin = b.result_pending;
      if (aPin && bPin) return 0;
      if (aPin) return 1;
      if (bPin) return -1;
      return null;
    }
    case "completion_rate": {
      const aTier = completionRatePinTier(a);
      const bTier = completionRatePinTier(b);
      if (aTier !== bTier) return aTier - bTier;
      return null;
    }
    case "scheduled_at":
      return null;
    default: {
      const _exhaustive: never = sort;
      return _exhaustive;
    }
  }
}

function comparePrimaryValues(
  a: CleanLogItem,
  b: CleanLogItem,
  sort: CleanLogSortKey
): number {
  switch (sort) {
    case "model_name":
      return compareCodepoint(a.model_name!, b.model_name!);
    case "serial_number":
      return compareCodepoint(a.serial_number!, b.serial_number!);
    case "scheduled_at":
      return Date.parse(a.scheduled_at) - Date.parse(b.scheduled_at);
    case "departed":
      return departedSortRank(a) - departedSortRank(b);
    case "grade":
      return GRADE_SORT_RANK[a.grade] - GRADE_SORT_RANK[b.grade];
    case "task_status":
      return taskStatusSortRank(a) - taskStatusSortRank(b);
    case "completion_rate":
      return a.completion_rate! - b.completion_rate!;
    default: {
      const _exhaustive: never = sort;
      return _exhaustive;
    }
  }
}

/**
 * 清掃詳細ログの並べ替え(指示書BH §2-1)。
 * ★`sort` が null のときは並べ替えず入力順の写しを返す★(fixtures 既定順を壊さない)。
 */
export function sortCleanLogs(
  items: readonly CleanLogItem[],
  sort: CleanLogSortKey | null,
  order: TableSortOrder
): CleanLogItem[] {
  if (sort === null) return [...items];

  return [...items].sort((a, b) => {
    const pinned = comparePinnedBottom(a, b, sort);
    if (pinned !== null && pinned !== 0) return pinned;

    const primary = comparePrimaryValues(a, b, sort);
    const directed = applyDirection(primary, order);
    if (directed !== 0) return directed;
    return compareCleanLogItems(a, b);
  });
}