Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
# Conflicts: # B09_Estimation/B09_Estimation_UI_Page.css # B09_Estimation/B09_Estimation_UI_Shell.ts # B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts
This commit is contained in:
@@ -13,8 +13,11 @@
|
||||
· 견적 = 사용자 손입력(quote · quote_page · 견적서 파일은 `B09_Estimation_Router_Quotes`)
|
||||
· 관급 · 별도계상 = 사용자 표시(없으면 입력 줄 기본 · B08 파형강관 = 관급)
|
||||
→ 재료비목록표 단가 = 적용 단가
|
||||
재료비목록표 = 수량(B08 총괄자재수량집계표 합계 열) × 적용 단가 = 총액 ROUND(·, 0)
|
||||
(엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸)
|
||||
재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0)
|
||||
(엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸) · 수량 =
|
||||
입력 `material_qty`(183-1 · 서버 1차 셈 역전개 총수량 = 재료비수량금액집계표 수량 ·
|
||||
없는 키 빈칸) ·
|
||||
`material_qty` 가 없으면 입력 줄 수량(B08 총괄 합계 열)
|
||||
중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임)
|
||||
+ 연료(재료 · ℓ/hr × 적용 단가) + 잡품(재료 · 연료 금액 × % ÷ 100)
|
||||
줄 = ROUNDDOWN(단가 × 수량, 1) · 비목 합 = ROUNDDOWN(Σ, 0) → 중기목록표
|
||||
@@ -178,14 +181,18 @@ def compare_sheet(
|
||||
return rows
|
||||
|
||||
|
||||
def material_sheet(compare: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""재료비목록표 — 단가 = 대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0)."""
|
||||
keep = ("no", "key", "name", "spec", "unit", "supply", "origin", "qty")
|
||||
def material_sheet(
|
||||
compare: list[dict[str, Any]], qty_of: dict[str, Any] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""재료비목록표 — 단가 = 대비표 적용 단가 · 수량 = `qty_of`(183-1) ?? 줄 수량 ·
|
||||
총액 = ROUND(수량 × 단가, 0)."""
|
||||
keep = ("no", "key", "name", "spec", "unit", "supply", "origin")
|
||||
rows = []
|
||||
for row in compare:
|
||||
price, q = row["applied"], row["qty"]
|
||||
price = row["applied"]
|
||||
q = row["qty"] if qty_of is None else num(qty_of.get(row["key"]))
|
||||
amount = None if price is None or q is None else round_half(q * price, 0)
|
||||
rows.append({**{k: row[k] for k in keep}, "price": price, "amount": amount})
|
||||
rows.append({**{k: row[k] for k in keep}, "qty": q, "price": price, "amount": amount})
|
||||
return rows
|
||||
|
||||
|
||||
@@ -328,7 +335,7 @@ def compute_base(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]
|
||||
"expense": expense,
|
||||
"basis": basis,
|
||||
"compare": compare,
|
||||
"material": material_sheet(compare),
|
||||
"material": material_sheet(compare, inputs.get("material_qty")),
|
||||
"machine_blocks": blocks,
|
||||
"machine": machine,
|
||||
"gaps": gaps,
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
* 노무비 · 경비목록표 = 굳힌 단가 · 환율및기초자료 = 운전사 시간 노임 ROUNDDOWN(일당 × 분자 ÷ 분모, 0)
|
||||
* 자재단가대비표 = 출처 중 최저 자동(견적 포함) · 적용 = 고른 출처 › 사용자 단가 › 최저 ·
|
||||
* 관급 · 별도 표시 → 재료비목록표
|
||||
* 재료비목록표 = 수량(B08 총괄 합계 열) × 적용 단가 = 총액 ROUND(·, 0) · 하나라도 빈칸이면 빈칸
|
||||
* 재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0) · 하나라도 빈칸이면 빈칸 · 수량 = 입력
|
||||
* material_qty(183-1 · 서버 역전개 총수량 · 없는 키 빈칸) ?? 입력 줄 수량(B08 총괄 합계 열)
|
||||
* 중기사용료 = 손료 + 운전사 + 연료 + 잡품 · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 → 중기목록표
|
||||
* 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)에 남김
|
||||
* ========================================================================== */
|
||||
@@ -157,13 +158,14 @@ function compareSheet(materials: Row[], user: Row, gaps: Gap[]): Row[] {
|
||||
});
|
||||
}
|
||||
|
||||
function materialSheet(compare: Row[]): Row[] {
|
||||
const keep = ["no", "key", "name", "spec", "unit", "supply", "origin", "qty"];
|
||||
function materialSheet(compare: Row[], qtyOf?: Row | null): Row[] {
|
||||
const keep = ["no", "key", "name", "spec", "unit", "supply", "origin"];
|
||||
return compare.map((row) => {
|
||||
const price: Num = row.applied;
|
||||
const q: Num = row.qty;
|
||||
const q: Num = qtyOf == null ? row.qty : num(qtyOf[row.key]);
|
||||
return {
|
||||
...Object.fromEntries(keep.map((k) => [k, row[k]])),
|
||||
qty: q,
|
||||
price,
|
||||
amount: price === null || q === null ? null : roundHalf(q * price, 0),
|
||||
};
|
||||
@@ -301,7 +303,7 @@ export function computeBase(inputs: Row, user: Row): Row {
|
||||
expense,
|
||||
basis,
|
||||
compare,
|
||||
material: materialSheet(compare),
|
||||
material: materialSheet(compare, inputs.material_qty),
|
||||
machine_blocks: blocks,
|
||||
machine,
|
||||
gaps,
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
연결된 사본 = `M02_Item_Snapshot.linked_snapshots`(지정 ∩ 같은 항목 사본 · B08 · B09 공용).
|
||||
사본마다 `일위대가.json` 담은 로직을 그 사본 단가 묶음(`단가.json`) · 입력값 · 고른자재로 돌리며
|
||||
실제로 부른 자원 줄(인력 LB · 기계 EQ · 자재 MT · 유가 MO)만 모음 — 단가 묶음 통째
|
||||
(고르기 후보 전부)는 안 씀 · 값은 쓰지 않음(단가는 원가 한 벌 `price_book.json` 이 맡음).
|
||||
(고르기 후보 전부)는 안 씀 · 값은 쓰지 않음(단가는 원가 한 벌 `price_book.json` 이 맡음) ·
|
||||
사본 일위대가 자재 · 굳힌 사전조합 자재 코드도(183-1 · 로직을 안 돌아 `get` 에 안 걸림).
|
||||
고르기가 훑은 후보 줄도 걸림 → 원가 길이 두 번 셈해 쓰는 줄만 남김(`Summary.used_materials`).
|
||||
구조물 조합 사본 없으면 그 구조물은 없음 · 일위대가 없는 사본은 자원 없음.
|
||||
"""
|
||||
|
||||
@@ -57,6 +59,10 @@ def used_keys(folder: Path) -> list[str]:
|
||||
labor_doc.run_line(
|
||||
snapshot, one["로직"], one.get("입력값"), one.get("켜쌓기율"), one.get("고름")
|
||||
)
|
||||
for mat in [*(doc.get("자재") or []), *(m for d in inner for m in d.get("자재") or [])]:
|
||||
code = str(mat.get("코드") or "") if isinstance(mat, dict) else ""
|
||||
if code[:2] in RESOURCE_TABLES and code not in seen:
|
||||
seen.append(code)
|
||||
return seen
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs
|
||||
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys
|
||||
from B09_Estimation.B09_Estimation_Rates import rate_candidates, rate_table
|
||||
from B09_Estimation.B09_Estimation_Router_Quotes import router as quotes_router
|
||||
from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines
|
||||
from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines, used_materials
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["B09 Estimation"])
|
||||
|
||||
@@ -81,6 +81,19 @@ def compute_sheets(root: Path, inputs: dict[str, Any], user: dict[str, Any]) ->
|
||||
return sheets
|
||||
|
||||
|
||||
def compute_twice(root: Path, inputs: dict[str, Any], user: dict[str, Any]) -> tuple[dict, dict]:
|
||||
"""183-1 두 번 셈 — 1차 셈으로 쓰는 자재만 남기고(`used_materials`) 재료비목록표 수량 =
|
||||
1차 역전개 총수량(`material_qty` = 재료비수량금액집계표 수량) → 2차 셈 · 집계표.
|
||||
`inputs` 를 고쳐 둠(화면 짝이 거른 입력 · 수량을 그대로 받음)."""
|
||||
first = compute_sheets(root, inputs, user)
|
||||
summary = compute_summary(first, hopyo_lines(first))
|
||||
keep = used_materials(first, inputs, user)
|
||||
inputs["materials"] = [m for m in inputs.get("materials") or [] if m["key"] in keep]
|
||||
inputs["material_qty"] = {row["key"]: row["qty"] for row in summary["sum_material"]}
|
||||
sheets = compute_sheets(root, inputs, user)
|
||||
return sheets, compute_summary(sheets, hopyo_lines(sheets))
|
||||
|
||||
|
||||
async def _demand(root: Path) -> list[str]:
|
||||
"""단가 수요 키 — 연결된 사본 + 설계내역서 손 줄 로직(166-2)."""
|
||||
keys = await asyncio.to_thread(linked_resource_keys, root)
|
||||
@@ -103,7 +116,7 @@ async def payload(project_id: UUID, root: Path) -> dict[str, Any]:
|
||||
inputs["hopyo"] = hopyo_prices(root)
|
||||
inputs["daega"] = await asyncio.to_thread(daega_inputs, root)
|
||||
user = await asyncio.to_thread(store.read_user, root)
|
||||
sheets = await asyncio.to_thread(compute_sheets, root, inputs, user)
|
||||
sheets, summary = await asyncio.to_thread(compute_twice, root, inputs, user)
|
||||
return {
|
||||
"inputs": inputs,
|
||||
"user": user,
|
||||
@@ -113,7 +126,7 @@ async def payload(project_id: UUID, root: Path) -> dict[str, Any]:
|
||||
"rate_table": rate_table(),
|
||||
"notes": notes,
|
||||
# 집계표 넷 = 서버 단독(145-5) — 짝 셈 결과를 역전개 · 조작 중엔 안 바뀜
|
||||
"summary": compute_summary(sheets, hopyo_lines(sheets)),
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -155,6 +155,25 @@ def block_lines(blocks: list[dict[str, Any]] | None) -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
def used_materials(sheets: dict[str, Any], inputs: dict[str, Any], user: dict[str, Any]) -> set:
|
||||
"""재료비목록표에 남길 자재 키(183-1) — 대가 · 산근 · 중기 블록 재료 줄(수량 빈칸 · 0 도 =
|
||||
집계표 역전개와 같은 줄) · B08 총괄 줄(수량 있는 입력) · 관급 · 별도 표시 줄.
|
||||
그 밖(고르기 후보 줄) = 뺌."""
|
||||
blocks = [*(sheets.get("sg") or []), *(sheets.get("daega_blocks") or [])]
|
||||
blocks += sheets.get("machine_blocks") or []
|
||||
keep = {
|
||||
str((line.get("ref") or {}).get("key"))
|
||||
for block in blocks
|
||||
for line in block.get("lines") or []
|
||||
if line.get("kind") == "material" and (line.get("ref") or {}).get("key")
|
||||
}
|
||||
marks = user.get("materials") or {}
|
||||
for m in inputs.get("materials") or []:
|
||||
if "qty" in m or m.get("supply") or (marks.get(m["key"]) or {}).get("supply"):
|
||||
keep.add(m["key"])
|
||||
return keep
|
||||
|
||||
|
||||
def hopyo_lines(sheets: dict[str, Any]) -> dict[str, Any]:
|
||||
"""호표 구성(1 단위당 자원 · 겹 호표) — 산근(145-1) + 대가(145-2 · 들어오면)."""
|
||||
return {**block_lines(sheets.get("sg")), **block_lines(sheets.get("daega_blocks"))}
|
||||
|
||||
@@ -106,3 +106,9 @@
|
||||
.b09-estimation .ui-data-table__table tbody tr.is-pick td:last-child {
|
||||
--pick-r: inset -2px 0 0 var(--color-danger);
|
||||
}
|
||||
|
||||
/* 183-3 자재단가대비표 — 적용 단가를 낸 출처 단가 칸(고른 출처 · 기본 최저) */
|
||||
.b09-estimation .ui-data-table__table tbody td.is-picked {
|
||||
background: color-mix(in srgb, var(--color-accent) 22%, var(--color-canvas));
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
buildMaterial,
|
||||
} from "./B09_Estimation_UI_Tabs_Base";
|
||||
import { buildBill } from "./B09_Estimation_UI_Tabs_Bill";
|
||||
import { attachRowMenu, type BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
import { attachRowMenu, openMenuAt, type BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
|
||||
import {
|
||||
buildSumExpense,
|
||||
buildSumLabor,
|
||||
@@ -206,6 +206,7 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
rowClass: extra
|
||||
? (i) => [spec.rowClass?.(i), extra(i)].filter(Boolean).join(" ") || null
|
||||
: spec.rowClass,
|
||||
cellClass: spec.cellClass,
|
||||
onCellClick: (i, key) => {
|
||||
if (spec.onCell) spec.onCell(i, key);
|
||||
if (spec.link && !spec.editable?.(i, key)) goto(spec.link(i, key));
|
||||
@@ -215,6 +216,17 @@ export function renderB09Estimation(root: HTMLElement): void {
|
||||
// 166-2 줄 우클릭 메뉴(설계내역서) — 공용 표는 그대로 · 표 root 에 붙임
|
||||
const menu = (spec as BillTable).menu;
|
||||
if (menu) attachRowMenu(made.root, menu);
|
||||
// 183-3 칸 고르기 메뉴 — 누른 칸 아래에 B09 공용 메뉴
|
||||
if (spec.choices)
|
||||
made.root.addEventListener("click", (event) => {
|
||||
const cell = (event.target as Element).closest<HTMLTableCellElement>("tr[data-i] td");
|
||||
const column = cell ? spec.columns[cell.cellIndex] : undefined;
|
||||
const items =
|
||||
cell && column && spec.choices?.(Number(cell.parentElement?.dataset.i), column.key);
|
||||
if (!cell || !items?.length) return;
|
||||
const box = cell.getBoundingClientRect();
|
||||
openMenuAt(box.left, box.bottom, items);
|
||||
});
|
||||
// 157-3 칸 풍선 — 공용 표는 그대로 · 올린 칸에 title
|
||||
if (spec.tip)
|
||||
made.root.addEventListener("mouseover", (event) => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 탭별 표 = `…_UI_Tabs_Base`(144) · `…_UI_Tabs_Cost`(145) · 여기는 차례 · 꼴 · 연결 점검만
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import type { DataColumn, DataRow } from "@ui/ui_template_data_table";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
|
||||
@@ -107,6 +108,10 @@ export interface TabTable {
|
||||
rowClass?: (index: number) => string | null | undefined;
|
||||
/** 칸 풍선 글(157-3 · 예 「이름 규격」 칸의 원문번호) */
|
||||
tip?: (index: number, key: string) => string | null | undefined;
|
||||
/** 칸 꼴 class(183-3 공용 표 `cellClass` · 예 고른 출처 칸 강조) */
|
||||
cellClass?: (index: number, key: string) => string | null | undefined;
|
||||
/** 칸 누름 고르기 메뉴(183-3 · 칸 아래 공용 메뉴 · 빈 것 = 안 엶) */
|
||||
choices?: (index: number, key: string) => MapContextMenuItem[] | null;
|
||||
}
|
||||
|
||||
export type TabBuild = (ctx: TabContext) => TabTable | string;
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
* B09_Estimation_UI_Tabs_Base.ts
|
||||
* B09 바탕 단가 탭 일곱(PLAN 144-2~4) — 노무비 · 경비목록표 · 환율및기초자료 · 자재단가대비표 ·
|
||||
* 재료비목록표 · 중기사용료 · 중기목록표(실무 엑셀 꼴 · 값 = `B09_Estimation_Calc_Base`).
|
||||
* 재료비목록표 = 엑셀 꼴 + 수량(B08 합계) · 총액 열(166-3)
|
||||
* 재료비목록표 = 엑셀 꼴 + 수량 · 총액 열(166-3) · 줄 = 대가 · 산근 · 중기가 쓰는 자재 + B08 줄 ·
|
||||
* 관급 · 별도 줄(183-1 · 서버가 거른 입력) · 수량 = 역전개 총수량 · 비고 = 쓰는 곳(`usesOf`)
|
||||
*
|
||||
* 고칠 칸 = 자재단가대비표 적용 단가(빈칸으로 지우면 최저 자동) · 관급 / 별도 칸 누름 = 표시 바꿈
|
||||
* · 견적 단가 · 면 손입력 · 적용 「출처」 칸 누름 = 고를 출처 돌림(값 있는 출처만 · 견적이 높아도)
|
||||
* · 「견적서」 칸 누름 = 파일 모달(`B09_Estimation_UI_Tabs_Base_Quote`)(166-4)
|
||||
* 자재단가대비표 적용 단가 칸 누름 = 고르기 메뉴(183-3 · 값 있는 출처 · 견적 · 「최저(자동)」 →
|
||||
* `pick` · 옛 수기 `applied` 지움) · 고른 출처 단가 칸 강조(`is-picked` · 기본 = 최저 출처) ·
|
||||
* 관급 / 별도 칸 누름 = 표시 바꿈 · 견적 단가 손입력
|
||||
* · 「견적서」 칸(견적 묶음 아래 · 183-2) 누름 = 파일 모달(`B09_Estimation_UI_Tabs_Base_Quote`)(166-4)
|
||||
* · 출처 묶음 둘째 칸 = 「출처」(M01 면수 · 유가 기준일 값 그대로 · 183-2 · 견적엔 없음)
|
||||
* 이동 = 재료비목록표 줄 → 대비표 · 중기목록표 줄 → 중기사용료 블록 · 블록 줄 → 경비 · 기초 · 대비표
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
import { SOURCES, type Row } from "./B09_Estimation_Calc_Base";
|
||||
import { openQuotes } from "./B09_Estimation_UI_Tabs_Base_Quote";
|
||||
@@ -107,10 +111,18 @@ export const buildBasis: TabBuild = ({ sheets }): TabTable => {
|
||||
|
||||
const SUPPLY_NEXT: Record<string, string | null> = { "": "관급", 관급: "별도", 별도: null };
|
||||
|
||||
/** 고를 출처 돌림 — 자동 → 값 있는 출처 차례 → 자동 */
|
||||
function nextPick(r: Row): string | null {
|
||||
/** 적용 단가 고르기 항목(183-3) — 값 있는 출처(견적 포함 · 0 뺌) 「출처 단가 · 최저」 + 「최저(자동)」 ·
|
||||
* `pick(s)` = 그 출처 · null = 자동 */
|
||||
function appliedChoices(r: Row, pick: (s: string | null) => void): MapContextMenuItem[] {
|
||||
const have = SOURCES.filter((s) => (r.prices[s] ?? 0) > 0);
|
||||
return have[have.indexOf(r.pick) + 1] ?? null;
|
||||
const label = (s: string): string =>
|
||||
`${s === r.applied_from ? "✓ " : ""}${s} ${Number(r.prices[s]).toLocaleString("ko-KR")}` +
|
||||
(s === r.lowest_from ? " · 최저" : "");
|
||||
return [
|
||||
...have.map((s): MapContextMenuItem => [label(s), () => pick(s)]),
|
||||
"-",
|
||||
[`${r.pick || r.applied_from === "사용자" ? "" : "✓ "}최저(자동)`, () => pick(null)],
|
||||
];
|
||||
}
|
||||
|
||||
export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => {
|
||||
@@ -148,35 +160,42 @@ export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => {
|
||||
text("name", "품명", 180),
|
||||
text("spec", "규격", 140),
|
||||
text("unit", "단위", 56),
|
||||
...SOURCES.flatMap((s) => [money(`p:${s}`, "단가", [s]), text(`g:${s}`, "면", 64, [s])]),
|
||||
// 183-2 — 견적 = 단가 · 견적서 · 그 밖 = 단가 · 출처(면수 값)
|
||||
...SOURCES.flatMap((s) => [
|
||||
money(`p:${s}`, "단가", [s]),
|
||||
s === "견적" ? text("files", "견적서", 72, [s]) : text(`g:${s}`, "출처", 64, [s]),
|
||||
]),
|
||||
money("lowest", "최저"),
|
||||
money("applied", "적용 단가", ["적용"]),
|
||||
text("applied_from", "출처", 80, ["적용"]),
|
||||
text("supply", "관급 · 별도", 80),
|
||||
text("files", "견적서", 72),
|
||||
],
|
||||
rows: out,
|
||||
frozen: 2,
|
||||
hint: "적용 단가 = 출처 중 최저(0 · 빈칸 뺌 · 견적 포함) 자동 · 칸에 넣으면 사용자 값 · 지우면 다시 최저 · 「출처」 칸 누름 = 고를 출처 돌림(값 있는 출처 · 견적이 높아도 고름) · 견적 단가 · 면 = 수기 입력 · 「관급 · 별도」 칸 누름 = 없음 → 관급 → 별도 · 「견적서」 칸 누름 = 파일 올리기 · 보기",
|
||||
editable: (_i, key) => key === "applied" || key === "p:견적" || key === "g:견적",
|
||||
hint: "적용 단가 = 출처 중 최저(0 · 빈칸 뺌 · 견적 포함) 자동 · 「적용 단가」 칸 누름 = 고르기(값 있는 출처 · 견적 · 최저(자동) · 견적이 높아도 고름) · 고른 출처 단가 칸 강조 · 견적 단가 = 수기 입력(출처 없는 B08 줄은 견적으로 단가) · 「관급 · 별도」 칸 누름 = 없음 → 관급 → 별도 · 「견적서」 칸 누름 = 파일 올리기 · 보기",
|
||||
editable: (_i, key) => key === "p:견적",
|
||||
onEdit: (i, key, value) => {
|
||||
const row = out[i];
|
||||
if (!row) return;
|
||||
const cell = mine(String(row.key));
|
||||
if (key === "g:견적") {
|
||||
if (value.trim()) cell.quote_page = value.trim();
|
||||
else delete cell.quote_page;
|
||||
return put(String(row.key), cell);
|
||||
}
|
||||
const v = parseNumber(value);
|
||||
if (v === undefined) return;
|
||||
const field = key === "applied" ? "applied" : key === "p:견적" ? "quote" : null;
|
||||
if (!field) return;
|
||||
if (v === null) delete cell[field];
|
||||
else cell[field] = v;
|
||||
if (field === "applied") delete cell.pick; // 손 수 = 고른 출처를 풂
|
||||
if (!row || key !== "p:견적" || v === undefined) return;
|
||||
const cell = mine(String(row.key));
|
||||
if (v === null) delete cell.quote;
|
||||
else cell.quote = v;
|
||||
put(String(row.key), cell);
|
||||
},
|
||||
// 183-3 — 고른 출처(없으면 최저) 단가 칸 강조 · 적용 단가 칸 누름 = 고르기 메뉴
|
||||
cellClass: (i, key) => (key === `p:${rows[i]?.applied_from}` ? "is-picked" : null),
|
||||
choices: (i, key) => {
|
||||
const r = rows[i];
|
||||
if (key !== "applied" || !r) return null;
|
||||
return appliedChoices(r, (s) => {
|
||||
const cell = mine(r.key);
|
||||
delete cell.applied; // 옛 수기 적용 단가 — 고르면 풂
|
||||
if (s) cell.pick = s;
|
||||
else delete cell.pick;
|
||||
put(r.key, cell);
|
||||
});
|
||||
},
|
||||
onCell: (i, key) => {
|
||||
const row = out[i];
|
||||
const r = rows[i];
|
||||
@@ -192,12 +211,6 @@ export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => {
|
||||
put(r.key, now);
|
||||
});
|
||||
}
|
||||
if (key === "applied_from") {
|
||||
const pick = nextPick(r);
|
||||
if (pick) cell.pick = pick;
|
||||
else delete cell.pick;
|
||||
return put(String(row.key), cell);
|
||||
}
|
||||
if (key !== "supply") return;
|
||||
const next = SUPPLY_NEXT[String(row.supply ?? "")] ?? null;
|
||||
if (next) cell.supply = next;
|
||||
@@ -207,10 +220,33 @@ export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 쓰는 곳(183-1) — 자재 키 → 「대가 n호표」 · 「산근 n호표」 · 「중기 n호표」 · 「내역 자재대」
|
||||
* (블록 재료 줄 · 설계내역서 자재 항목 · 표시 잇기만 · 셈 아님) */
|
||||
function usesOf(sheets: Row): Map<string, string[]> {
|
||||
const out = new Map<string, string[]>();
|
||||
const add = (key: unknown, label: string): void => {
|
||||
if (!key) return;
|
||||
const list = out.get(String(key)) ?? [];
|
||||
if (!list.includes(label)) out.set(String(key), [...list, label]);
|
||||
};
|
||||
const scan = (blocks: Row[] | undefined, label: (b: Row, n: number) => string): void =>
|
||||
(blocks || []).forEach((b, n) =>
|
||||
(b.lines || []).forEach((ln: Row) => ln.kind === "material" && add(ln.ref?.key, label(b, n))),
|
||||
);
|
||||
scan(sheets.daega_blocks, (b) => `대가 ${b.no}호표`);
|
||||
scan(sheets.sg, (b) => `산근 ${b.no}호표`);
|
||||
scan(sheets.machine_blocks, (_b, n) => `중기 ${n + 1}호표`);
|
||||
for (const r of sheets.bill?.rows || [])
|
||||
if (r.kind === "item" && r.ref?.tab === "material") add(r.ref.key, "내역 자재대");
|
||||
return out;
|
||||
}
|
||||
|
||||
export const buildMaterial: TabBuild = ({ sheets }): TabTable => {
|
||||
const uses = usesOf(sheets);
|
||||
const rows: DataRow[] = (sheets.material || []).map((r: Row) => ({
|
||||
...r,
|
||||
supply: r.supply ?? "",
|
||||
uses: (uses.get(r.key) ?? []).join(" · ") || (r.origin === "B08" ? "B08 총괄" : ""),
|
||||
}));
|
||||
return {
|
||||
columns: [
|
||||
@@ -222,14 +258,14 @@ export const buildMaterial: TabBuild = ({ sheets }): TabTable => {
|
||||
money("price", "단가"),
|
||||
money("amount", "총액"),
|
||||
text("supply", "관급 · 별도", 80),
|
||||
text("origin", "비고", 90),
|
||||
text("uses", "비고(쓰는 곳)", 240),
|
||||
],
|
||||
rows,
|
||||
total: rows.length
|
||||
? { name: "계", amount: rows.reduce((sum, r) => sum + (Number(r.amount) || 0), 0) }
|
||||
: null,
|
||||
frozen: 2,
|
||||
hint: `수량 = B08 총괄자재수량집계표 합계 열(할증 포함 · 사본 자원 줄은 빈칸) · 단가 = 자재단가대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0) · 계 = 빈칸 뺀 합 · ${LINK_HINT}`,
|
||||
hint: `줄 = 연결된 일위대가 · 산근 · 중기가 쓰는 자재 + B08 총괄 줄 · 관급 · 별도 줄 · 수량 = 설계내역서 × 호표 역전개 총수량(재료비수량금액집계표와 같음 · B08 할증 값과 다를 수 있음) · 단가 = 자재단가대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0) · 계 = 빈칸 뺀 합 · 비고 = 쓰는 곳 · ${LINK_HINT}`,
|
||||
link: (i) => (rows[i] ? { tab: "compare", key: String(rows[i].key) } : null),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -348,17 +348,27 @@ export function openLinkMenu(ctx: TabContext, rows: DataRow[], i: number): void
|
||||
shared.open(box.left, box.bottom, more.length ? [...picks, "-", ...more] : picks);
|
||||
}
|
||||
|
||||
/** B09 메뉴 한 벌(우클릭 · 183-3 칸 고르기 같이 씀) — 화면 기준 자리 */
|
||||
function sharedMenu(): MapContextMenu {
|
||||
shared ??= createMapContextMenu("b09-estimation");
|
||||
if (!shared.element.isConnected) {
|
||||
shared.element.style.position = "fixed"; // 화면 기준 자리(clientX · Y)
|
||||
document.body.append(shared.element);
|
||||
}
|
||||
return shared;
|
||||
}
|
||||
|
||||
/** 화면 자리(x · y)에 B09 메뉴를 엶(183-3 칸 고르기) */
|
||||
export function openMenuAt(x: number, y: number, items: MapContextMenuItem[]): void {
|
||||
if (items.length) sharedMenu().open(x, y, items);
|
||||
}
|
||||
|
||||
/** 표에 줄 우클릭 메뉴를 붙임(공용 표는 안 고침) — 글 칸 위는 브라우저 메뉴(붙여넣기) 그대로 */
|
||||
export function attachRowMenu(
|
||||
root: HTMLElement,
|
||||
menu: (index: number) => MapContextMenuItem[],
|
||||
): void {
|
||||
shared ??= createMapContextMenu("b09-estimation");
|
||||
const ctxMenu = shared;
|
||||
if (!ctxMenu.element.isConnected) {
|
||||
ctxMenu.element.style.position = "fixed"; // 화면 기준 자리(clientX · Y)
|
||||
document.body.append(ctxMenu.element);
|
||||
}
|
||||
const ctxMenu = sharedMenu();
|
||||
// 칸 누름보다 먼저(잡기 단계) — 「연결 ▾」 메뉴를 그 칸 자리에 엶
|
||||
root.addEventListener(
|
||||
"click",
|
||||
|
||||
@@ -14,7 +14,7 @@ SPOTS = {
|
||||
"B09_Estimation/B09_Estimation_Calc_Bill.ts": ['"수기 줄 — 연결 없음(우클릭 [연결])"'],
|
||||
"B09_Estimation/B09_Estimation_Calc_Bill.py": ['"수기 줄 — 연결 없음(우클릭 [연결])"'],
|
||||
"B09_Estimation/B09_Estimation_Sangeun.py": ['f"수기 줄 {'],
|
||||
"B09_Estimation/B09_Estimation_UI_Tabs_Base.ts": ["견적 단가 · 면 = 수기 입력"],
|
||||
"B09_Estimation/B09_Estimation_UI_Tabs_Base.ts": ["견적 단가 = 수기 입력"],
|
||||
"B09_Estimation/B09_Estimation_UI_Tabs_Total.ts": ['"수기 입력"', '"수기 입력 = 안전관리비'],
|
||||
"ui_template/ui_template_locale_b2.ts": [
|
||||
"수량 원천: 수기 입력(직접비)",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""183-2 자재단가대비표 머리 — 견적 「면」 뺌 · 다른 출처 「면」 → 「출처」(값 그대로) · 견적서 = 견적 아래.
|
||||
|
||||
소스 시험(열 정의 · 고칠 칸) — 셈 · 저장(`quote_page` 남음)은 안 바뀜(test_166_4).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Base.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _compare() -> str:
|
||||
start = SRC.index("export const buildCompare")
|
||||
return SRC[start : SRC.index("export const buildMaterial")]
|
||||
|
||||
|
||||
def test_견적은_단가_견적서_그_밖은_단가_출처():
|
||||
body = _compare()
|
||||
assert (
|
||||
's === "견적" ? text("files", "견적서", 72, [s]) : text(`g:${s}`, "출처", 64, [s])' in body
|
||||
)
|
||||
assert '"면"' not in body # 면 머리 없음
|
||||
assert body.count('text("files", "견적서"') == 1 # 맨 끝 견적서 열 지움
|
||||
|
||||
|
||||
def test_견적_면_수기_칸_없앰():
|
||||
body = _compare()
|
||||
assert 'key === "g:견적"' not in body and "quote_page" not in body
|
||||
assert "견적 단가 = 수기 입력" in body and "견적 단가 · 면" not in body
|
||||
@@ -0,0 +1,113 @@
|
||||
"""183-3 자재단가대비표 적용 단가 — 칸 누름 고르기 메뉴 · 고른 출처 칸 강조 · 기본 최저.
|
||||
|
||||
- 공용 표 `cellClass` 옵션(칸 class · 안 주면 같은 글) — Node 로 `renderDataTableHtml`
|
||||
- 고르기 항목 `appliedChoices`(값 있는 출처 · 견적 · 최저(자동) · ✓ 표시) — Node 로 함수만 떼어 돌림
|
||||
- 배선: 수기 적용 칸 · 출처 칸 돌림 없앰 · Shell 이 cellClass · choices 를 넘김 · B09 메뉴 한 벌
|
||||
셈 · 저장(pick › applied › 최저)은 그대로(test_144 · test_166_4).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TS = ROOT / "config" / "node_modules" / "typescript"
|
||||
|
||||
|
||||
def _src(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _node(script: str):
|
||||
out = subprocess.run(
|
||||
["node", "-e", script], capture_output=True, text=True, encoding="utf-8", timeout=60
|
||||
)
|
||||
assert out.returncode == 0, out.stderr
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
PRELUDE = f"""
|
||||
const fs = require("fs");
|
||||
const ts = require({json.dumps(str(TS))});
|
||||
const js = (code) => ts.transpileModule(code, {{
|
||||
compilerOptions: {{ module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }},
|
||||
}}).outputText;
|
||||
"""
|
||||
|
||||
|
||||
def test_공용_표_cellClass_칸_class_안_주면_같은_글():
|
||||
table = json.dumps(str(ROOT / "ui_template" / "ui_template_data_table.ts"))
|
||||
got = _node(
|
||||
PRELUDE
|
||||
+ f"""
|
||||
const box = {{ exports: {{}} }};
|
||||
new Function("exports", "module", "require", js(fs.readFileSync({table}, "utf8")))(
|
||||
box.exports, box, (p) => (p.endsWith(".css") ? {{}} : require(p)));
|
||||
const m = box.exports;
|
||||
const o = {{ columns: [{{ key: "a", label: "A" }}, {{ key: "b", label: "B" }}],
|
||||
rows: [{{ a: 1, b: 2 }}, {{ a: 3, b: 4 }}] }};
|
||||
const marked = m.renderDataTableHtml({{ ...o,
|
||||
cellClass: (i, k) => (i === 0 && k === "b" ? "is-picked" : null) }});
|
||||
console.log(JSON.stringify({{
|
||||
plain: m.renderDataTableHtml(o),
|
||||
none: m.renderDataTableHtml({{ ...o, cellClass: () => null }}),
|
||||
count: marked.split("is-picked").length - 1,
|
||||
row0: marked.split("<tbody>")[1].split("</tr>")[0],
|
||||
}}));
|
||||
"""
|
||||
)
|
||||
assert got["plain"] == got["none"]
|
||||
assert got["count"] == 1 and '<td class="is-picked">2</td>' in got["row0"]
|
||||
|
||||
|
||||
def test_고르기_항목_값_있는_출처_견적_최저_자동():
|
||||
base = _src("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts")
|
||||
start = base.index("function appliedChoices")
|
||||
body = base[start : base.index("\n}\n", start) + 3]
|
||||
sources = _src("B09_Estimation/B09_Estimation_Calc_Base.ts")
|
||||
at = sources.index("export const SOURCES")
|
||||
line = sources[at : sources.index(";", at) + 1] + "\n"
|
||||
got = _node(
|
||||
PRELUDE
|
||||
+ f"""
|
||||
const appliedChoices = new Function(js({json.dumps(line.replace("export ", "") + body)}) + "; return appliedChoices;")();
|
||||
const r = {{ prices: {{ 관급: null, 물가정보: 1200, 물가자료: 0, 유통물가: 1000, 견적: 1500 }},
|
||||
lowest_from: "유통물가", applied_from: "유통물가", pick: null }};
|
||||
const seen = [];
|
||||
const items = appliedChoices(r, (s) => seen.push(s));
|
||||
items.forEach((x) => Array.isArray(x) && x[1]());
|
||||
const pick = {{ ...r, applied_from: "견적", pick: "견적" }};
|
||||
console.log(JSON.stringify({{
|
||||
labels: items.map((x) => (Array.isArray(x) ? x[0] : x)),
|
||||
seen,
|
||||
picked: appliedChoices(pick, () => 0).map((x) => (Array.isArray(x) ? x[0] : x)),
|
||||
}}));
|
||||
"""
|
||||
)
|
||||
assert got["labels"] == [
|
||||
"물가정보 1,200",
|
||||
"✓ 유통물가 1,000 · 최저",
|
||||
"견적 1,500",
|
||||
"-",
|
||||
"✓ 최저(자동)",
|
||||
]
|
||||
assert got["seen"] == ["물가정보", "유통물가", "견적", None]
|
||||
assert got["picked"][2] == "✓ 견적 1,500" and got["picked"][-1] == "최저(자동)"
|
||||
|
||||
|
||||
def test_배선_수기_적용_칸_출처_돌림_없앰():
|
||||
base = _src("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts")
|
||||
compare = base[base.index("export const buildCompare") : base.index("function usesOf")]
|
||||
assert 'editable: (_i, key) => key === "p:견적",' in compare
|
||||
assert "nextPick" not in base and 'key === "applied_from"' not in compare
|
||||
assert 'key === `p:${rows[i]?.applied_from}` ? "is-picked"' in compare
|
||||
assert 'if (key !== "applied" || !r) return null;' in compare
|
||||
assert "delete cell.applied" in compare
|
||||
shell = _src("B09_Estimation/B09_Estimation_UI_Shell.ts")
|
||||
assert (
|
||||
"cellClass: spec.cellClass," in shell and "openMenuAt(box.left, box.bottom, items)" in shell
|
||||
)
|
||||
edit = _src("B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts")
|
||||
assert "export function openMenuAt(" in edit and "const ctxMenu = sharedMenu();" in edit
|
||||
css = _src("B09_Estimation/B09_Estimation_UI_Page.css")
|
||||
assert "td.is-picked" in css and "var(--color-accent)" in css
|
||||
@@ -0,0 +1,88 @@
|
||||
"""183-1 재료비목록표 = 연결된 일위대가 · 산근 · 중기가 쓰는 자재만 · 수량 · 단가 한 줄 · 쓰는 곳.
|
||||
|
||||
- 수요: 사본 일위대가 자재 · 굳힌 사전조합 자재 코드도 든다(`Linked.used_keys`)
|
||||
- 거름: 대가 · 산근 · 중기 블록 재료 줄 · B08 줄 · 관급 · 별도 줄만 남김(`Summary.used_materials`)
|
||||
- 수량 = 입력 `material_qty`(1차 역전개 총수량) — `Calc_Base` 짝 거울 · 두 번 셈(`Router.compute_twice`)
|
||||
- 화면 비고 = 쓰는 곳(`UI_Tabs_Base.usesOf`)
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from B09_Estimation import B09_Estimation_Router as router_mod
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base
|
||||
from B09_Estimation.B09_Estimation_Linked import used_keys
|
||||
from B09_Estimation.B09_Estimation_Summary import used_materials
|
||||
from resources.tester.test_144_b09_base_calc import _run_ts
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
MATERIALS = [
|
||||
{"key": "MT1", "name": "쓰는 자재", "prices": {"물가자료": 1000}},
|
||||
{"key": "MT2", "name": "고르기 후보", "prices": {"물가자료": 500}},
|
||||
{"key": "MT3", "name": "관급 표시", "prices": {"물가자료": 300}},
|
||||
{"key": "MO1", "name": "경유", "prices": {"유가": 1800}},
|
||||
{"key": "b08:pipe_800", "name": "파형강관", "prices": {}, "qty": 8, "supply": "관급"},
|
||||
]
|
||||
USER = {"materials": {"MT3": {"supply": "관급"}}}
|
||||
SG = [{"key": "SG1", "no": 1, "lines": [{"kind": "material", "ref": {"key": "MT1"}, "qty": 2}]}]
|
||||
MACHINE = [{"key": "EQ1", "lines": [{"kind": "material", "ref": {"key": "MO1"}, "qty": None}]}]
|
||||
|
||||
|
||||
def test_사본_일위대가_자재_사전조합_자재_코드도_수요(tmp_path):
|
||||
labor = {
|
||||
"줄": {"담은로직": [], "자재": [{"코드": "MT000053"}, {"코드": "PM000001"}]},
|
||||
"참조사본": {"참조|system||SC-000001": {"담은로직": [], "자재": [{"코드": "MT000008"}]}},
|
||||
}
|
||||
(tmp_path / "일위대가.json").write_text(json.dumps(labor, ensure_ascii=False), "utf-8")
|
||||
assert used_keys(tmp_path) == ["MT000053", "MT000008"] # 개인 자재(PM) = 원가 표 밖
|
||||
|
||||
|
||||
def test_쓰는_자재만_남김():
|
||||
sheets = {"sg": SG, "daega_blocks": [], "machine_blocks": MACHINE}
|
||||
inputs = {"materials": MATERIALS}
|
||||
assert used_materials(sheets, inputs, USER) == {"MT1", "MO1", "MT3", "b08:pipe_800"}
|
||||
|
||||
|
||||
def test_수량_원천_material_qty_거울():
|
||||
inputs = {"labor": [], "machines": [], "materials": MATERIALS}
|
||||
old = {r["key"]: r["qty"] for r in compute_base(inputs, USER)["material"]}
|
||||
assert old["b08:pipe_800"] == 8 and old["MT1"] is None # 없으면 입력 줄 수량(지금 길)
|
||||
got = compute_base({**inputs, "material_qty": {"MT1": 6, "b08:pipe_800": 8}}, USER)
|
||||
rows = {r["key"]: r for r in got["material"]}
|
||||
assert (rows["MT1"]["qty"], rows["MT1"]["amount"]) == (6, 6000)
|
||||
assert rows["MT2"]["qty"] is None and rows["MT2"]["amount"] is None # 없는 키 = 빈칸
|
||||
for case in (inputs, {**inputs, "material_qty": {"MT1": 6}}):
|
||||
assert _run_ts(case, USER) == compute_base(case, USER)
|
||||
|
||||
|
||||
def test_두_번_셈_후보_줄_빠지고_수량_역전개(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake(root, inputs, user):
|
||||
calls.append([m["key"] for m in inputs["materials"]])
|
||||
base = compute_base(inputs, user)
|
||||
bill = {"rows": [{"kind": "item", "qty": 3, "ref": {"tab": "sg", "key": "SG1"}}]}
|
||||
return {**base, "sg": SG, "daega_blocks": [], "machine_blocks": MACHINE, "bill": bill}
|
||||
|
||||
monkeypatch.setattr(router_mod, "compute_sheets", fake)
|
||||
inputs = {"labor": [], "machines": [], "materials": list(MATERIALS)}
|
||||
sheets, summary = router_mod.compute_twice(Path("."), inputs, USER)
|
||||
assert calls[0] == [m["key"] for m in MATERIALS]
|
||||
assert calls[1] == ["MT1", "MT3", "MO1", "b08:pipe_800"] # 후보 MT2 뺌
|
||||
assert inputs["material_qty"] == {"MT1": 6.0} # 설계내역서 3 × 호표 2
|
||||
rows = {r["key"]: r for r in sheets["material"]}
|
||||
assert (rows["MT1"]["qty"], rows["MT1"]["amount"]) == (6.0, 6000)
|
||||
assert [r["qty"] for r in summary["sum_material"]] == [rows["MT1"]["qty"]] # 집계표와 같은 값
|
||||
|
||||
|
||||
def test_화면_비고_쓰는_곳():
|
||||
src = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Base.ts").read_text(encoding="utf-8")
|
||||
for label in (
|
||||
"`대가 ${b.no}호표`",
|
||||
"`산근 ${b.no}호표`",
|
||||
"`중기 ${n + 1}호표`",
|
||||
'"내역 자재대"',
|
||||
):
|
||||
assert label in src
|
||||
assert 'text("uses", "비고(쓰는 곳)"' in src and 'text("origin", "비고"' not in src
|
||||
@@ -60,6 +60,8 @@ export interface DataTableOptions {
|
||||
onEdit?: (index: number, key: string, text: string) => void;
|
||||
/** 줄 꼴 class(본문 줄만 · 예 `is-title` = 굵게) — 빈 값이면 안 붙임 */
|
||||
rowClass?: (index: number) => string | null | undefined;
|
||||
/** 칸 꼴 class(본문 줄 · 글 상자 아닌 칸만 · 예 `is-picked` = 고른 칸 강조) — 빈 값이면 안 붙임 */
|
||||
cellClass?: (index: number, key: string) => string | null | undefined;
|
||||
}
|
||||
|
||||
export interface DataTable {
|
||||
@@ -139,6 +141,7 @@ function rowHtml(
|
||||
editable?: (index: number, key: string) => boolean,
|
||||
focus = false,
|
||||
extra: string | null | undefined = null,
|
||||
cellClass?: (key: string) => string | null | undefined,
|
||||
): string {
|
||||
let tr = "";
|
||||
for (let j = 0; j < cols.length; j++) {
|
||||
@@ -152,7 +155,12 @@ function rowHtml(
|
||||
}
|
||||
const [txt, empty] = formatCell(r[key], fmts[j], blank);
|
||||
const frz = left[j] !== null;
|
||||
const cls = [frz && "is-frozen", frz && left[j + 1] == null && "is-edge", empty && "is-empty"]
|
||||
const cls = [
|
||||
frz && "is-frozen",
|
||||
frz && left[j + 1] == null && "is-edge",
|
||||
empty && "is-empty",
|
||||
cellClass?.(key),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
const attrs =
|
||||
@@ -218,6 +226,7 @@ export function renderDataTableHtml(o: DataTableOptions, win?: RowWindow): strin
|
||||
o.editable,
|
||||
o.focus === i,
|
||||
o.rowClass?.(i),
|
||||
o.cellClass ? (key) => o.cellClass?.(i, key) : undefined,
|
||||
);
|
||||
if (win) body += pad((n - to) * win.rowH, cols.length);
|
||||
const foot = o.total ? `<tfoot>${rowHtml(cols, fmts, left, o.total, false, blank)}</tfoot>` : "";
|
||||
|
||||
Reference in New Issue
Block a user