- 서버가 내역 줄 성분 단가에 율을 얹어 다시 셈(줄 칸이 전체 칸보다 우선) · 수량은 그대로 - 공용 작은 버튼 글자색 상속 — 어두운 화면에서 검정 글씨로 묻히던 것 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
/* =============================================================================
|
|
* B09_Estimation_UI_BillRates.ts
|
|
* 설계내역서 **할증율·일괄보정** 칸 — STmate `wBoqRate`·`wM_Rate` 본 (PLAN 12장 2차 ④)
|
|
*
|
|
* - 범위: 선택 항목만(줄마다 한 칸) / 내역서 전체(`*`) · 방식: 같은 비율 / 비목별(재·노·경)
|
|
* - 비목마다 절사·반올림 × 소수 0~4 자리(안 고르면 원 미만 절사).
|
|
* - ⚠ 여러 줄을 한꺼번에 바꾸므로 「할증 전부 되돌리기」를 둠 — 줄마다 ↺ 도 있음.
|
|
* - 화면은 고른 값만 보냄 — 금액은 서버가 다시 셈(내역 한 벌을 새로 받음).
|
|
* ========================================================================== */
|
|
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import { L, el, userMark } from "./B09_Estimation_UI_Sheet";
|
|
import { loadEdits, saveEdits, type BillRowDto, type EditChange } from "./B09_Estimation_UI_Store";
|
|
|
|
const SECTION = "bill_rates";
|
|
const PARTS = [
|
|
["material", "B09_Sheet_Col_Material"],
|
|
["labor", "B09_Sheet_Col_Labor"],
|
|
["expense", "B09_Sheet_Col_Expense"],
|
|
] as const;
|
|
|
|
function save(projectId: string, changes: EditChange[], reload: () => void): void {
|
|
if (changes.length === 0) return;
|
|
void saveEdits(projectId, changes)
|
|
.then(() => {
|
|
showToast(L("B09_Sheet_Saved"), "success");
|
|
reload();
|
|
})
|
|
.catch((error: Error) => showToast(`${L("B09_Sheet_SaveFailed")} ${error.message}`, "error"));
|
|
}
|
|
|
|
function numberBox(placeholder: string): HTMLInputElement {
|
|
const input = el("input", "b09s-edit-input");
|
|
input.type = "text";
|
|
input.inputMode = "decimal";
|
|
input.placeholder = placeholder;
|
|
return input;
|
|
}
|
|
|
|
function choice(options: Array<[string, string]>): HTMLSelectElement {
|
|
const select = el("select");
|
|
for (const [value, label] of options) {
|
|
const option = el("option", "", label);
|
|
option.value = value;
|
|
select.append(option);
|
|
}
|
|
return select;
|
|
}
|
|
|
|
/** 할증·일괄보정 칸 한 벌 — `selected` = 고른 줄의 할증 칸 이름. */
|
|
export function rateForm(
|
|
projectId: string,
|
|
selected: Set<string>,
|
|
reload: () => void,
|
|
): HTMLElement {
|
|
const box = el("div", "b09s-group");
|
|
const scope = choice([
|
|
["selected", L("B09_Sheet_Rate_Selected")],
|
|
["all", L("B09_Sheet_Rate_All")],
|
|
]);
|
|
const way = choice([
|
|
["same", L("B09_Sheet_Rate_Same")],
|
|
["parts", L("B09_Sheet_Rate_Parts")],
|
|
]);
|
|
const same = numberBox("%");
|
|
const parts = PARTS.map(([key, label]) => {
|
|
const rate = numberBox("%");
|
|
const mode = choice([
|
|
["floor", L("B09_Sheet_Rate_Floor")],
|
|
["round", L("B09_Sheet_Rate_Round")],
|
|
]);
|
|
const digits = choice(
|
|
[0, 1, 2, 3, 4].map((n): [string, string] => [
|
|
String(n),
|
|
`${L("B09_Sheet_Rate_Digits")} ${n}`,
|
|
]),
|
|
);
|
|
const line = el("span", "b09s-inline");
|
|
line.append(el("span", "b09s-head", L(label)), rate, mode, digits);
|
|
return { key, rate, mode, digits, line };
|
|
});
|
|
|
|
const apply = el("button", "b09s-undo", L("B09_Sheet_Apply"));
|
|
apply.type = "button";
|
|
apply.addEventListener("click", () => {
|
|
const rounding = Object.fromEntries(
|
|
parts.map((part) => [part.key, { mode: part.mode.value, digits: Number(part.digits.value) }]),
|
|
);
|
|
const value =
|
|
way.value === "same"
|
|
? { all: same.value.trim() || "0", rounding }
|
|
: {
|
|
...Object.fromEntries(parts.map((part) => [part.key, part.rate.value.trim() || "0"])),
|
|
rounding,
|
|
};
|
|
const keys = scope.value === "all" ? ["*"] : [...selected];
|
|
if (keys.length === 0) {
|
|
showToast(L("B09_Sheet_Rate_PickRows"), "info");
|
|
return;
|
|
}
|
|
save(
|
|
projectId,
|
|
keys.map((key) => ({ section: SECTION, key, value })),
|
|
reload,
|
|
);
|
|
});
|
|
|
|
const clear = el("button", "b09s-undo", L("B09_Sheet_Rate_ClearAll"));
|
|
clear.type = "button";
|
|
clear.addEventListener("click", () => {
|
|
void loadEdits(projectId).then((stored) =>
|
|
save(
|
|
projectId,
|
|
Object.keys(stored.edits[SECTION] ?? {}).map((key) => ({
|
|
section: SECTION,
|
|
key,
|
|
value: null,
|
|
})),
|
|
reload,
|
|
),
|
|
);
|
|
});
|
|
|
|
const head = el("div", "b09s-bar");
|
|
head.append(
|
|
el("span", "b09s-head", L("B09_Sheet_Rate")),
|
|
scope,
|
|
way,
|
|
same,
|
|
apply,
|
|
clear,
|
|
el("span", "b09s-hint", `${L("B09_Sheet_Rate_SelectedCount")} ${selected.size}`),
|
|
);
|
|
const partLine = el("div", "b09s-bar");
|
|
partLine.append(...parts.map((part) => part.line));
|
|
box.append(head, partLine);
|
|
return box;
|
|
}
|
|
|
|
/** 줄 비고의 할증 표시 — 그 줄 칸으로 얹혔으면 「사용자」+↺, 내역서 전체로 얹혔으면 표시만. */
|
|
export function rateMark(
|
|
projectId: string,
|
|
row: BillRowDto,
|
|
reload: () => void,
|
|
): HTMLElement | null {
|
|
if (!row.rate) return null;
|
|
if (row.rate.source === "*")
|
|
return el("span", "b09s-badge b09s-badge--user", L("B09_Sheet_Rate_AllBadge"));
|
|
return userMark(() =>
|
|
save(projectId, [{ section: SECTION, key: row.rate_key, value: null }], reload),
|
|
);
|
|
}
|