/**
* GET /ds-devices で使うクエリ処理。
* 仕様の正は `d-watch-api.yaml` の /ds-devices。ソート順は D-055。
*/
import { parseMasterIdFilters } from "@/lib/api/master-id";
import { fail, type QueryParseResult } from "@/lib/api/query-result";
import { PAGINATION } from "@/lib/config";
import { includesFoldName, includesFoldPlain } from "@/lib/format/search-text";
import type { DsDevice, DsDeviceStatusFilter } from "@/lib/types";
const STATUS_FILTERS: readonly DsDeviceStatusFilter[] = ["all", "error", "ok"];
export interface DsDeviceQuery {
q: string;
status: DsDeviceStatusFilter;
/**
* ホールディングス(`groups.id`)で絞り込み。★`null` = 絞らない★(指示書AK 段階2)。
* ★`customerId` と同時に指定されたら AND★(`d-watch-api.yaml` の `GroupId`)。
*/
groupId: number | null;
/** 顧客(`customers.id`)で絞り込み。★`null` = 絞らない★ */
customerId: number | null;
page: number;
perPage: number;
}
function toInt(value: string | null, fallback: number): number {
if (!value) return fallback;
const n = Number.parseInt(value, 10);
return Number.isNaN(n) ? fallback : n;
}
/**
* クエリパラメータをパースする。
* 不正な値があれば `{ ok: false }` を返す(`error` / `status` をそのまま422応答に使う。
* どのパラメータが不正かは `error.error.details` に入る)。
* ★group_id / customer_id を読む(指示書AK 段階2。D-053 の「受け取っても無視する」は解除)★
* 検証は `parseMasterIdFilters()`(`lib/api/master-id.ts`)の1本に任せる。
* ★`Number.parseInt()` で書き直さないこと★(`1abc` が `1` として通る。D-123)。
*
* page / per_page は不正値でも失敗させず、既定値・上限へ丸める(/clean-logs と同じ挙動)。
*/
export function parseDsDeviceQuery(
searchParams: URLSearchParams
): QueryParseResult<DsDeviceQuery> {
const statusRaw = searchParams.get("status") ?? "all";
if (!STATUS_FILTERS.includes(statusRaw as DsDeviceStatusFilter)) {
return fail(422, "validation_failed", "statusの値が不正です", {
status: [`${STATUS_FILTERS.join(" / ")} のいずれかを指定してください`],
});
}
const masters = parseMasterIdFilters(searchParams);
if (!masters.ok) return masters;
const page = Math.max(1, toInt(searchParams.get("page"), 1));
const perPage = Math.min(
PAGINATION.maxPerPage,
Math.max(1, toInt(searchParams.get("per_page"), PAGINATION.defaultPerPage))
);
return {
ok: true,
query: {
q: searchParams.get("q")?.trim() ?? "",
status: statusRaw as DsDeviceStatusFilter,
groupId: masters.query.groupId,
customerId: masters.query.customerId,
page,
perPage,
},
};
}
/**
* 施設名・シリアルの部分一致(YAMLのqの定義通り)。大文字小文字を区別しない。
*
* ★照合は `lib/format/search-text.ts` の1本に寄せる(指示書AG D-1)★
* 施設名は記号(中点・長音・空白・ハイフン類)も無視する = `includesFoldName`。
* ★シリアルは大文字小文字のみ★ = `includesFoldPlain`
* (D-S制御盤のシリアルは `DS-001` 形式。記号を除くと桁の境界が消える。D-2)。
*/
export function filterDsDevicesByQ(items: DsDevice[], q: string): DsDevice[] {
if (!q) return items;
return items.filter(
(item) =>
includesFoldName(item.facility_name, q) ||
includesFoldPlain(item.serial_number, q)
);
}
/** error = "error"(YAMLの定義通り。通信断は「異常」そのものなので別値を持たない。D-065) */
export function filterDsDevicesByStatus(
items: DsDevice[],
status: DsDeviceStatusFilter
): DsDevice[] {
return items.filter((item) => {
switch (status) {
case "error":
return item.connection_status === "error";
case "ok":
return item.connection_status === "ok";
case "all":
return true;
}
});
}
const CONNECTION_STATUS_ORDER: Record<DsDevice["connection_status"], number> = {
error: 0,
ok: 1,
};
/**
* 異常→正常の順。同順位は serial_number の昇順で安定させる(D-055)。
* ★安定化のためだけの規則で、運用上の優先度を表すものではない★
*/
export function sortDsDevices(items: DsDevice[]): DsDevice[] {
return [...items].sort((a, b) => {
const order =
CONNECTION_STATUS_ORDER[a.connection_status] -
CONNECTION_STATUS_ORDER[b.connection_status];
if (order !== 0) return order;
return a.serial_number < b.serial_number
? -1
: a.serial_number > b.serial_number
? 1
: 0;
});
}
export interface DsDeviceSummary {
total: number;
ok: number;
/** "error" の件数(YAMLの定義通り。通信断は「異常」に含まれる。D-065) */
error: number;
}
/**
* デバイスの現況サマリ。
*
* ★summary はフリートの部分集合(日付・グループ・会社)で絞る。
* 閲覧の絞り込み(q・status・type)では絞らない。★
* グループ・会社マスタ導入時は、ここに絞り込みを追加する(現在は D-053 で未実装)。
* この区別を崩すと、F-03 で起きた「既定表示で解消済みが常に0」と同じ不具合になる(D-054)。
*/
export function summarizeDsDevices(items: DsDevice[]): DsDeviceSummary {
const ok = items.filter((item) => item.connection_status === "ok").length;
const error = items.filter((item) => item.connection_status === "error").length;
const total = items.length;
return { total, ok, error };
}