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);
});
}