- B08 줄 [?][<][>][저장][초기화] · POST quantity/reset · 초안 넷 비움 · 이정 저장본 다시 읽음 - B08 B09 쪽 이동 = 저장 뒤 POST quantity/complete · 실패면 머묾 - B09 줄 [?][<][>][초기화] · [내역 초기화] → [초기화] 하나(POST estimation/reset) - B09 [>] = 고친 값 저장 → POST estimation/complete → DRAFT.next(인쇄 미리보기) · 초안 없으면 흐림 - 시험: 새 test_298_2_b08_b09_buttons · test_184_3 · test_304 · test_481_2 고쳐 씀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
8.2 KiB
TypeScript
184 lines
8.2 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_UI_Page.ts
|
|
* 로그인 후 08: 5차 워크플로우 (수량 산출) — 위 탭 화면 (PLAN 61)
|
|
*
|
|
* 탭 = 구조물 집계표(61-1) · 자재 · 토공 · 수리집수면적표 (PLAN 95-1 · 160-1) · 거리이정표(244-1). 위 탭 단추 = 공용
|
|
* 기본 크기(md) · 보조 탭 단추 = sm(PLAN 69-1). 구조물도는 집계표 행을 눌러 여는 모달(95-3).
|
|
* 자재 아래 보조 탭 = 총괄자재수량집계표 · 콘크리트소요자재집계표 · 자재수량집계표(121-1 `…_UI_Material`).
|
|
* 옛 수량 산출 코드(`old_code/B08_Quantity/`)는 가져오지 않음 — 새로 씀.
|
|
* 좌측 패널 = 메인 탭과 같은 컨테이너 셋 + [저장] 줄(`B08_Quantity_UI_Options` · PLAN 122).
|
|
* 단계 이동(☰ · 진행 단계 · [<] [>]) = 보이는 화면(좌측 옵션 + 지금 탭) 저장 성공 뒤 · 실패면 머묾(481-2).
|
|
* B09 쪽 이동 = 저장 뒤 POST …/quantity/complete(단계 5 완료 · 요구 298-2 · 300 B1) 성공 뒤.
|
|
* ========================================================================== */
|
|
|
|
import "@ui/ui_template_general_layout.css";
|
|
import "./B08_Quantity_UI_Page.css";
|
|
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
|
import { createButton, showToast } from "@ui/ui_template_elements";
|
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
|
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
|
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
|
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
|
import { renderStructureSummary } from "./B08_Quantity_UI_Summary";
|
|
import { renderMaterialSheet } from "./B08_Quantity_UI_Material";
|
|
import { renderHydro, request } from "./B08_Quantity_UI_Hydro";
|
|
import { renderEarthwork } from "./B08_Quantity_UI_Earthwork";
|
|
import { createOptionsPanel } from "./B08_Quantity_UI_Options";
|
|
import { renderMilestone } from "./B08_Quantity_UI_Milestone";
|
|
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
const TABS = [
|
|
{ id: "summary", label: "B08_Quantity_Tab_StructureSummary" },
|
|
{ id: "material", label: "B08_Quantity_Tab_Materials" },
|
|
{ id: "earthwork", label: "B08_Quantity_Tab_Earthwork" },
|
|
{ id: "hydrology", label: "B08_Quantity_Tab_Hydrology" },
|
|
{ id: "milestone", label: "B08_Quantity_Tab_Milestone" },
|
|
] as const;
|
|
|
|
type TabId = (typeof TABS)[number]["id"];
|
|
|
|
/** 자재 탭 보조 탭 — 실무 엑셀 차례. */
|
|
const SHEETS = [
|
|
{ id: "material", label: "B08_Quantity_Tab_MaterialTotal" },
|
|
{ id: "concrete", label: "B08_Quantity_Tab_ConcreteMaterial" },
|
|
{ id: "quantity", label: "B08_Quantity_Tab_MaterialQuantity" },
|
|
] as const;
|
|
|
|
type SheetId = (typeof SHEETS)[number]["id"];
|
|
|
|
/** 지금 탭의 이동 전 저장 — 자재 · 수리 탭이 서면 등록 · 떠난 탭(`host.isConnected` 거짓)은 안 씀(481-2). */
|
|
type TabSave = (host: HTMLElement, save: () => Promise<boolean>) => void;
|
|
|
|
function pendingNote(): HTMLElement {
|
|
const pending = document.createElement("p");
|
|
pending.className = "ui-general-block__empty";
|
|
pending.textContent = L("B08_Quantity_Tab_Pending");
|
|
return pending;
|
|
}
|
|
|
|
/** 자재 탭 — 보조 탭 줄 + 몸통(보조 탭마다 새 몸통 · 늦은 응답이 다른 보조 탭을 덮지 않게). */
|
|
function renderMaterials(host: HTMLElement, projectId: string | null, onSave: TabSave): void {
|
|
const tabs = document.createElement("div");
|
|
tabs.className = "b08-quantity__tabs ui-subtabs b08-structure__tabs";
|
|
tabs.setAttribute("role", "tablist");
|
|
const view = document.createElement("div");
|
|
const select = (picked: SheetId): void => {
|
|
tabs.replaceChildren(
|
|
...SHEETS.map(({ id, label }) => {
|
|
const button = createButton({
|
|
label: L(label),
|
|
variant: id === picked ? "filled" : "ghost",
|
|
size: "sm",
|
|
onClick: () => id !== picked && select(id),
|
|
});
|
|
button.setAttribute("role", "tab");
|
|
button.setAttribute("aria-selected", String(id === picked));
|
|
button.dataset.b08StructureSheet = id;
|
|
return button;
|
|
}),
|
|
);
|
|
const body = document.createElement("div");
|
|
view.replaceChildren(body);
|
|
if (!projectId) body.append(pendingNote());
|
|
else void renderMaterialSheet(body, projectId, picked, (save) => onSave(body, save));
|
|
};
|
|
host.replaceChildren(tabs, view);
|
|
select("material");
|
|
}
|
|
|
|
/** 탭마다 새 몸통 — 늦게 온 응답이 다른 탭 화면을 덮지 않게(떨어진 몸통에만 그림). */
|
|
function renderTab(host: HTMLElement, tab: TabId, projectId: string | null, onSave: TabSave): void {
|
|
const view = document.createElement("div");
|
|
host.replaceChildren(view);
|
|
if (tab === "material") return renderMaterials(view, projectId, onSave);
|
|
if (tab === "summary" && projectId) {
|
|
// 보조 탭 줄 자리(170-4) — 보이지 않는 sm 단추 한 줄 · 자재 탭과 같은 높이라 탭 바꿀 때 안 튐
|
|
const blank = document.createElement("div");
|
|
blank.className = "b08-quantity__tabs ui-subtabs b08-quantity__tabs--blank";
|
|
blank.setAttribute("aria-hidden", "true");
|
|
const ghost = createButton({ label: L(SHEETS[0].label), variant: "ghost", size: "sm" });
|
|
ghost.tabIndex = -1;
|
|
blank.append(ghost);
|
|
const body = document.createElement("div");
|
|
view.append(blank, body);
|
|
return void renderStructureSummary(body, projectId);
|
|
}
|
|
if (tab === "hydrology" && projectId)
|
|
return void renderHydro(view, projectId, (save) => onSave(view, save));
|
|
if (tab === "earthwork" && projectId) return void renderEarthwork(view, projectId);
|
|
if (tab === "milestone" && projectId) return renderMilestone(view, projectId);
|
|
view.append(pendingNote());
|
|
}
|
|
|
|
export function renderB08Quantity(root: HTMLElement): void {
|
|
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
|
|
|
const tabBar = document.createElement("div");
|
|
tabBar.className = "b08-quantity__tabs";
|
|
tabBar.setAttribute("role", "tablist");
|
|
const panel = document.createElement("div");
|
|
panel.className = "b08-quantity__panel";
|
|
panel.setAttribute("role", "tabpanel");
|
|
|
|
let current: TabId = "summary";
|
|
let tabSave: { host: HTMLElement; save: () => Promise<boolean> } | null = null;
|
|
const onTabSave: TabSave = (host, save) => (tabSave = { host, save });
|
|
const select = (tab: TabId): void => {
|
|
current = tab;
|
|
tabBar.replaceChildren(
|
|
...TABS.map(({ id, label }) => {
|
|
const button = createButton({
|
|
label: L(label),
|
|
variant: id === tab ? "filled" : "ghost",
|
|
onClick: () => id !== tab && select(id),
|
|
});
|
|
button.setAttribute("role", "tab");
|
|
button.setAttribute("aria-selected", String(id === tab));
|
|
button.dataset.b08Tab = id;
|
|
return button;
|
|
}),
|
|
);
|
|
renderTab(panel, tab, projectId, onTabSave);
|
|
};
|
|
|
|
const body = document.createElement("div");
|
|
body.className = "b08-quantity";
|
|
body.append(tabBar, panel);
|
|
select("summary");
|
|
|
|
// [저장] 뒤 열린 탭을 서버 정본으로 다시 그림
|
|
const options = createOptionsPanel(projectId, () => select(current));
|
|
// 왼쪽 → 지금 탭 차례 · 탭 저장은 왼쪽 저장이 탭을 다시 그리기 전에 쥠
|
|
const saveShown = async (): Promise<boolean> => {
|
|
const tab = tabSave?.host.isConnected ? tabSave.save : null;
|
|
return (await options.save()) && (!tab || (await tab()));
|
|
};
|
|
|
|
const layout = createWorkflowLayout({
|
|
title: L("B08_Quantity_Title"),
|
|
steps: workflowSteps(),
|
|
activeStep: 5,
|
|
mainContent: body,
|
|
leftPanel: options.panel,
|
|
routes: WORKFLOW_STEP_ROUTES,
|
|
onStepClick: (stepIndex: number) => {
|
|
if (!projectId) return;
|
|
void (async () => {
|
|
if (!(await saveShown())) return;
|
|
// B09 쪽 = 단계 5 완료 뒤(요구 300 B1) · 실패면 머묾
|
|
if (stepIndex > 5)
|
|
try {
|
|
await request(`/projects/${projectId}/quantity/complete`, { method: "POST" });
|
|
} catch (error) {
|
|
return showToast(`단계를 마치지 못했습니다 — ${String(error)}`, "error");
|
|
}
|
|
goToWorkflowStage(projectId, WORKFLOW_STEP_ROUTES[stepIndex]);
|
|
})();
|
|
},
|
|
});
|
|
root.append(layout.root);
|
|
}
|