Files
Aislo/tools/stmate-test/scr_ui_dan.js
T

330 lines
18 KiB
JavaScript
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.
/**
* STMATE w 8.0 — 단가목록표 화면(wM_JabPum) 끼워 넣기 (scr_ui_dan.js)
* 근거: ui_form_catalog.txt wM_JabPum(cBoxCode 10종 · cBox_IFORM 명칭,규격/산근내용) · wM_Boxa(RG_MComp) · wM_Edt_Hal(wcBx_Up)
* 후보 5칸 = COSTN AMT_11~51(페이지 PG_11~51) · 채택 = PG_1 · 가격지 이름 = 초기값 JMOK1~5/JMK10~50
* 기초단가(M·L·S) 고침 = COSTN 자기 비목 칸 + 계산 원천 자원(manualPrice) → 연쇄 재계산
*/
(function (global) {
"use strict";
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
screens: {},
actions: {},
menus: [],
});
const U = global.STMATE_UI_DAN;
const esc = (s) =>
String(s == null ? "" : s).replace(
/[&<>"]/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
);
const $ = (id) => document.getElementById(id);
const prefs = () => (global.STMATE_UI_APP_MENU && global.STMATE_UI_APP_MENU.prefs) || {};
function st(ctx) {
const u = ctx.uiState;
u.dan = u.dan || {
kind: u.danFilter && u.danFilter !== "ALL" ? u.danFilter : "ALL",
kw: "",
mode: "name",
sort: "",
code: "",
};
return u.dan;
}
// 자원(계산 원천) 맞춤 — 기초단가 한 건
function syncRes(ctx, code, price, how) {
const res = (ctx.state.resources || []).find((r) => r.code === code && !r.isSynthetic);
if (res) Object.assign(res, { adoptMode: how || "manual", manualPrice: price });
}
function render(ctx, api) {
if (!ctx.dbfData)
return `<div class="win-form"><div class="win-form-header"><span>단가목록표 [wM_JabPum]</span></div><p class="stm-note" style="padding:12px;">열린 프로젝트 없음</p></div>`;
const s = st(ctx),
rows = ctx.dbfData.costn_rows || [];
const ini = ctx.dbfData.start_ini || {};
const slots = U.slotNames(ini);
const sort = s.sort || prefs().dflIndex || "C";
let list = U.filterRows(rows, {
kind: s.kind,
kw: s.kw,
mode: s.mode,
sort,
memos: ctx.dbfData.costn_memos || {},
});
const UA = global.STMATE_UI_CODE_AMT;
if (UA && s.pp) list = list.filter((r) => UA.ppMatch(r, s.pp)); // 속성 추려보기(rg_FLT_*_PP)
if (s.nm) list = list.filter((r) => String(r.NAME || "").trim() === s.nm); // 명칭 추려보기
if (UA && s.called) {
const called = UA.calledCodes(ctx.dbfData); // 불러간 목록(wM_Box_H)
list = list.filter((r) => called.has(U.codeOf(r)));
}
const cnt = U.counts(rows);
const n2 = prefs().useName2;
const nm = (r, k) => (n2 && String(r[`${k}2`] || "").trim() ? r[`${k}2`] : r[k]) || "";
const slotName = (n) => {
const x = slots.list.find((q) => q.slot === n);
return x ? x.short || x.name : "";
};
const { fmt } = api;
const body = list
.map((r, i) => {
const c = U.codeOf(r),
base = U.isBase(c),
cand = U.candidates(r),
pg = U.pages(r),
ad = U.adoptedSlot(r);
const priceCell = (k) =>
base && k[0] === c[0]
? `<input class="cell-input num dan-price" data-code="${c}" value="${U.num(r[k])}" />`
: `${U.num(r[k]) ? fmt(r[k]) : ""}`;
return `<tr class="${s.code === c ? "boq-sel" : ""}" data-code="${c}">
<td class="center rowhead dan-pick" data-code="${c}">${i + 1}</td><td class="center"><strong>${c}</strong></td>
<td>${esc(nm(r, "NAME"))}</td><td>${esc(nm(r, "SIZE"))}</td><td class="center">${esc(r.UNIT)}</td>
<td class="num">${priceCell("MAMT1")}</td><td class="num">${priceCell("LAMT1")}</td><td class="num">${priceCell("SAMT1")}</td>
<td class="num"><strong>${fmt(U.appliedPrice(r))}</strong></td>
${slots.list
.map(({ slot: n }) => {
const v = cand[n - 1];
const on = ad === n ? "background:#fde68a;font-weight:700;" : "";
const click =
base && v > 0
? ` class="num dan-adopt" data-code="${c}" data-slot="${n}" style="cursor:pointer;${on}" title="이 단가 채택"`
: ` class="num" style="${on}"`;
return `<td${click}>${v ? fmt(v) : ""}${pg[n - 1] ? `<br/><small>p.${esc(pg[n - 1])}</small>` : ""}</td>`;
})
.join("")}
<td class="center">${ad ? esc(slotName(ad)) : base && U.isUserEdited(r) ? "직접" : ""}</td>
<td class="center">${/^[MLSW]/.test(c) ? `<input type="checkbox" class="dan-prot" data-code="${c}" ${U.isProtected(r) ? "checked" : ""} title="보호(UC) — 일괄 손질에서 빠짐" />` : ""}</td>
<td class="center">${esc(r.BQCODE || "")}</td><td class="center">${esc(r.EQCODE || "")}</td>
<td class="center">${/^[BDX]/.test(c) ? `<button class="win-btn dan-ilwi" data-code="${c}">대가표</button>` : ""}</td>
</tr>`;
})
.join("");
return `<div class="win-form" id="dan-form">
<div class="win-form-header"><span>단가목록표 [wM_JabPum · COSTN.DBF] — ${rows.length}건 중 ${list.length}건</span><span>${esc(slots.applied)} = 재료비 + 노무비 + 경비</span></div>
<div class="win-subbar">
<label>코드종류(cBoxCode)</label><select id="dan-kind" class="win-select">${U.KINDS.map(([k, t]) => `<option value="${k}" ${s.kind === k ? "selected" : ""}>${t} (${cnt[k]})</option>`).join("")}</select>
<select id="dan-mode" class="win-select"><option value="name">명칭,규격 으로검색</option><option value="memo" ${s.mode === "memo" ? "selected" : ""}>산근내용 으로검색</option></select>
<input id="dan-kw" class="win-input" value="${esc(s.kw)}" placeholder="찾을 글자" style="width:150px;" />
<select id="dan-sort" class="win-select">${[
["C", "코드번호순"],
["N", "명 칭 순"],
["I", "분류번호순"],
["E", "시설코드순"],
]
.map(([k, t]) => `<option value="${k}" ${sort === k ? "selected" : ""}>${t}</option>`)
.join("")}</select>
<button class="win-btn" id="dan-boxa">가격지 비교·채택 (wM_Boxa)</button>
<button class="win-btn" id="dan-edthal">기준단가 수정 (wM_Edt_Hal)</button>
<button class="win-btn" id="dan-use" ${s.code ? "" : "disabled"}>사용처 찾기${s.code ? ` (${esc(s.code)})` : ""}</button>
<span class="stm-note">${esc(ctx.uiState.danMsg || "")}</span>
</div>
${toolBar(ctx, s)}
<div class="split-vert"><div class="pane-box" style="flex:1;"><div class="grid-scroll" id="dan-grid"><table class="dbgrid"><thead><tr>
<th>No</th><th>코드</th><th>명칭</th><th>규격</th><th>단위</th><th>재료비</th><th>노무비</th><th>경비</th><th>${esc(slots.applied)}</th>
${slots.list.map((x) => `<th title="${esc(x.name)}">${esc(x.name)}</th>`).join("")}<th>채택</th><th title="보호표시(√) — 일괄 손질 제외">보호</th><th>분류번호</th><th>시설코드</th><th></th>
</tr></thead><tbody>${body}</tbody></table></div></div></div>
</div>`;
}
// 코드 관리·단가 손질 단추줄 — scr_ui_code.js · scr_ui_dan2.js · 추려보기(속성·명칭·불러간 목록)
function toolBar(ctx, s) {
const UA = global.STMATE_UI_CODE_AMT;
if (!UA || !global.STMATE_UI_CODE_SCR) return "";
const pp = s.kind === "B" ? UA.PP_B : s.kind === "M" ? UA.PP_M : [];
const names = UA.nameList(ctx.dbfData.costn_rows, s.kind === "ALL" ? "" : s.kind, "NAME");
const b = (id, t, on) =>
`<button class="win-btn" id="${id}" ${on === false ? "disabled" : ""}>${t}</button>`;
return `<div class="win-subbar">${b("dan-new", "신규등록(A)")}${b("dan-del", "현재코드 삭제", Boolean(s.code))}${b("dan-chg", "코드번호 변경", Boolean(s.code))}${b("dan-rp", "현재코드 항목교체(내역포함)", Boolean(s.code))}${b("dan-jd", "호환규정코드 검색")}${b("dan-hal", "적용단가 일괄할증")}${b("dan-chgamt", "단가 일괄변경")}${b("dan-xr", "손료계수(F6)")}${b("dan-check", "성분 검사(TAMT=재+노+경)")}
${pp.length ? `<select id="dan-pp" class="win-select"><option value="">속성: 모 두 보 임</option>${pp.map((x) => `<option ${s.pp === x ? "selected" : ""}>${esc(x)}</option>`).join("")}</select>` : ""}
<select id="dan-nm" class="win-select" style="max-width:180px;"><option value="">명칭 추려보기: 모두보기</option>${names.map(([n, k]) => `<option value="${esc(n)}" ${s.nm === n ? "selected" : ""}>${esc(n)} (${k})</option>`).join("")}</select>
<label><input type="checkbox" id="dan-called" ${s.called ? "checked" : ""} /> 불러간 목록만</label>
<label title="Chk_Auto_Hal — 보호를 풀면 마지막 일괄할증 설정을 그 행에 다시 걺"><input type="checkbox" id="dan-autohal" ${s.autoHal === false ? "" : "checked"} /> 보호해제시 적용단가 자동할증</label></div>`;
}
function boxa(ctx, api) {
const slots = U.slotNames(ctx.dbfData.start_ini);
const s = st(ctx);
api.openModal(
"단가목록표 가격지 비교 (wM_Boxa)",
`<div class="stm-form"><div class="stm-row"><label>채택(RG_MComp)</label><select id="bx-how" class="win-select"><option value="none">변동없음</option>${slots.list.map((x) => `<option value="${x.slot}">${x.slot} 단가 — ${esc(x.name)}</option>`).join("")}<option value="min">최소단가</option></select></div>
<div class="stm-row"><label>대상</label><select id="bx-scope" class="win-select"><option value="M">재료비 전체</option><option value="MLS">기초단가 전체(재·노·경)</option>${s.code ? `<option value="one">선택 코드 ${esc(s.code)}</option>` : ""}</select></div>
<div class="stm-row"><label><input type="checkbox" id="bx-skip0" checked /> 단가가 0원인 것은 적용안함</label><label><input type="checkbox" id="bx-keep" checked /> 직접 수정한 단가 유지</label></div>
<div class="stm-row"><button class="win-btn primary" id="bx-ok">적용</button></div></div>`,
);
$("bx-ok").onclick = () => {
const how = $("bx-how").value,
scope = $("bx-scope").value;
const rows =
scope === "one"
? ctx.dbfData.costn_rows.filter((r) => U.codeOf(r) === s.code)
: ctx.dbfData.costn_rows;
const ch = U.adoptAll(rows, how, {
kinds: scope === "one" ? "MLS" : scope,
skipZero: $("bx-skip0").checked,
keepUser: scope !== "one" && $("bx-keep").checked,
});
ch.forEach((c) => syncRes(ctx, c.code, c.to, `slot${c.slot}`));
const kept = rows.filter(U.isProtected).length;
ctx.uiState.danMsg = `${ch.length}건 채택 바꿈${kept ? ` · 보호 ${kept}건 그대로` : ""}`;
api.closeModal();
api.cascadeAndRender();
};
}
function edtHal(ctx, api) {
const s = st(ctx);
api.openModal(
"기준단가 수정 (wM_Edt_Hal)",
`<div class="stm-form"><div class="stm-row"><label>적용단가 = 기준단가 ×</label><input id="eh-pct" type="number" class="win-input" value="100" style="width:80px;" /> %</div>
<div class="stm-row"><label>소수</label><select id="eh-dot" class="win-select">${[0, 1, 2].map((d) => `<option value="${d}">${d} 자리 미만</option>`).join("")}<option value="-1">1원 단위 미만(10원)</option></select><select id="eh-up" class="win-select"><option value="trunc">절사</option><option value="round">반올림</option></select></div>
<div class="stm-row"><label>대상</label><select id="eh-kind" class="win-select"><option value="M">재료비</option><option value="L">노무비</option><option value="S">경비</option><option value="MLS">기초단가 전체</option>${s.code ? `<option value="one">선택 코드 ${esc(s.code)}</option>` : ""}</select></div>
<p class="stm-note">※ 단【 적용단가 】가 "0" 원인것은 수정안됨 · 기준단가 = 채택 물가지 단가(채택 없으면 지금 적용단가)</p>
<p class="stm-note">※ 보호표시(√)된 단가도 수정안됨</p>
<div class="stm-row"><button class="win-btn primary" id="eh-ok">수정</button></div></div>`,
);
$("eh-ok").onclick = () => {
const kind = $("eh-kind").value;
const ch = U.edtHal(ctx.dbfData.costn_rows, {
kinds: kind === "one" ? "MLS" : kind,
codes: kind === "one" ? [s.code] : null,
pct: $("eh-pct").value,
digits: Number($("eh-dot").value),
mode: $("eh-up").value,
});
ch.forEach((c) => syncRes(ctx, c.code, c.to, "manual"));
const kept = ctx.dbfData.costn_rows.filter(U.isProtected).length;
ctx.uiState.danMsg = `기준단가 수정 ${ch.length}건${kept ? ` · 보호 ${kept}건 제외` : ""}`;
api.closeModal();
api.cascadeAndRender();
};
}
// 성분 검사(10 §3) — COSTN · 지금 내역 표 · 어긋난 칸 목록
function checkAll(ctx, api) {
const CK = global.STMATE_UI_CODE_CHECK;
if (!CK) return;
const bad = CK.checkComponents(ctx.dbfData.costn_rows).concat(
CK.checkBoq(ctx.dbfData.boq_rows),
);
const { fmt } = api;
api.openModal(
`성분 검사 — 어긋남 ${bad.length}건`,
`<div class="stm-form"><p class="stm-note">TAMT = 재료비 + 노무비 + 경비 · L/M/S 코드는 자기 비목만 · 차수 칸 1~4 · 내역은 단가·금액 칸</p>
${
bad.length
? `<div class="grid-scroll" style="max-height:320px;"><table class="dbgrid"><thead><tr><th>자리</th><th>코드</th><th>차수</th><th>내용</th><th>계</th><th>재</th><th>노</th><th>경</th></tr></thead><tbody>${bad
.map(
(x) =>
`<tr><td>${esc(x.where)}</td><td>${esc(x.code)}</td><td class="center">${x.slot}</td><td>${esc(x.msg)}</td>${["T", "M", "L", "S"].map((k) => `<td class="num">${fmt(x.v[k])}</td>`).join("")}</tr>`,
)
.join("")}</tbody></table></div>`
: "<p>어긋난 칸 없음</p>"
}</div>`,
);
}
function findUse(ctx, api) {
const s = st(ctx),
f = U.findUse(s.code, ctx.dbfData);
const li = (a) => (a.length ? a.join("") : "<li>(없음)</li>");
api.openModal(
`사용처 찾기 — ${s.code}`,
`<div class="stm-form"><strong>내역서 ${f.boq.length}곳</strong><ul>${li(f.boq.map((b) => `<li>${esc(b.table)} · ${esc(b.itnum)} ${esc(b.name)} · 수량 ${b.qty}</li>`))}</ul>
<strong>일위대가·중기 구성표 ${f.comp.length}곳</strong><ul>${li(f.comp.map((c) => `<li>${esc(c.parent)} · 수량 ${c.qty}</li>`))}</ul>
<strong>단가산출근거 식 ${f.memo.length}곳</strong><ul>${li(f.memo.map((m) => `<li>${esc(m)}</li>`))}</ul></div>`,
);
}
function bind(ctx, api) {
if (!ctx.dbfData) return;
const s = st(ctx),
rr = () => api.renderWorkspace();
$("dan-kind").onchange = (e) => ((s.kind = e.target.value), rr());
$("dan-mode").onchange = (e) => ((s.mode = e.target.value), rr());
$("dan-kw").onchange = (e) => ((s.kw = e.target.value), rr());
$("dan-sort").onchange = (e) => ((s.sort = e.target.value), rr());
$("dan-boxa").onclick = () => boxa(ctx, api);
$("dan-edthal").onclick = () => edtHal(ctx, api);
$("dan-use").onclick = () => findUse(ctx, api);
const C = global.STMATE_UI_CODE_SCR,
T2 = global.STMATE_UI_DAN2;
const on = (id, fn) => $(id) && ($(id).onclick = fn);
if (C) {
on("dan-new", () => C.copyNew(ctx, api, s.code));
on("dan-del", () => C.delOne(ctx, api, s.code));
on("dan-chg", () => C.chgOne(ctx, api, s.code, false));
on("dan-rp", () => C.chgOne(ctx, api, s.code, true));
}
if (T2) {
on("dan-jd", () => T2.openJD(ctx, api));
on("dan-hal", () => T2.openHal(ctx, api));
on("dan-chgamt", () => T2.openChgAmt(ctx, api));
on("dan-xr", () => T2.openXRate(ctx, api, s.code));
}
on("dan-check", () => checkAll(ctx, api));
if ($("dan-pp")) $("dan-pp").onchange = (e) => ((s.pp = e.target.value), rr());
if ($("dan-nm")) $("dan-nm").onchange = (e) => ((s.nm = e.target.value), rr());
if ($("dan-called")) $("dan-called").onchange = (e) => ((s.called = e.target.checked), rr());
if ($("dan-autohal")) $("dan-autohal").onchange = (e) => (s.autoHal = e.target.checked);
document.querySelectorAll("#dan-form .dan-prot").forEach(
(el) =>
(el.onchange = () => {
const c = el.getAttribute("data-code");
const r = ctx.dbfData.costn_rows.find((x) => U.codeOf(x) === c);
const hal = s.autoHal !== false && ctx.uiState.danHal;
const res = U.setProtect(r, el.checked, { autoHal: hal || null });
if (res && res.hal) syncRes(ctx, c, res.hal.to, "manual");
ctx.uiState.danMsg = `${c} 보호${el.checked ? "" : " 해제"}${res && res.hal ? ` · 자동할증 ${res.hal.to}` : ""}`;
api.cascadeAndRender();
}),
);
const grid = $("dan-grid");
if (s.scroll) grid.scrollTop = s.scroll;
grid.onscroll = () => (s.scroll = grid.scrollTop);
document
.querySelectorAll("#dan-form .dan-pick")
.forEach((td) => (td.onclick = () => ((s.code = td.getAttribute("data-code")), rr())));
document.querySelectorAll("#dan-form .dan-price").forEach(
(inp) =>
(inp.onchange = () => {
const c = inp.getAttribute("data-code");
const r = ctx.dbfData.costn_rows.find((x) => U.codeOf(x) === c);
const v = U.num(inp.value);
U.writeBase(r, v);
r.PG_1 = "0";
syncRes(ctx, c, v, "manual");
ctx.uiState.danMsg = `${c} 적용단가 ${v} 직접 수정`;
api.cascadeAndRender();
}),
);
document.querySelectorAll("#dan-form .dan-adopt").forEach(
(td) =>
(td.onclick = () => {
const c = td.getAttribute("data-code");
const r = ctx.dbfData.costn_rows.find((x) => U.codeOf(x) === c);
const ch = U.adopt(r, td.getAttribute("data-slot"), { skipZero: true });
if (!ch) return;
syncRes(ctx, c, ch.to, `slot${ch.slot}`);
ctx.uiState.danMsg = `${c} ${ch.slot} 단가 채택`;
api.cascadeAndRender();
}),
);
document
.querySelectorAll("#dan-form .dan-ilwi")
.forEach(
(b) =>
(b.onclick = () => (
(ctx.uiState.selectedIlwiCode = b.getAttribute("data-code")),
(ctx.uiState.activeScreen = "wM_Edit_iLWi"),
rr()
)),
);
}
P.screens.wM_JabPum = { render, bind };
})(window);