/* ============================================================================= * B08_Quantity_UI_Link.ts * 구조물도 모달 맨 위 「조합 연결」 칸(PLAN 154-2 · 156 · 163) — 그 측점 시설의 칸 전부 + 연결 여부. * * 꼴(163-1) = 시설마다 한 줄(제목 + 칸 칩 · flex-wrap) · 공용 접기(모두 연결이면 접힘 · 미연결 있으면 펼침) * [선택] → 공용 고르기 모달(`openItemPicker` · 누를 때 `import()`) → 바로 저장 * = PUT …/items/{사본 id}/link(그 칸 하나만 정본에 · 서버가 사본 맞춤) → 집계표 다시 셈 * [연결 끊기](연결된 칸만) → 확인 창 → 같은 길에 null(그 칸만 비움 · 사본 폴더 그대로 · 156-1) * [업데이트](연결된 칸만 · 163-4) = 칩 그린 뒤 `list(사본 폴더)` 로 배지(마스터가 바뀜 · 고침 있음 · * 원본이 없어짐) · 마스터가 바뀜일 때만 켬 → 확인 창 → 고르기 모달 [재적용]과 같은 길(`reapply`) * 단추 = 작은 ghost(칩 안 · 163-1) * B05/B06 세션 초안(구조물 · 관)이 있으면 막고 알림 — 그쪽 [저장]이 목록 통째로 덮어 연결이 사라짐 * ========================================================================== */ import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { attachCollapsible } from "@ui/ui_template_collapsible"; import { projectItems, structureFolder, type ProjectItemInfo, } from "@ui/ui_template_item_picker_project"; import { readPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures"; import { readPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft"; import type { SummaryItem } from "../common_util/common_util_summary_item"; import type { UnlinkedFacility, UnlinkedSlot } from "./B08_Quantity_Summary_Data"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } /** B05/B06 에 저장 안 한 구조물 · 관 조작이 있나(세션 초안). */ export const hasPendingDesign = (projectId: string): boolean => readPendingStructures(projectId) !== null || readPendingPipes(projectId) !== null; /** 칸 하나 고르고 바로 저장 — 저장했으면 true(취소 · 막힘 · 실패 = false). */ async function pickAndLink( projectId: string, facility: UnlinkedFacility, slot: UnlinkedSlot, ): Promise { if (hasPendingDesign(projectId)) { showToast(L("B08_Quantity_Link_Pending"), "warning", 5000); return false; } const { openItemPicker } = await import("@ui/ui_template_item_picker"); const result = await openItemPicker({ current: slot.item, canCopy: true, projectId, structureId: slot.copy, }); if (result.action === "cancel") return false; const item: SummaryItem | null = result.action === "apply" ? result.item : null; try { const failed = await projectItems(projectId).link(slot.copy, facility.facility, item); if (failed.length) showToast(`${L("B08_Quantity_Link_CopyFailed")} — ${failed.join(", ")}`, "warning"); else showToast(L("B08_Quantity_Link_Saved"), "success"); return true; } catch (error) { showToast(`${L("B08_Quantity_Link_Failed")} — ${String(error)}`, "error"); return false; } } /** 칸 하나 연결 끊기 — 끊었으면 true(취소 · 막힘 · 실패 = false · 156-1). */ async function unlink(projectId: string, facility: UnlinkedFacility, slot: UnlinkedSlot) { if (hasPendingDesign(projectId)) { showToast(L("B08_Quantity_Link_Pending"), "warning", 5000); return false; } if (!(await showConfirmDialog(L("B08_Quantity_Link_UnlinkAsk"), L("B08_Quantity_Link_Unlink")))) return false; try { await projectItems(projectId).link(slot.copy, facility.facility, null); showToast(L("B08_Quantity_Link_Unlinked"), "success"); return true; } catch (error) { showToast(`${L("B08_Quantity_Link_UnlinkFailed")} — ${String(error)}`, "error"); return false; } } /** [업데이트] — 사본을 원본으로 덮음(입력값 · 고른자재는 같은 키면 서버가 살림) · 했으면 true. */ async function update(projectId: string, info: ProjectItemInfo): Promise { const ask = L("B08_Quantity_Link_UpdateAsk") + (info.고침 ? ` ${L("B08_Quantity_Link_UpdateEdited")}` : ""); if (!(await showConfirmDialog(ask, L("B08_Quantity_Link_Update")))) return false; try { const got = await projectItems(projectId).reapply(info); showToast( got.covered.length ? `${L("B08_Quantity_Link_UpdateDone")} — ${got.covered.join(" · ")}` : L("B08_Quantity_Link_UpdateNone"), "success", ); return true; } catch (error) { showToast(`${L("B08_Quantity_Link_UpdateFailed")} — ${String(error)}`, "error"); return false; } } /** 칩 배지 · 테두리 색 · [업데이트] — 사본 목록(원본판 대조)을 뒤에서 받아 칠함 · 못 받으면 단추 꺼진 채. */ async function paintCopyState( projectId: string, slot: UnlinkedSlot, chip: HTMLElement, badges: HTMLElement, button: HTMLButtonElement, ): Promise { const folder = structureFolder(slot.copy); const info = await projectItems(projectId) .list(folder) .then((rows) => rows.find((row) => row.폴더 === folder) ?? null) .catch(() => null); if (!info) return null; const badge = (key: keyof typeof ui_locales, warn: boolean): HTMLElement => { const node = document.createElement("span"); node.className = warn ? "b08-link__badge is-warn" : "b08-link__badge"; node.textContent = L(key); return node; }; if (info.고침) badges.append(badge("B08_Quantity_Link_Edited", false)); if (info.원본없음) badges.append(badge("B08_Quantity_Link_Gone", true)); else if (info.마스터바뀜) badges.append(badge("B08_Quantity_Link_Changed", true)); // 테두리 상태 색(170-3) — 앞섬 = 없어짐 › 바뀜 › 고침 › 연결 const tone = info.원본없음 ? "is-gone" : info.마스터바뀜 ? "is-changed" : info.고침 ? "is-edited" : null; if (tone) chip.classList.replace("is-linked", tone); button.disabled = !info.마스터바뀜 || !!info.원본없음; button.title = button.disabled ? L("B08_Quantity_Link_UpdateSame") : L("B08_Quantity_Link_UpdateTip"); return info; } /** 「조합 연결」 칸 — 시설마다 한 줄(제목 + 칸 칩) · 공용 접기. `onLinked` = 저장 뒤(모달 닫고 다시 셈). */ export function linkSection( projectId: string, facilities: readonly UnlinkedFacility[], onLinked: () => void, ): HTMLElement { const slots = facilities.flatMap((f) => f.slots); const missing = slots.filter((slot) => !slot.linked).length; const section = document.createElement("section"); // 공용 접기(163-1) — 모두 연결이면 접힘 · 미연결 있으면 펼침 section.className = `b08-sheets__combo b08-link ui-collapsible${missing ? "" : " is-collapsed"}`; section.dataset.b08Link = "section"; const title = document.createElement("h4"); title.className = "b08-sheets__title ui-collapsible__title"; const count = document.createElement("span"); count.className = "b08-sheets__from"; count.textContent = `${L("B08_Quantity_Link_Linked")} ${slots.length - missing} / ${slots.length}`; const label = document.createElement("span"); label.textContent = L("B08_Quantity_Link_Title"); label.append(" ", count); title.append(label); section.append(title); attachCollapsible(section); for (const facility of facilities) { const line = document.createElement("div"); line.className = "b08-link__row"; const head = document.createElement("span"); head.className = "b08-link__facility"; head.textContent = facility.name; line.append(head); for (const slot of facility.slots) { const chip = document.createElement("span"); chip.className = slot.linked ? "b08-link__chip is-linked" : "b08-link__chip is-missing"; chip.dataset.b08LinkSlot = slot.copy; const name = document.createElement("span"); name.className = "b08-link__name"; name.textContent = slot.group ?? L("B08_Quantity_Link_Body"); const state = document.createElement("span"); state.className = slot.linked ? "b08-link__state" : "b08-link__state is-missing"; state.textContent = slot.linked ? (slot.item?.name ?? L("B08_Quantity_Link_Linked")) : L("B08_Quantity_Summary_Unlinked"); state.title = state.textContent; // 칩이 좁으면 긴 조합 이름이 잘림 const badges = document.createElement("span"); badges.className = "b08-link__badges"; const buttons = document.createElement("span"); buttons.className = "b08-link__buttons"; const act = (label: string, run: () => Promise): HTMLButtonElement => createButton({ label, variant: "ghost", size: "sm", // 칩 안 작은 단추(163-1) onClick: () => { const was = [...buttons.querySelectorAll("button")].map((b) => b.disabled); buttons.querySelectorAll("button").forEach((b) => (b.disabled = true)); void run().then((saved) => saved ? onLinked() : buttons.querySelectorAll("button").forEach((b, i) => (b.disabled = was[i])), ); }, }); buttons.append( act(L("B08_Quantity_Link_Pick"), () => pickAndLink(projectId, facility, slot)), ); if (slot.linked) { buttons.append(act(L("B08_Quantity_Link_Unlink"), () => unlink(projectId, facility, slot))); let info: ProjectItemInfo | null = null; const updateButton = act(L("B08_Quantity_Link_Update"), () => info ? update(projectId, info) : Promise.resolve(false), ); updateButton.disabled = true; // 원본판 대조를 받기 전엔 꺼 둠 buttons.append(updateButton); void paintCopyState(projectId, slot, chip, badges, updateButton).then( (got) => (info = got), ); } chip.append(name, state, badges, buttons); line.append(chip); } section.append(line); } return section; }