- 위험 채움 토큰 밝음 #e5484d → #d6363c(흰 글 3.91 → 4.72) · 어두움 5.6 · 위험 글 #c0392b 그대로 - 새 부품 계산 줄(ui_template_calc_row) — B08 수리집수면적표 b08-hydro__row · 식 칸 배치를 대신함 - 표 부품 칸 램프(ui-data-table__lamp · --only · ✓) — B08 집계표 · 거리이정표 · B09 설계내역 램프 CSS 세 벌을 걷음(표지 class 는 풍선 · 가이드용으로 남김) - 모달 넓은 중간 창(wide + width 1100px · 1100 × 680) — B09 운반 창 손 인라인 크기를 걷음 - 값 목록 규칙 꼴(rule) — ☰ 설정 규칙 줄 ul/li 를 걷음 - 접이 구역 머리(createCollapsibleHead = 패널 머리 + 접는 제목) — B08 구조물도 「조합 연결」 제목(옛 b08-sheets__title)을 대신함 - 일반 화면 틀 좁은 폭 · 입력 칸 2열 · 끝 단추 줄 — D01 · E01 이 B02 CSS 를 빌리던 것을 끊음(가이드 장면 선택자 맞춤) - 부품 모음 견본 · 지킴 시험 test_557_2 · 낡은 시험(198_1 · 247_1 · 251_1 · 257_1 · 276_1 · 551_1)은 부품 CSS · 새 class 로 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
237 lines
10 KiB
TypeScript
237 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* 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, createCollapsibleHead } from "@ui/ui_template_collapsible";
|
|
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
|
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<boolean> {
|
|
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<boolean> {
|
|
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;
|
|
}
|
|
}
|
|
|
|
type ChipState = "is-linked" | "is-missing" | "is-changed" | "is-edited" | "is-gone";
|
|
const CHIP_TONE: Record<ChipState, BadgeTone> = {
|
|
"is-linked": "success",
|
|
"is-missing": "danger",
|
|
"is-changed": "warning",
|
|
"is-edited": "info",
|
|
"is-gone": "danger",
|
|
};
|
|
|
|
/** 칩 = 부품 배지(알약 20) 꼴 · 색 = 상태(연결 초록 · 미연결 빨강 · 바뀜 노랑 · 고침 파랑 · 없어짐 빨강 점선) */
|
|
function toneChip(chip: HTMLElement, state: ChipState): void {
|
|
const look = createBadge("", CHIP_TONE[state], { size: 20 });
|
|
chip.className = `${look.className} b08-link__chip ${state}`;
|
|
chip.dataset.size = look.dataset.size ?? "20";
|
|
}
|
|
|
|
/** 칩 배지 · 테두리 색 · [업데이트] — 사본 목록(원본판 대조)을 뒤에서 받아 칠함 · 못 받으면 단추 꺼진 채. */
|
|
async function paintCopyState(
|
|
projectId: string,
|
|
slot: UnlinkedSlot,
|
|
chip: HTMLElement,
|
|
badges: HTMLElement,
|
|
button: HTMLButtonElement,
|
|
): Promise<ProjectItemInfo | null> {
|
|
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 =>
|
|
createBadge(L(key), warn ? "warning" : "neutral", { size: 18 });
|
|
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) toneChip(chip, 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 head = createCollapsibleHead({
|
|
title: L("B08_Quantity_Link_Title"),
|
|
sub: `${L("B08_Quantity_Link_Linked")} ${slots.length - missing} / ${slots.length}`,
|
|
level: 4,
|
|
});
|
|
section.append(head.root);
|
|
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");
|
|
toneChip(chip, slot.linked ? "is-linked" : "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<boolean>): 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;
|
|
}
|