/**
* エラー一覧専用の下書き(指示書BE §5・§6。D-185)。
*
* アラートと D-Sec は `lib/prechecks/edit-drafts.ts` のまま(`error_list_id` を送らない)。
* こちらの正規化は対応状況・備考に加えて `error_list_id` を持つ。行の値と同じなら消し、
* 項目が0になったらエントリごと消す。
*/
import { draftNote, draftStatus } from "@/lib/prechecks/edit-drafts";
import type { EditableRow, ResolutionDraft } from "@/lib/prechecks/edit-drafts";
import type { ErrorListItem, ErrorStatusUpdateRequest, PrecheckStatus } from "@/lib/types";
export type ErrorResolutionDraft = ResolutionDraft & { error_list_id?: number };
export type ErrorResolutionDrafts = ReadonlyMap<string, ErrorResolutionDraft>;
/** エラー内容の下書きを載せられる行(`ErrorReportItem` はこれを満たす) */
export type ErrorEditableRow = EditableRow & {
error_list_id: number | null;
error_list: ErrorListItem | null;
/** この行でエラー内容を選べるか。★false の行には備考必須を掛けない★(サーバーと同じ。指示書BD §3-3 手順4) */
error_list_selectable: boolean;
};
/** その他を選んだのに備考が空のとき、行の直後に出す文言(表示時に `t()` を通す) */
export const NOTE_REQUIRED_ROW_MESSAGE = "その他を選んだときは備考を入力してください";
function normalizeErrorDraft(
row: ErrorEditableRow,
draft: ErrorResolutionDraft
): ErrorResolutionDraft | null {
const next: ErrorResolutionDraft = {};
if (draft.status !== undefined && draft.status !== row.resolution_status) {
next.status = draft.status;
}
if (draft.note !== undefined && draft.note !== (row.note ?? "")) {
next.note = draft.note;
}
if (draft.error_list_id !== undefined && draft.error_list_id !== row.error_list_id) {
next.error_list_id = draft.error_list_id;
}
if (
next.status === undefined &&
next.note === undefined &&
next.error_list_id === undefined
) {
return null;
}
return next;
}
function withErrorDraft(
drafts: ErrorResolutionDrafts,
row: ErrorEditableRow,
patch: ErrorResolutionDraft
): Map<string, ErrorResolutionDraft> {
const result = new Map(drafts);
const normalized = normalizeErrorDraft(row, { ...drafts.get(row.id), ...patch });
if (normalized) result.set(row.id, normalized);
else result.delete(row.id);
return result;
}
export function setErrorDraftStatus(
drafts: ErrorResolutionDrafts,
row: ErrorEditableRow,
status: PrecheckStatus
): Map<string, ErrorResolutionDraft> {
return withErrorDraft(drafts, row, { status });
}
export function setErrorDraftNote(
drafts: ErrorResolutionDrafts,
row: ErrorEditableRow,
note: string
): Map<string, ErrorResolutionDraft> {
return withErrorDraft(drafts, row, { note });
}
export function setErrorDraftErrorListId(
drafts: ErrorResolutionDrafts,
row: ErrorEditableRow,
errorListId: number
): Map<string, ErrorResolutionDraft> {
return withErrorDraft(drafts, row, { error_list_id: errorListId });
}
/**
* PATCH の本文。対応状況は必須、備考と `error_list_id` は変えたときだけ。
* ★送る必要が無ければ null★。
*/
export function errorPatchRequestFromDraft(
row: ErrorEditableRow,
draft: ErrorResolutionDraft | undefined
): ErrorStatusUpdateRequest | null {
const normalized = draft ? normalizeErrorDraft(row, draft) : null;
if (!normalized) return null;
const request: ErrorStatusUpdateRequest = { status: draftStatus(row, normalized) };
if (normalized.note !== undefined) request.note = normalized.note;
if (normalized.error_list_id !== undefined) request.error_list_id = normalized.error_list_id;
return request;
}
/** 適用後のエラー内容(下書きの id があれば選択肢から、無ければ保存済み) */
export function appliedErrorList(
row: ErrorEditableRow,
draft: ErrorResolutionDraft | undefined,
catalog: readonly ErrorListItem[]
): ErrorListItem | null {
if (draft?.error_list_id !== undefined) {
return catalog.find((item) => item.id === draft.error_list_id) ?? null;
}
return row.error_list;
}
/**
* 適用後のエラー内容が備考必須か(指示書BE §6)。
* ★選べない行(`error_list_selectable` が false)は常に false★(サーバーも掛けない。指示書BD §3-3 手順4)。
* 画面の placeholder「備考を入力(必須)」と警告色の枠も、保存前の検証も、この関数1つで決める。
*/
export function requiresNote(
row: ErrorEditableRow,
draft: ErrorResolutionDraft | undefined,
catalog: readonly ErrorListItem[]
): boolean {
if (!row.error_list_selectable) return false;
return appliedErrorList(row, draft, catalog)?.note_required === true;
}
/**
* 保存前に送らない行か(指示書BE §6)。
* 下書きがエラー内容か備考を変えていて、適用後が `note_required`(`requiresNote()`)かつ備考(前後の空白を除く)が空。
* ★対応状況だけの変更には掛けない★。★選べない行には掛けない★。
*/
export function blocksSaveForMissingNote(
row: ErrorEditableRow,
draft: ErrorResolutionDraft | undefined,
catalog: readonly ErrorListItem[]
): boolean {
if (!draft) return false;
if (draft.error_list_id === undefined && draft.note === undefined) return false;
if (!requiresNote(row, draft, catalog)) return false;
return draftNote(row, draft).trim() === "";
}
export interface SaveErrorDraftsResult<T> {
updated: Map<string, T>;
failed: Map<string, unknown>;
remaining: Map<string, ErrorResolutionDraft>;
}
/** 送った行の数の進み具合(`done` は送り終えた行数、`total` は送る行数。★送らなかった行は数えない★) */
export type SaveProgress = (done: number, total: number) => void;
/**
* エラー一覧の下書きを順に送る。★1行の失敗で止めない★(`saveDrafts()` と同じ順)。
* アラートと D-Sec はこの関数を使わない。★保存前の検証は掛けない★(画面は `saveErrorDraftsWithCheck()` を使う)。
*/
export async function saveErrorDrafts<T extends ErrorEditableRow>(
rows: readonly T[],
drafts: ErrorResolutionDrafts,
send: (row: T, request: ErrorStatusUpdateRequest) => Promise<T>,
onProgress?: SaveProgress
): Promise<SaveErrorDraftsResult<T>> {
const targets: { row: T; request: ErrorStatusUpdateRequest }[] = [];
for (const row of rows) {
const request = errorPatchRequestFromDraft(row, drafts.get(row.id));
if (request) targets.push({ row, request });
}
const updated = new Map<string, T>();
const failed = new Map<string, unknown>();
const remaining = new Map<string, ErrorResolutionDraft>();
let done = 0;
for (const { row, request } of targets) {
try {
updated.set(row.id, await send(row, request));
} catch (error) {
failed.set(row.id, error);
const draft = drafts.get(row.id);
if (draft) remaining.set(row.id, draft);
}
done += 1;
onProgress?.(done, targets.length);
}
return { updated, failed, remaining };
}
export interface SaveErrorDraftsWithCheckResult<T> extends SaveErrorDraftsResult<T> {
/** 保存前の検証で送らなかった行(id → 行の直後に出す文言。表示時に `t()` を通す) */
blocked: Map<string, string>;
}
/**
* 保存(指示書BE §6)。★その他で備考が空の行(`blocksSaveForMissingNote()`)は送らない★。
* ・送らなかった行は `blocked` に `NOTE_REQUIRED_ROW_MESSAGE` で入り、★下書きはそのまま `remaining` に残る★
* ・ほかの行は `saveErrorDrafts()` で送る(1行の失敗で止めない。失敗した行の下書きも `remaining` に残る)
* ・成功した行は `remaining` に入らない
* 画面は `failed.size === 0 && blocked.size === 0` のときだけ通常モードに戻る。
*/
export async function saveErrorDraftsWithCheck<T extends ErrorEditableRow>(
rows: readonly T[],
drafts: ErrorResolutionDrafts,
catalog: readonly ErrorListItem[],
send: (row: T, request: ErrorStatusUpdateRequest) => Promise<T>,
onProgress?: SaveProgress
): Promise<SaveErrorDraftsWithCheckResult<T>> {
const blocked = new Map<string, string>();
const sendable = new Map(drafts);
for (const row of rows) {
if (!blocksSaveForMissingNote(row, drafts.get(row.id), catalog)) continue;
blocked.set(row.id, NOTE_REQUIRED_ROW_MESSAGE);
sendable.delete(row.id);
}
const { updated, failed, remaining } = await saveErrorDrafts(rows, sendable, send, onProgress);
for (const id of blocked.keys()) {
const draft = drafts.get(id);
if (draft) remaining.set(id, draft);
}
return { updated, failed, blocked, remaining };
}