/**
* GET /ds-alerts で使うクエリ処理。
* 仕様の正は `d-watch-api.yaml` の /ds-alerts。ソート順は D-052と同じ規則。
*/
import { parseMasterIdFilters } from "@/lib/api/master-id";
import { fail, type QueryParseResult } from "@/lib/api/query-result";
import { PAGINATION } from "@/lib/config";
import type { DsAlert, DsAlertStatusFilter, DsAlertTypeFilter } from "@/lib/types";
const TYPE_FILTERS: readonly DsAlertTypeFilter[] = [
"all",
"security_off",
"disconnected",
];
const STATUS_FILTERS: readonly DsAlertStatusFilter[] = ["open", "all", "resolved"];
export interface DsAlertQuery {
type: DsAlertTypeFilter;
status: DsAlertStatusFilter;
/**
* ホールディングス(`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 parseDsAlertQuery(
searchParams: URLSearchParams
): QueryParseResult<DsAlertQuery> {
const typeRaw = searchParams.get("type") ?? "all";
if (!TYPE_FILTERS.includes(typeRaw as DsAlertTypeFilter)) {
return fail(422, "validation_failed", "typeの値が不正です", {
type: [`${TYPE_FILTERS.join(" / ")} のいずれかを指定してください`],
});
}
const statusRaw = searchParams.get("status") ?? "open";
if (!STATUS_FILTERS.includes(statusRaw as DsAlertStatusFilter)) {
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: {
type: typeRaw as DsAlertTypeFilter,
status: statusRaw as DsAlertStatusFilter,
groupId: masters.query.groupId,
customerId: masters.query.customerId,
page,
perPage,
},
};
}
/** alert_type のみで絞り込む */
export function filterDsAlertsByType(
items: DsAlert[],
type: DsAlertTypeFilter
): DsAlert[] {
if (type === "all") return items;
return items.filter((item) => item.alert_type === type);
}
/** open = 未解消+対応中(解消でない)/resolved = 解消のみ(指示書AX D10)。YAML定義通り */
export function filterDsAlertsByStatus(
items: DsAlert[],
status: DsAlertStatusFilter
): DsAlert[] {
return items.filter((item) => {
switch (status) {
case "open":
return item.resolution_status !== "resolved";
case "resolved":
return item.resolution_status === "resolved";
case "all":
return true;
}
});
}
/**
* detected_at の降順。同値のときは id の昇順で安定させる(D-052と同じ規則)。
* ★id は string(`error_id`)。辞書順(`<` / `>`)で比べる★(指示書AX D11。`"10"` が `"9"` より先)。
* ★`localeCompare` を使わない★(実行環境のロケールで順が変わる。`compareErrorReportKeys()` と同じ規則)
*/
export function sortDsAlerts(items: DsAlert[]): DsAlert[] {
return [...items].sort((a, b) => {
if (a.detected_at !== b.detected_at) {
return a.detected_at < b.detected_at ? 1 : -1;
}
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
});
}
/** 対応状況ごとの件数(キーは `PrecheckStatus` と同じ。指示書AX D10) */
export interface DsAlertSummary {
unresolved: number;
in_progress: number;
resolved: number;
}
/**
* デバイスの現況サマリ。
*
* ★summary はフリートの部分集合(日付・グループ・会社)で絞る。
* 閲覧の絞り込み(q・status・type)では絞らない。★
* グループ・会社マスタ導入時は、ここに絞り込みを追加する(現在は D-053 で未実装)。
* この区別を崩すと、F-03 で起きた「既定表示で解消済みが常に0」と同じ不具合になる(D-054)。
*/
export function summarizeDsAlerts(items: DsAlert[]): DsAlertSummary {
const unresolved = items.filter(
(item) => item.resolution_status === "unresolved"
).length;
const in_progress = items.filter(
(item) => item.resolution_status === "in_progress"
).length;
const resolved = items.filter((item) => item.resolution_status === "resolved").length;
return { unresolved, in_progress, resolved };
}