Newer
Older
client_dwatch / lib / facilities / filter-by-ids.ts
/**
 * 解決済みの施設ID配列で「行」を絞る純粋関数(指示書AK 段階4)。
 *
 * ------------------------------------------------------------------------
 * ★★なぜ SQL ではなくここで絞るのか(この判断を消す前に読むこと)★★
 * ------------------------------------------------------------------------
 * 分類(`group_id` / `customer_id`)の絞り込みは、★データ層のクエリに降ろすのが本筋★です
 * (`db.cleanLogs()` / `db.prechecks()` は `facility_ids` を受け取り、
 *  `where: { facility_id: { in: [...] } }` に落としています)。
 *
 * ★★ただし `dsDevices()` / `dsAlerts()` / `facilities()` / `schedules()` の4本は
 *   施設の引数を取らない契約★★で(★`dsAlerts()` は 2026-10-19 に `open_only` だけ足した。指示書BB D17★)、
 *   ★`dsAlerts()` と `schedules()` は `/dashboard` からも呼ばれています★。ここに引数を足すと ★`/dashboard`(段階4b・別担当)の
 *   呼び出し側を書き換えることになる★ため、段階4a では触っていません。
 *   ★4本とも、もともと絞り込み・並べ替え・ページングを**ルート側の純粋関数**で
 *   行う作り★(`filterDsDevicesByQ()` / `filterFacilitiesByFields()` /
 *   `filterSchedules()` …)なので、★分類の絞り込みも同じ場所に置く★のが素直でした。
 *
 * ★★申し送り★★ — ★4本に引数を足して SQL に降ろすなら、`/dashboard` の
 *   呼び出し側(`db.schedules()` / `db.dsAlerts()`)と同時に直すこと★。
 *   ★そのときも `null`(絞らない)と `[]`(0件ヒット)の区別を崩さないこと★。
 *
 * ------------------------------------------------------------------------
 * ★★`null` と `[]` は別物★★(★罠1 / 落とし穴1★)
 * ------------------------------------------------------------------------
 *   - `null` … ★分類で絞っていない★(=全行を素通しする)
 *   - `[]`   … ★絞ったが1件も当たらなかった★(=★0件を返す★)
 *
 * ★★`facilityIds?.length` で分岐しないこと★★ — `[]` が「未指定」に化け、
 *   ★0件ヒットが全件表示になります。例外もテスト失敗も出ません★。
 *   分岐は必ず ★`=== null`★ で書くこと(`lib/facilities/intersect-ids.ts` と同じ作法)。
 *
 * ★純粋関数にすること★(`CLAUDE.md` §9.4)— DBアクセスやログ出力を混ぜない。
 */

/**
 * `facility_id` を持つ行(`DsDevice` / `DsAlert` / `Schedule` 等)を絞る。
 *
 * ★★呼ぶ場所はサマリ計算より**前**★★ — `/ds-devices` / `/ds-alerts` の `summary` は
 *   ★q / status の絞り込みの影響を受けない★約束(D-054)ですが、
 *   ★分類の絞り込みは「見え方」ではなく「母集団」を選ぶもの★なので、
 *   ★★サマリにも効かせる★★(「この屋号の異常◯件」が見たい。指示書AK §7-2 が
 *   `/dashboard` の数値カードについて言っているのと同じ理由)。
 *   ★一覧とサマリで母集団がずれないよう、必ず同じ配列から作ること★。
 *
 * ★並び順は入力のまま★(並べ替えは各ルートの `sortXxx()` の仕事)。
 */
export function filterRowsByFacilityIds<T extends { facility_id: number }>(
  rows: readonly T[],
  facilityIds: number[] | null
): T[] {
  if (facilityIds === null) return [...rows];
  const allowed = new Set(facilityIds);
  return rows.filter((row) => allowed.has(row.facility_id));
}

/**
 * 施設そのものの一覧(`Facility[]`)を絞る。
 *
 * ★★見る列が違う★★ — `/facilities` が返すのは ★施設の行そのもの★なので、
 *   突き合わせるのは `facility_id` ではなく ★`id`★ です
 *   (`DsDevice` 等は「施設に紐づく行」なので `facility_id`)。
 *
 * ★★`Facility` は段階5 で `customer_id` / `group_id` を持ったが、それでも
 *   ここで直接絞らないこと★★ — ★絞り込みの入口は `resolveFacilityIdsByMasters()` のまま★に
 *   しておく(指示書AK §2-2 / 段階5)。★施設名の解決(`resolveFacilityIdsByName()`)との
 *   積集合を同じ形(施設IDの配列)で取れるようにするため★で、
 *   ここに所属の判定を写すと ★同じ規則が2箇所に散る★。
 */
export function filterFacilitiesByIds<T extends { id: number }>(
  rows: readonly T[],
  facilityIds: number[] | null
): T[] {
  if (facilityIds === null) return [...rows];
  const allowed = new Set(facilityIds);
  return rows.filter((row) => allowed.has(row.id));
}