feat(est): move estimate rows safely
This commit is contained in:
@@ -1,6 +1,28 @@
|
||||
from test_est_rea_parity import run
|
||||
|
||||
|
||||
def test_estimate_move_preserves_row_identity_and_blocks_ambiguous_or_total_rows():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const rows=[{PkID:1,Code:'A'},{PkID:2,Code:'B'},{PkID:3,Code:'C'},{LinkIndex:54}];
|
||||
const moved=EstBusiness.moveEstimateRows(rows,new Set([1,2]),-1);
|
||||
const order=rows.map(row=>row.PkID||0);
|
||||
const restored=EstBusiness.moveEstimateRows(rows,new Set(moved),1);
|
||||
let blocked=0;
|
||||
for (const [data,selection,direction] of [
|
||||
[[{Code:'A'},{Code:'A'}],[0],1],
|
||||
[[{PkID:1},{LinkIndex:54}],[0],1],
|
||||
[[{PkID:1},{PkID:2},{PkID:3}],[0,2],1],
|
||||
[[{PkID:1},{Name:'unknown'}],[0],1]]) {
|
||||
try{EstBusiness.moveEstimateRows(data,new Set(selection),direction)}catch{blocked++}
|
||||
}
|
||||
return {moved,order,restored,blocked};
|
||||
})()""",
|
||||
)
|
||||
assert result == {"moved": [0, 1], "order": [2, 3, 1, 0], "restored": [1, 2], "blocked": 4}
|
||||
|
||||
|
||||
def test_duplicate_codes_match_distinct_original_rows_and_track_deletions():
|
||||
result = run(
|
||||
"math",
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
><button data-action="add-row">행 추가</button><button data-action="copy-row">복사</button
|
||||
><button data-action="paste-row">붙여넣기</button
|
||||
><button data-action="delete-row">삭제</button
|
||||
><button id="move-estimate-up" data-action="move-estimate-up" hidden>행 ↑</button
|
||||
><button id="move-estimate-down" data-action="move-estimate-down" hidden>행 ↓</button
|
||||
><input
|
||||
id="search"
|
||||
type="search"
|
||||
|
||||
@@ -45,7 +45,21 @@ const EstBusinessActions = (() => {
|
||||
const { state } = EstApp,
|
||||
project = state.project,
|
||||
round = currentRound();
|
||||
if (name === "unlock-round") {
|
||||
if (name === "move-estimate-up" || name === "move-estimate-down") {
|
||||
if (state.view !== "EstMainDat") throw Error("내역서에서만 행을 이동할 수 있습니다.");
|
||||
const direction = name === "move-estimate-up" ? -1 : 1;
|
||||
const indices = EstBusiness.moveEstimateRows(
|
||||
structuredClone(project.tables.EstMainDat),
|
||||
state.selection,
|
||||
direction,
|
||||
);
|
||||
if (indices.every((index) => state.selection.has(index))) return;
|
||||
EstApp.change(() => {
|
||||
state.selection = new Set(
|
||||
EstBusiness.moveEstimateRows(project.tables.EstMainDat, state.selection, direction),
|
||||
);
|
||||
});
|
||||
} else if (name === "unlock-round") {
|
||||
if (confirm("이 회차와 이후 회차의 확정을 해제하시겠습니까? 입력값은 유지됩니다."))
|
||||
EstApp.change(() => (state.project = EstProgress.unlock(project, round.id)), false);
|
||||
} else if (name === "delete-round") {
|
||||
|
||||
@@ -4,6 +4,35 @@ const EstBusiness = (() => {
|
||||
const copy = (value) => structuredClone(value);
|
||||
const rowKey = (row, index) => String(row.PkID ?? (row.Code || `row:${index}`));
|
||||
const special = (row) => [57, 58, 62, 67].includes(num(row.LinkIndex));
|
||||
function moveEstimateRows(rows, selection, direction) {
|
||||
const indices = [...selection].sort((a, b) => a - b);
|
||||
if (!indices.length) throw Error("이동할 내역 행을 선택하세요.");
|
||||
if (
|
||||
![1, -1].includes(direction) ||
|
||||
indices.some((value, index) => value !== indices[0] + index)
|
||||
)
|
||||
throw Error("연속된 내역 행을 선택하세요.");
|
||||
const first = indices[0],
|
||||
last = indices.at(-1),
|
||||
neighbor = direction < 0 ? first - 1 : last + 1;
|
||||
if (neighbor < 0 || neighbor >= rows.length) return indices;
|
||||
const affected = rows.slice(Math.min(first, neighbor), Math.max(last, neighbor) + 1);
|
||||
if (affected.some((row) => [54, 55, 58, 59].includes(num(row.LinkIndex))))
|
||||
throw Error("합계 행은 이동하거나 건너뛸 수 없습니다.");
|
||||
const counts = new Map();
|
||||
rows.forEach((row, index) => {
|
||||
const key = rowKey(row, index);
|
||||
counts.set(key, (counts.get(key) || 0) + 1);
|
||||
});
|
||||
for (let index = Math.min(first, neighbor); index <= Math.max(last, neighbor); index++) {
|
||||
const row = rows[index];
|
||||
if ((row.PkID == null && !row.Code) || counts.get(rowKey(row, index)) !== 1)
|
||||
throw Error("행 식별자가 없거나 중복되어 연결을 보존할 수 없습니다.");
|
||||
}
|
||||
const group = rows.splice(first, indices.length);
|
||||
rows.splice(first + direction, 0, ...group);
|
||||
return indices.map((index) => index + direction);
|
||||
}
|
||||
function baseline(project) {
|
||||
const working = { ...project };
|
||||
delete working.baseline;
|
||||
@@ -355,6 +384,7 @@ const EstBusiness = (() => {
|
||||
annual,
|
||||
newAnnualName,
|
||||
rowKey,
|
||||
moveEstimateRows,
|
||||
importContract,
|
||||
importBid,
|
||||
annualCopy,
|
||||
|
||||
@@ -97,6 +97,8 @@ const EstViews = (() => {
|
||||
byId("search").value = state.query;
|
||||
byId("standard-tools").hidden = !project.tables[view];
|
||||
byId("add-group").hidden = !grouped;
|
||||
byId("move-estimate-up").hidden = view !== "EstMainDat";
|
||||
byId("move-estimate-down").hidden = view !== "EstMainDat";
|
||||
if (project.tables[view]) {
|
||||
const all = EstApp.visibleRows(),
|
||||
start = state.page * 100,
|
||||
|
||||
Reference in New Issue
Block a user