- B09 데이터 점검 → 내역 점검 · 공기 옵션 → 공사기간 조건 · 박스보기/호표보기 → 카드 보기/표 보기 · 하이퍼링크/팝업 → 탭 이동/곁 창 · 굳힌 단가 → 이 공사 단가 책 · 산식/요율 출처 → 계산식/요율 근거 · 빈 줄 → 금액 빈 줄 · 단가연결 칸 램프 → 단가연결 칸 ● · 새 산근 → 새 단가산출(산근) · 연결 없음 → 일위대가 없음 - 단가연결 창 종류 일위대가 사전조합 → 일위대가(자료집) · 일위대가 호표 → 이 공사 일위대가 · 단가산출 호표 → 이 공사 단가산출 · 대가N/산근 N호표 → 일위대가 N호/단가산출 N호 · 구조물 조합 줄 번호(SS…) 숨김 · 넣을 값 창 제목 번호 → 알림 글 끝 - B08 조합 사본 · 조합 연결 · 노랑 램프 → 구조물 조합 · 구조물 조합 고르기 · 노랑 ●(locale_b4 B08_Quantity_* 글만 · 한시 브레인 승인) - ☰ 서랍 · 진행 판 끝 「내 자료집(일위대가 · 구조물 조합)」 = M02 일위대가조합 개인 층(떠남 물음 뒤) - 낡은 시험 글 고침(새 낱말): 143-1 · 157-2 · 182 · 203-1 · 204-2 · 304 · 311 · 701-3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
193 lines
8.4 KiB
TypeScript
193 lines
8.4 KiB
TypeScript
/* =============================================================================
|
|
* B09_Estimation_UI_Check.ts
|
|
* B09 좌측 「데이터 점검」 묶음(PLAN 203-1) — [업데이트] · 연결 점검(옛 「연결 점검」 탭 내용).
|
|
*
|
|
* [업데이트] = 단가를 지금 M01 값으로 새로 굳힘 · 서버가 낡았다고 보면(`payload.stale` 까닭 한 줄)
|
|
* 단추에 점 배지(`is-pending`) · 굳힌 시각 · 까닭 = 공용 호버
|
|
* 미리 보기 고리(703-3) = [업데이트] 누름을 먼저 잡아 POST …/estimation/prices/preview(화면 캐시) →
|
|
* 「바뀌는 단가 줄 N · 다시 가져올 구조물 사본 K · 고친 칸 M 지킴 · 노임 판 전 → 후」 창 → [적용] =
|
|
* 원래 단추 일(서버가 사본 재적용 + 단가 다시 굳힘 · 덮개 그대로) · 닫기 = 안 함
|
|
* 연결 점검 = 「빈 줄 n」(설계내역서 금액 모르는 줄 · 안내 · 집계표와 같은 수 · 701-2) 목록 +
|
|
* 그 밖 확인 목록(일위대가 · 단가산출 · 재료 등 · 탭 · 키마다 하나)(누르면 그 탭 그 줄로 · 까닭 = 공용 호버) + 서버 알림 · M01 에 없는 키
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { createButton, showToast } from "@ui/ui_template_elements";
|
|
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import { createBadge } from "@ui/ui_template_badge";
|
|
import { createText } from "@ui/ui_template_text";
|
|
import type { Row } from "./B09_Estimation_Calc_Base";
|
|
import { dateTimeText } from "../common_util/common_util_time";
|
|
import { readState } from "../A00_Common/b_page_state";
|
|
import { currentProjectId } from "../A00_Common/b_project_switch";
|
|
import { blankRows, tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
|
|
|
|
export interface CheckItem {
|
|
link: Link;
|
|
tab: string;
|
|
name: string;
|
|
why: string;
|
|
}
|
|
|
|
const BLANK_WHY = "금액 모름 — 단가연결 칸 ● · 이은 호표의 빈 줄을 확인";
|
|
|
|
/** 빈 줄(설계내역서 줄마다 하나 · 까닭 모음) · 그 밖 확인 줄(탭 · 키마다 하나) · 알림 글 */
|
|
export function checkItems(
|
|
sheets: Row,
|
|
payload: Row,
|
|
): { items: CheckItem[]; others: CheckItem[]; notes: string[] } {
|
|
const why = new Map<string, string[]>();
|
|
const order: string[] = [];
|
|
const named = new Map<string, Row>();
|
|
for (const g of (sheets.gaps || []) as Row[]) {
|
|
const id = `${g.tab}|${g.key}`;
|
|
if (!why.has(id)) (why.set(id, []), order.push(id), named.set(id, g));
|
|
const list = why.get(id)!;
|
|
if (g.why && !list.includes(String(g.why))) list.push(String(g.why));
|
|
}
|
|
const item = (tab: string, key: string, name: unknown, reasons?: string[]): CheckItem => ({
|
|
link: { tab: tab as TabId, key },
|
|
tab: tabName(tab),
|
|
name: String(name ?? ""),
|
|
why: reasons?.length ? reasons.join(" · ") : BLANK_WHY,
|
|
});
|
|
const items = blankRows(sheets).map((r) => item("bill", r.key, r.name, why.get(`bill|${r.key}`)));
|
|
const others = order
|
|
.filter((id) => !id.startsWith("bill|"))
|
|
.map((id) => item(named.get(id)!.tab, named.get(id)!.key, named.get(id)!.name, why.get(id)));
|
|
const notes: string[] = [...(payload.notes || [])];
|
|
const missing: string[] = payload.inputs?.missing || [];
|
|
if (missing.length) notes.push(`M01 에 없는 키 ${missing.length} — ${missing.join(" · ")}`);
|
|
return { items, others, notes };
|
|
}
|
|
|
|
/** [업데이트] 호버 글 — 굳힌 시각 + 낡은 까닭 */
|
|
export function refreshTip(payload: Row | null): string {
|
|
const made = payload?.book?.made;
|
|
const when = made ? `단가 굳힘 ${dateTimeText(String(made))}` : "단가 아직 안 굳힘";
|
|
const stale = payload?.stale;
|
|
return `${when} · 누르면 미리 보기 → [적용] = 지금 M01 값으로 새로 굳힘 · 원본 바뀐 구조물 사본 재적용 · 고친 칸 그대로${stale ? ` · 업데이트 필요 — ${stale}` : ""}`;
|
|
}
|
|
|
|
/** [업데이트] 미리 보기 글 줄(703-3) */
|
|
export function previewLines(got: Row): string[] {
|
|
const copies: Row[] = got.사본 || [];
|
|
const edition = got.노임판 || {};
|
|
return [
|
|
`바뀌는 단가 줄 ${got.단가줄 ?? 0}(새 줄 ${got.새줄 ?? 0} · 값 바뀜 ${got.바뀐줄 ?? 0})` +
|
|
`${got.계수 ? " · 운전사 계수 바뀜" : ""}${got.환율 ? " · 환율 바뀜" : ""}`,
|
|
`다시 가져올 구조물 사본 ${copies.length}${copies.length ? ` — ${copies.map((c) => c.이름).join(" · ")}` : ""}`,
|
|
`고친 칸 ${got.고친칸 ?? 0} 지킴(일위대가 덮개 · 새 판 · 재적용 뒤에도 그대로)`,
|
|
`노임 판 ${edition.전 || "—"} → ${edition.후 || "—"}`,
|
|
];
|
|
}
|
|
|
|
/** [업데이트] 누름 앞 미리 보기 — [적용] 이면 원래 단추 일 · 닫기 = 안 함 */
|
|
function attachPreview(refresh: HTMLElement, payload: () => Row | null): void {
|
|
let pass = false;
|
|
refresh.addEventListener(
|
|
"click",
|
|
(event) => {
|
|
if (pass) return void (pass = false);
|
|
event.stopImmediatePropagation();
|
|
const projectId = currentProjectId();
|
|
const user = readState<Row>("b09-estimation", projectId) ?? payload()?.user ?? {};
|
|
void fetch(`${API_BASE_URL}/projects/${projectId}/estimation/prices/preview`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ user }),
|
|
})
|
|
.then(async (response) => {
|
|
const got = (await response.json()) as Row;
|
|
if (!response.ok) throw new Error(String(got?.detail ?? response.status));
|
|
openModal({
|
|
title: "업데이트 미리 보기",
|
|
closeLabel: "닫기",
|
|
width: "40rem",
|
|
mount: (body, close, head) => {
|
|
const apply = createButton({
|
|
label: "적용",
|
|
variant: "filled",
|
|
onClick: () => {
|
|
close();
|
|
pass = true;
|
|
refresh.click();
|
|
},
|
|
});
|
|
apply.dataset.b09RefreshApply = "";
|
|
head.append(apply);
|
|
body.append(...previewLines(got).map((line) => createText(line)));
|
|
},
|
|
});
|
|
})
|
|
.catch((error) => showToast(`미리 보기 못 함 — ${String(error)}`, "error"));
|
|
},
|
|
true,
|
|
);
|
|
}
|
|
|
|
export interface CheckPanel {
|
|
/** 묶음 안 연결 점검 자리 */
|
|
list: HTMLElement;
|
|
draw: (payload: Row, sheets: Row) => void;
|
|
}
|
|
|
|
export function createCheckPanel(refresh: HTMLElement, goto: (link: Link) => void): CheckPanel {
|
|
let payload: Row | null = null;
|
|
let items: CheckItem[] = [];
|
|
attachHoverInfo(refresh, () => refreshTip(payload));
|
|
attachPreview(refresh, () => payload);
|
|
const box = document.createElement("div");
|
|
box.className = "b09-check";
|
|
attachHoverInfo(box, (row) => items[Number(row.dataset.i)]?.why, "[data-i]");
|
|
|
|
const head = (text: string): HTMLElement => {
|
|
const p = createText(text, "muted");
|
|
p.classList.add("b09-check__head");
|
|
return p;
|
|
};
|
|
const list = (group: CheckItem[], offset: number): HTMLElement => {
|
|
const rows = document.createElement("div");
|
|
rows.className = "ui-sidebar-list";
|
|
rows.append(
|
|
...group.map((item, n) => {
|
|
const row = document.createElement("button");
|
|
row.type = "button";
|
|
row.className = "ui-sidebar-row";
|
|
row.dataset.i = String(offset + n);
|
|
const name = document.createElement("span");
|
|
name.textContent = item.name;
|
|
const tab = createBadge(item.tab, "neutral", { size: 18 });
|
|
tab.classList.add("b09-check__tab");
|
|
row.append(name, tab);
|
|
row.addEventListener("click", () => goto(item.link));
|
|
return row;
|
|
}),
|
|
);
|
|
return rows;
|
|
};
|
|
|
|
const draw = (next: Row, sheets: Row): void => {
|
|
payload = next;
|
|
refresh.classList.toggle("is-pending", Boolean(next.stale));
|
|
const made = checkItems(sheets, next);
|
|
items = [...made.items, ...made.others];
|
|
const blank = [
|
|
head(`금액 빈 줄 ${made.items.length}`),
|
|
...(made.items.length ? [list(made.items, 0)] : []),
|
|
];
|
|
const others = made.others.length
|
|
? [head(`그 밖 확인 ${made.others.length}곳`), list(made.others, made.items.length)]
|
|
: [];
|
|
const notes = made.notes.map((text) => {
|
|
const p = createText(text, "muted");
|
|
p.classList.add("b09-check__note");
|
|
return p;
|
|
});
|
|
box.replaceChildren(...blank, ...others, ...notes);
|
|
};
|
|
return { list: box, draw };
|
|
}
|