Newer
Older
client_dwatch / lib / reports / facility-performance-load.ts
/**
 * 施設別パフォーマンスの「取得 → 絞り込み → 並べ替え → 連続日数の上限処理」(D-163)
 *
 * ★★一覧(`GET /reports/facility-performance`)と CSV(`GET /reports/facility-performance/export`)の
 *   両方がこの1本を呼ぶ★★ — ★片方だけ直すと、画面の表と CSV の中身が食い違います★
 *   (例外もテスト失敗も出ない壊れ方。`/clean-logs` と `/clean-logs/export` が
 *   同じ引数を渡すことにしているのと同じ理由。指示書AC 段階2 D-6)。
 *
 * ★`limit` はここでは適用しない★ — 一覧は戻り値を `slice(0, limit)` し、
 *   CSV は全件を出す(画面の「全施設表示」の200件上限に縛られない。D-163)。
 *
 * ★DB の例外はそのまま投げる★ — 500 にするか(とログを出すか)はルートが決める。
 */
import { CONSECUTIVE_DAYS_DISPLAY } from "@/lib/config";
import { db } from "@/lib/db";
import { intersectFacilityIds } from "@/lib/facilities/intersect-ids";
import { resolveFacilityIdsByMasters } from "@/lib/facilities/resolve-by-master";
import { resolveFacilityIdsByName } from "@/lib/facilities/resolve-by-name";
import {
  capFacilityPerformance,
  filterFacilityPerformanceByName,
  filterFacilityPerformanceBySerial,
  filterFacilityPerformanceByStatus,
  selectFacilityPerformanceRows,
  sortFacilityPerformance,
  toReportDbQuery,
  type ReportQuery,
} from "@/lib/reports/query";
import { loadConsecutiveDaysMax } from "@/lib/settings/display-limits";
import type { FacilityPerformance } from "@/lib/types";

/**
 * 絞り込み・並べ替え・連続日数の上限処理まで済ませた ★全件★ を返す(`limit` は見ない)。
 *
 * 並び順は稼働率の昇順(ワースト順)・同値は facility_id の昇順・null は最後
 * (`sortFacilityPerformance()`。`d-watch-api.yaml` の /reports/facility-performance)。
 */
export async function loadFacilityPerformance(
  query: ReportQuery
): Promise<FacilityPerformance[]> {
  // ★表示上限(指示書AF 段階6)★ — 設定が読めないだけで一覧を 500 にしない。
  //   ★型注釈 `: number` が要る★(`CONSECUTIVE_DAYS_DISPLAY` は `as const` でリテラル型)
  let maxDays: number = CONSECUTIVE_DAYS_DISPLAY.defaultMax;
  try {
    maxDays = await loadConsecutiveDaysMax();
  } catch {
    // 既定値のまま続行する
  }

  // ★★施設名の照合はアプリ側で施設IDに解決してから SQL に渡す★★(指示書AC C-1 / 段階5。
  //   ★段階2(`/clean-logs`)で確定した作法をそのまま真似る。新しい書き方を発明しない★)—
  //   ★記号(中点等)を無視する照合は LIKE では書けない★(照合順序でも無理。実測済み)。
  //   ★★戻り値の `null` と `[]` は別物★★: `null` = 施設名の条件なし(全施設)/
  //   `[]` = ★照合したが1件も当たらなかった(=0件)★。
  //   ★★施設名の絞り込みを持つレポートAPIはここだけ★★(`d-watch-api.yaml` の
  //   `/reports/facility-performance` と、その CSV の `/export` にだけ `facility_name` がある)。
  //   ★`serial` は据え置き=メモリ絞り込みのまま★(索引が効かず利得が薄い。C-1)。
  //   ★★分類(`group_id` / `customer_id`)とは**積集合(AND)**を取る★★
  //   (指示書AK §6-2 / 段階4b)— ★どちらか片方を捨てないこと★。
  //   ★積が空なら `[]` = 0件★(★`null` にすると条件ごと消えて全件表示に化ける★。
  //   落とし穴2。例外もテスト失敗も出ない)。
  const [byName, byMasters] = await Promise.all([
    resolveFacilityIdsByName(query.facilityName),
    resolveFacilityIdsByMasters(query),
  ]);
  const rows = await db.facilityPerformance(
    toReportDbQuery(query, intersectFacilityIds(byName, byMasters))
  );

  // ここから下は純粋な絞り込み・並べ替え。
  const inPeriod: FacilityPerformance[] = selectFacilityPerformanceRows(rows, query);
  const filtered = filterFacilityPerformanceBySerial(
    filterFacilityPerformanceByName(
      filterFacilityPerformanceByStatus(inPeriod, query.status),
      query.facilityName
    ),
    query.serial
  );
  const sorted = sortFacilityPerformance(filtered);

  // ★表示上限は絞り込み・並べ替えの**後**にかける★(指示書AF 段階6-2)
  //   件数(`meta.total`)も `status` の判定も上限とは無関係(上限は表示のためだけ)。
  //   ★1行ずつの写像なので、`limit` で切る前にかけても後にかけても結果は同じ★(D-163)。
  return capFacilityPerformance(sorted, maxDays);
}