Files
Aislo/B08_Quantity/B08_Quantity_UI_Drawing.ts
T

257 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B08_Quantity_UI_Drawing.ts
* 구조물 집계표 행 → 구조물도 모달(PLAN 95-3) — 그 측점에 놓인 조합 전부를 세로로 쌓음.
* 조합 한 개 = M02 항목 카드(`M02_MasterTemplete_ItemCards`) 그대로 — 도면(도각 포함 · 보기 전용 CAD ·
* 여러 장이면 장 탭) · 일위대가 · 면적산출 = 공용 카드 셋을 한 줄 세 칸(156-4 · 창 = 공용 wide).
*
* 원천 = 그 구조물의 프로젝트 사본만(연결된 사본 · PLAN 143-1) — 없으면 「사본 없음」
* 카드 [수정](163-3) = 모달 몸통이 사본 전용 편집 화면(고르기 모달과 같은 `createEditView`)으로 바뀜 ·
* 도면 · 면적산출 = 그 부분 · 일위대가 카드 = 넣을 값만(담은로직 · 자재는 원본 층에서 · 서버 400 · 142-1) ·
* [뒤로] = 카드 다시 읽음 · 저장했으면 닫을 때 집계표 다시 셈 · 판 다르면 서버 409 그대로
* 조합 목록 = 집계표와 같음(`B08_Quantity_Summary_Data` `combosAt`)
* 시설이 있으면 맨 위 「조합 연결」 칸(`B08_Quantity_UI_Link` · PLAN 154-2 · 156-1) — 저장 · 끊기면 닫고 다시 셈
* 카드 · CAD · 스프레드시트는 모달을 열 때만 `import()` · 닫을 때 카드를 모두 정리
* ========================================================================== */
import "./B08_Quantity_UI_Drawing.css";
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import { openModal } from "@ui/ui_template_modal";
import { showToast } from "@ui/ui_template_elements";
import { PART_OF, projectItems, withCopyValues } from "@ui/ui_template_item_picker_project";
import type { EditView } from "@ui/ui_template_item_picker_edit";
import type { LaborDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model";
import type { EditKind, ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards";
import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource";
import { summaryTooltip, type SummaryItem } from "../common_util/common_util_summary_item";
import type {
ComboLabel,
LinkedSnapshot,
StationCombo,
UnlinkedFacility,
} from "./B08_Quantity_Summary_Data";
import { linkSection } from "./B08_Quantity_UI_Link";
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
function note(text: string): HTMLElement {
const node = document.createElement("p");
node.className = "ui-general-block__empty";
node.textContent = text;
return node;
}
export interface SheetsRequest {
projectId: string;
/** 모달 제목에 붙는 측점 글 */
station: string;
combos: StationCombo[];
/** 연결된 사본 `{구조물 id: 사본}`(집계 응답 · 143-1) */
linked: Record<string, LinkedSnapshot>;
describe: (item: SummaryItem) => ComboLabel | null;
/** 그 측점 시설 칸 전부(154 · 156-1) */
facilities: UnlinkedFacility[];
/** 바로 연결 · 끊기를 저장한 뒤 — 집계표 다시 셈 */
onLinked: () => void;
}
/** 사본 폴더 읽기 — 그 부분 파일이 없으면 던짐(카드가 「없음」) ·
* 일위대가 = 사본 `일위대가` · 입력값 · 고른자재 그대로(142-1 고르기 모달과 같은 길 · 161-1). */
function sourceOf(projectId: string, folder: string): ItemSource {
const items = projectItems(projectId);
return {
layer: "project",
projectId,
read: async (part) => {
const doc = await items.read(folder, PART_OF[part]);
if (doc == null) throw new Error(L("B08_Quantity_Sheets_NoCopy"));
return { 문서: doc };
},
labor: {
read: async () => {
const [labor, values, picks] = await Promise.all([
items.read(folder, "일위대가") as Promise<{ 줄?: LaborDoc } | null>,
items.read(folder, "입력값") as Promise<ValueDoc | null>,
items.read(folder, "고른자재") as Promise<Record<string, unknown> | null>,
]);
if (!labor?.줄) throw new Error(L("B08_Quantity_Sheets_NoCopy"));
return { 문서: withCopyValues(labor.줄, values, picks) };
},
calc: () => items.calcLabor(folder),
},
};
}
/** 카드 [수정] — 사본 · 고칠 부분(일위대가 카드 = 넣을 값) · 카드 다시 읽기 */
type OpenEdit = (
copy: LinkedSnapshot,
name: string,
kind: EditKind | "inputs",
reload: () => void,
) => void;
/** 조합 한 칸 — 제목줄(이름 · 원천) + 안내 + 카드. */
function comboSection(
request: SheetsRequest,
combo: StationCombo,
createItemCards: typeof import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards").createItemCards,
handles: ItemCardsHandle[],
openEdit: OpenEdit,
): HTMLElement {
const label = request.describe(combo.item);
const name = label
? [label.group, label.name].filter(Boolean).join(" ")
: combo.item.name || combo.item.key;
const copy = combo.copyId ? request.linked[combo.copyId] : undefined;
const section = document.createElement("section");
section.className = "b08-sheets__combo";
section.dataset.b08Combo = combo.id;
const title = document.createElement("h4");
title.className = "b08-sheets__title";
title.textContent = name;
title.title = summaryTooltip(combo.item);
section.append(title);
if (!copy) {
section.append(note(L("B08_Quantity_Drawing_Missing")));
return section;
}
const source = sourceOf(request.projectId, copy.폴더);
const from = document.createElement("span");
from.className = "b08-sheets__from";
from.textContent = L("B08_Quantity_Sheets_FromCopy");
title.append(from);
if (!copy.부분.includes("도면")) section.append(note(L("B08_Quantity_Drawing_NoDrawing")));
const load = (): void => void cards.load({ id: copy.폴더, label: name });
// 카드 안 [저장]은 없음(자료 자리에 write 없음) — [수정]만 사본 편집 화면으로(163-3)
const cards = createItemCards({
source: () => source,
editable: () => true,
onEdit: (kind) => openEdit(copy, name, kind, load),
warnOnFail: () => false, // 못 읽으면 카드에 「없음」
labor: () => openEdit(copy, name, "inputs", load), // 일위대가 카드(161-1) [수정] = 넣을 값만
});
handles.push(cards);
section.append(cards.root);
load();
return section;
}
/** 모달을 엶 — 조합이 없으면 안내 한 줄 · 불러오다 실패하면 까닭 한 줄. */
export function openStructureSheets(request: SheetsRequest): void {
const handles: ItemCardsHandle[] = [];
let closed = false;
let edited = false; // 사본을 고쳐 저장함 — 닫을 때 집계표 다시 셈(163-3)
let editView: EditView | null = null;
let modalBody: HTMLElement | null = null;
// 머리 단추 칸(170-2) — 공용 모달은 열 때 단추가 있어야 머리에 붙임 → 빈 칸을 미리 넘김 · 편집 화면에서만 채움
const head = document.createElement("div");
head.className = "ui-modal__actions";
head.hidden = true;
const clearHead = (): void => {
head.replaceChildren();
head.hidden = true;
};
/** 카드 [수정] — 몸통을 사본 편집 화면으로 · [뒤로] = 몸통 되돌리고 그 카드 다시 읽음 */
const openEdit: OpenEdit = (copy, name, kind, reload) => {
const body = modalBody;
if (!body || editView) return;
void (async () => {
try {
const [{ createEditView }] = await Promise.all([
import("@ui/ui_template_item_picker_edit"),
import("@ui/ui_template_item_picker.css"),
]);
if (closed) return;
const kept = [...body.children];
const host = document.createElement("div");
host.className = "ui-item-picker__edit-host";
const view = createEditView(
{
kind,
row: { key: copy.항목.key, 열: copy.항목.key, 경로: [name] },
info: { 폴더: copy.폴더, 원본: copy.항목 },
items: projectItems(request.projectId),
projectId: request.projectId,
},
(saved) => {
view.destroy();
editView = null;
clearHead();
body.replaceChildren(...kept);
edited ||= saved;
if (saved) reload();
},
);
editView = view;
// [← 뒤로] · [저장] = 머리 [닫기] 왼쪽 · 크기 = [닫기](sm 뗌)
view.back.classList.remove("ui-btn--sm");
view.save.classList.remove("ui-btn--sm");
head.replaceChildren(view.back, view.save);
head.hidden = false;
host.append(
...(kind === "inputs" ? [note(L("B08_Quantity_Sheets_InputsOnly"))] : []),
view.root,
);
body.replaceChildren(host);
view.start();
} catch (error) {
showToast(`${L("B08_Quantity_Drawing_Failed")} — ${String(error)}`, "error");
}
})();
};
openModal({
title: `${request.station} ${L("B08_Quantity_Sheets_Title")}`,
closeLabel: L("Common_Btn_Close"),
wide: true, // 공용 큰 창(96vw × 94vh · 156-4)
dialogClass: "b08-sheets",
actions: [head],
beforeClose: async () => {
if (editView && !(await editView.leave())) return false; // 저장 안 한 고침 — 버릴지 물음
editView?.destroy();
clearHead();
closed = true; // 아직 불러오는 중이면 뒤늦게 카드를 만들지 않음
handles.forEach((cards) => cards.destroy());
if (edited) request.onLinked(); // 사본을 고쳤으면 집계표 다시 셈(연결 저장과 같은 꼴)
return true;
},
mount: (body, close) => {
modalBody = body;
const linking = request.facilities.length
? [
linkSection(request.projectId, request.facilities, () => {
closed = true;
handles.forEach((cards) => cards.destroy());
close();
request.onLinked();
}),
]
: [];
if (!request.combos.length)
return void body.append(
...linking,
...(linking.length ? [] : [note(L("B08_Quantity_Summary_NoCombo"))]),
);
body.append(...linking, note(L("B08_Quantity_Drawing_Loading")));
void (async () => {
try {
const { createItemCards } =
await import("../M02_MasterTemplete/M02_MasterTemplete_ItemCards");
if (closed) return;
body.replaceChildren(
...linking,
...request.combos.map((combo) =>
comboSection(request, combo, createItemCards, handles, openEdit),
),
);
} catch (error) {
if (closed) return;
const reason = error instanceof Error ? error.message : String(error);
body.replaceChildren(...linking, note(`${L("B08_Quantity_Drawing_Failed")} — ${reason}`));
}
})();
},
});
}