- 줄 조합은 양식+프로젝트(종류별), [내 라이브러리에 저장] 때 양식에 실림 - 수동 단가는 프로젝트만(값·출처·넣은 날짜) — 빨간 테두리 + 「미확정 N건」 - 고르개는 이 프로젝트 단가표에서 품셈·자원을 낱말로 찾음 - 다른 양식을 가져오면 그 종류의 줄 조합·수동 단가도 비움 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
191 lines
6.5 KiB
TypeScript
191 lines
6.5 KiB
TypeScript
/* =============================================================================
|
|
* B08_Quantity_UI_StructureSheet_UnitPrice.ts
|
|
* 구조물도 장 아래 **일위대가 표**(미리보기) — PLAN 3장 하단 ①②③.
|
|
*
|
|
* ⚠ 값을 셈하지 않음 — 서버(`…/structure-sheets/unit-price`)가 B09 단가표로 낸 금액을 적기만.
|
|
* ⚠ 장 조회와 따로 받음 — 단가표 첫 조립이 십여 초라 표가 늦게 차도 위 수량표는 먼저 보임.
|
|
* ⚠ 막힌 줄은 0 이 아니라 까닭을 적고, 하나라도 있으면 합계 앞에 「미완」.
|
|
* ⚠ 수동 단가 줄은 금액 칸 빨간 테두리 + 머리 「미확정 N건」 배지(PLAN 확정 ⑦).
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { el, num } from "./B08_Quantity_UI_StructureSheet_Formula";
|
|
import {
|
|
unitPriceEditor,
|
|
type UnitPriceEditorData,
|
|
} from "./B08_Quantity_UI_StructureSheet_UnitPriceEdit";
|
|
|
|
interface UnitPriceRow {
|
|
seq: number;
|
|
name: string;
|
|
spec: string;
|
|
ref_code: string;
|
|
unit: string;
|
|
quantity: number | null;
|
|
skipped: boolean;
|
|
reason: string;
|
|
material?: number;
|
|
labor?: number;
|
|
expense?: number;
|
|
total?: number;
|
|
manual?: boolean;
|
|
manual_source?: string;
|
|
manual_entered_at?: string;
|
|
}
|
|
|
|
interface UnitPriceTable {
|
|
code: string;
|
|
name: string;
|
|
unit: string;
|
|
rows: UnitPriceRow[];
|
|
material: number;
|
|
labor: number;
|
|
expense: number;
|
|
total: number;
|
|
blocked: number;
|
|
complete: boolean;
|
|
unconfirmed: number;
|
|
}
|
|
|
|
/** 금액 칸 — 0.1원 자리까지(금액란 규칙). 못 푼 줄은 빈칸. */
|
|
function won(value: number | undefined): string {
|
|
return value === undefined ? "" : num(value, 1);
|
|
}
|
|
|
|
/** 장 아래 일위대가 칸 — 받는 동안 안내를 두고, 오면 표로 갈음. 저장 뒤엔 다시 받음. */
|
|
export function unitPriceSection(
|
|
projectId: string,
|
|
sheetKey: string,
|
|
sheetRows: { no: number; name: string; unit: string }[],
|
|
): HTMLElement {
|
|
const wrap = el("div", "b08-grid");
|
|
const load = async (): Promise<void> => {
|
|
wrap.replaceChildren(
|
|
el("p", "b08-grid__caption", "일위대가(미리보기) 불러오는 중… 단가표 첫 조립은 십여 초"),
|
|
);
|
|
try {
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/structure-sheets/unit-price?sheet_key=${encodeURIComponent(sheetKey)}`,
|
|
{ credentials: "include" },
|
|
);
|
|
const payload = (await response.json().catch(() => ({}))) as {
|
|
unit_price?: UnitPriceTable | null;
|
|
editor?: UnitPriceEditorData;
|
|
message?: string;
|
|
};
|
|
if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`);
|
|
const table = payload.unit_price;
|
|
const editor = payload.editor;
|
|
if (!table || !editor) {
|
|
wrap.replaceChildren(el("p", "b08-grid__caption", "이 양식에는 일위대가 줄이 아직 없음"));
|
|
return;
|
|
}
|
|
const open = el("button", "", editor.edited ? "줄 고치기 (고친 조합)" : "줄 고치기");
|
|
open.type = "button";
|
|
open.style.alignSelf = "flex-start";
|
|
const view = table.rows.length
|
|
? render(table)
|
|
: [el("p", "b08-grid__caption", "일위대가 줄이 아직 없음 — [줄 고치기]로 더함")];
|
|
open.addEventListener("click", () => {
|
|
open.disabled = true;
|
|
const panel = unitPriceEditor({
|
|
projectId,
|
|
sheetKey,
|
|
sheetRows,
|
|
data: editor,
|
|
onSaved: () => void load(),
|
|
onClose: () => {
|
|
panel.remove();
|
|
open.disabled = false;
|
|
},
|
|
});
|
|
wrap.append(panel);
|
|
});
|
|
wrap.replaceChildren(...view, open);
|
|
} catch (error) {
|
|
wrap.replaceChildren(
|
|
el(
|
|
"p",
|
|
"b08-grid__caption b08-grid__caption--warn",
|
|
`일위대가를 불러오지 못함 — ${error instanceof Error ? error.message : ""}`,
|
|
),
|
|
);
|
|
}
|
|
};
|
|
void load();
|
|
return wrap;
|
|
}
|
|
|
|
function render(table: UnitPriceTable): HTMLElement[] {
|
|
const total = table.complete
|
|
? `${num(table.total, 0)}원`
|
|
: `미완 — 막힌 줄 ${table.blocked} · 선 줄만 ${num(table.total, 0)}원`;
|
|
const head = el(
|
|
"p",
|
|
"b08-sheet__head",
|
|
`일위대가 ${table.code} · ${table.unit}당 ${total} (미리보기 — 내역 금액은 원가계산이 셈)`,
|
|
);
|
|
if (table.unconfirmed) {
|
|
head.append(el("span", "b08-unit__badge", `미확정 ${table.unconfirmed}건`));
|
|
}
|
|
const scroller = el("div", "b08-grid__scroll");
|
|
const grid = el("table", "b08-grid__table b08-grid__table--summary");
|
|
const headRow = document.createElement("tr");
|
|
for (const label of [
|
|
"공종·자원",
|
|
"코드",
|
|
"수량",
|
|
"단위",
|
|
"재료비",
|
|
"노무비",
|
|
"경비",
|
|
"합계",
|
|
"비고",
|
|
]) {
|
|
headRow.append(el("th", "", label));
|
|
}
|
|
const thead = document.createElement("thead");
|
|
thead.append(headRow);
|
|
const tbody = document.createElement("tbody");
|
|
for (const row of table.rows) {
|
|
const tr = document.createElement("tr");
|
|
const manual = row.manual
|
|
? `수동 단가 — ${row.manual_source || "출처 없음"} · ${row.manual_entered_at ?? ""}`
|
|
: "";
|
|
const note = row.skipped ? `안 섬 — ${row.reason}` : row.reason ? `⚠ ${row.reason}` : manual;
|
|
// 단가표 이름에 갈래가 이미 들었거나 규격 칸이 코드 자체면 겹쳐 적지 않음.
|
|
const spec =
|
|
row.spec && !row.name.includes(row.spec) && !row.ref_code.includes(row.spec) ? row.spec : "";
|
|
const money = [row.material, row.labor, row.expense, row.total].map((value) => {
|
|
const cell = el("td", row.manual ? "b08-unit__manual" : "", won(value));
|
|
if (row.manual) cell.title = manual;
|
|
return cell;
|
|
});
|
|
tr.append(
|
|
el("td", "", spec ? `${row.name} (${spec})` : row.name),
|
|
el("td", "", row.ref_code),
|
|
el("td", "", row.quantity === null ? "" : num(row.quantity, 3)),
|
|
el("td", "", row.unit),
|
|
...money,
|
|
el("td", "", note),
|
|
);
|
|
tbody.append(tr);
|
|
}
|
|
const foot = document.createElement("tr");
|
|
foot.append(
|
|
el("td", "", "계"),
|
|
el("td", "", ""),
|
|
el("td", "", ""),
|
|
el("td", "", ""),
|
|
el("td", "", won(table.material)),
|
|
el("td", "", won(table.labor)),
|
|
el("td", "", won(table.expense)),
|
|
el("td", "", table.complete ? num(table.total, 0) : `미완 ${num(table.total, 0)}`),
|
|
el("td", "", ""),
|
|
);
|
|
tbody.append(foot);
|
|
grid.append(thead, tbody);
|
|
scroller.append(grid);
|
|
return [head, scroller];
|
|
}
|