Fix organization member editing and drag sync

This commit is contained in:
hyunho
2026-04-02 10:38:47 +09:00
parent a4480c3435
commit 8125193378
6 changed files with 155 additions and 66 deletions

View File

@@ -8,8 +8,8 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;600;700;900&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;600;700;900&display=swap" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/legacy/static/common.css?v=20260331-01" /> <link rel="stylesheet" href="/legacy/static/common.css?v=20260402-02" />
<link rel="stylesheet" href="/legacy/static/organization.css?v=20260331-01" /> <link rel="stylesheet" href="/legacy/static/organization.css?v=20260402-02" />
</head> </head>
<body> <body>
<input type="file" id="upload-excel" class="hidden" accept=".xlsx, .csv" /> <input type="file" id="upload-excel" class="hidden" accept=".xlsx, .csv" />
@@ -60,6 +60,6 @@
</div> </div>
</div> </div>
<script src="/legacy/static/organization.js?v=20260331-01"></script> <script src="/legacy/static/organization.js?v=20260402-02"></script>
</body> </body>
</html> </html>

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Callable from typing import Callable
from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi import Body, FastAPI, File, HTTPException, UploadFile
def register_member_routes( def register_member_routes(
@@ -19,6 +19,7 @@ def register_member_routes(
serialize_member_payload: Callable[[object, int], tuple[object, ...]], serialize_member_payload: Callable[[object, int], tuple[object, ...]],
sync_auth_users_from_members: Callable[[object], None], sync_auth_users_from_members: Callable[[object], None],
create_history_revision: Callable[[object, str, str], int], create_history_revision: Callable[[object, str, str], int],
fetch_history_revision_created_at: Callable[[object, int], datetime],
sync_member_versions: Callable[[object, list[int], str, int], None], sync_member_versions: Callable[[object, list[int], str, int], None],
sync_seat_assignment_versions: Callable[[object, list[int], str, int], None], sync_seat_assignment_versions: Callable[[object, list[int], str, int], None],
replace_members: Callable[[list[object]], list[dict[str, object]]], replace_members: Callable[[list[object]], list[dict[str, object]]],
@@ -46,7 +47,8 @@ def register_member_routes(
return {"items": build_member_compare_items(from_items, to_items)} return {"items": build_member_compare_items(from_items, to_items)}
@app.post("/api/members") @app.post("/api/members")
def create_member(payload: member_payload_cls) -> dict[str, object]: def create_member(payload: dict = Body(...)) -> dict[str, object]:
payload = member_payload_cls.model_validate(payload)
with get_conn() as conn: with get_conn() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute("SELECT COALESCE(MAX(sort_order), -1) + 1 AS next_order FROM members") cur.execute("SELECT COALESCE(MAX(sort_order), -1) + 1 AS next_order FROM members")
@@ -67,16 +69,19 @@ def register_member_routes(
member = cur.fetchone() member = cur.fetchone()
sync_auth_users_from_members(cur) sync_auth_users_from_members(cur)
revision_no = create_history_revision(cur, "member-create", f"Member created id={int(member['id'])}") revision_no = create_history_revision(cur, "member-create", f"Member created id={int(member['id'])}")
sync_member_versions(cur, [int(member["id"])], "member-create", revision_no) revision_created_at = fetch_history_revision_created_at(cur, revision_no)
sync_member_versions(cur, [int(member["id"])], "member-create", revision_no, revision_created_at)
conn.commit() conn.commit()
return {"item": member} return {"item": member}
@app.put("/api/members/bulk-sync") @app.put("/api/members/bulk-sync")
def bulk_sync_members(payload: member_bulk_payload_cls) -> dict[str, list[dict[str, object]]]: def bulk_sync_members(payload: dict = Body(...)) -> dict[str, list[dict[str, object]]]:
payload = member_bulk_payload_cls.model_validate(payload)
return {"items": replace_members(payload.items)} return {"items": replace_members(payload.items)}
@app.put("/api/members/{member_id}") @app.put("/api/members/{member_id}")
def update_member(member_id: int, payload: member_payload_cls) -> dict[str, object]: def update_member(member_id: int, payload: dict = Body(...)) -> dict[str, object]:
payload = member_payload_cls.model_validate(payload)
with get_conn() as conn: with get_conn() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
cur.execute( cur.execute(
@@ -112,8 +117,9 @@ def register_member_routes(
raise HTTPException(status_code=404, detail="Member not found.") raise HTTPException(status_code=404, detail="Member not found.")
sync_auth_users_from_members(cur) sync_auth_users_from_members(cur)
revision_no = create_history_revision(cur, "member-update", f"Member updated id={member_id}") revision_no = create_history_revision(cur, "member-update", f"Member updated id={member_id}")
sync_member_versions(cur, [member_id], "member-update", revision_no) revision_created_at = fetch_history_revision_created_at(cur, revision_no)
sync_seat_assignment_versions(cur, [member_id], "member-update", revision_no) sync_member_versions(cur, [member_id], "member-update", revision_no, revision_created_at)
sync_seat_assignment_versions(cur, [member_id], "member-update", revision_no, revision_created_at)
conn.commit() conn.commit()
return {"item": member} return {"item": member}
@@ -126,8 +132,9 @@ def register_member_routes(
if deleted: if deleted:
sync_auth_users_from_members(cur) sync_auth_users_from_members(cur)
revision_no = create_history_revision(cur, "member-delete", f"Member deleted id={member_id}") revision_no = create_history_revision(cur, "member-delete", f"Member deleted id={member_id}")
sync_member_versions(cur, [member_id], "member-delete", revision_no) revision_created_at = fetch_history_revision_created_at(cur, revision_no)
sync_seat_assignment_versions(cur, [member_id], "member-delete", revision_no) sync_member_versions(cur, [member_id], "member-delete", revision_no, revision_created_at)
sync_seat_assignment_versions(cur, [member_id], "member-delete", revision_no, revision_created_at)
conn.commit() conn.commit()
if not deleted: if not deleted:
raise HTTPException(status_code=404, detail="Member not found.") raise HTTPException(status_code=404, detail="Member not found.")

View File

@@ -16,7 +16,7 @@
<link rel="stylesheet" href="/design-patterns.css?v=20260401-01"> <link rel="stylesheet" href="/design-patterns.css?v=20260401-01">
<link rel="stylesheet" href="/legacy/static/common.css"> <link rel="stylesheet" href="/legacy/static/common.css">
<!-- Keep login and common hub defaults aligned with 8080. --> <!-- Keep login and common hub defaults aligned with 8080. -->
<link rel="stylesheet" href="/styles.css?v=20260330-01"> <link rel="stylesheet" href="/styles.css?v=20260402-01">
<!-- 8081-only hub overrides must not restyle the login screen. --> <!-- 8081-only hub overrides must not restyle the login screen. -->
<link rel="stylesheet" href="/styles-8081-design.css?v=20260401-01"> <link rel="stylesheet" href="/styles-8081-design.css?v=20260401-01">
</head> </head>
@@ -105,7 +105,7 @@
<section id="organization-stage" class="main-stage"> <section id="organization-stage" class="main-stage">
<div class="stage-frame"> <div class="stage-frame">
<!-- Legacy organization keeps its own CSS/JS responsibility under /legacy/static. --> <!-- Legacy organization keeps its own CSS/JS responsibility under /legacy/static. -->
<iframe id="organization-frame" src="/legacy/organization?v=20260330-02" data-src="/legacy/organization?v=20260330-02" title="조직도 메인 화면"></iframe> <iframe id="organization-frame" src="/legacy/organization?v=20260402-02" data-src="/legacy/organization?v=20260402-02" title="조직도 메인 화면"></iframe>
</div> </div>
</section> </section>
<section id="project-stage" class="main-stage" hidden> <section id="project-stage" class="main-stage" hidden>

View File

@@ -344,6 +344,12 @@ body {
padding-right: 8px; padding-right: 8px;
} }
.header-date-field select option {
background: var(--color-surface);
color: var(--color-text);
font-weight: 700;
}
.header-date-sep { .header-date-sep {
color: var(--color-text-muted); color: var(--color-text-muted);
font-size: 12px; font-size: 12px;

View File

@@ -338,11 +338,16 @@ body {
.modal-content.wide #modal-fields { .modal-content.wide #modal-fields {
min-height: 0; min-height: 0;
overflow-y: auto;
padding-right: 4px;
} }
.modal-content.wide #modal-footer-area { .modal-content.wide #modal-footer-area {
margin-top: 0 !important; margin-top: 0 !important;
flex-shrink: 0; flex-shrink: 0;
position: relative;
z-index: 2;
background: var(--color-surface);
} }
.member-photo-field { .member-photo-field {
@@ -761,6 +766,7 @@ body {
height: 300px; height: 300px;
border: 0; border: 0;
background: var(--color-surface); background: var(--color-surface);
pointer-events: none;
} }
.seat-preview-card.is-assigned .seat-preview-frame { .seat-preview-card.is-assigned .seat-preview-frame {
@@ -1033,7 +1039,8 @@ body {
.modal-footer-actions { .modal-footer-actions {
display: flex; display: flex;
gap: 10px; gap: 10px;
width: 100%; width: auto;
flex: 1 1 auto;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
} }
@@ -1045,10 +1052,12 @@ body {
padding: 14px 18px; padding: 14px 18px;
transition: all 0.2s ease; transition: all 0.2s ease;
border: 1px solid transparent; border: 1px solid transparent;
white-space: nowrap;
writing-mode: horizontal-tb;
} }
.modal-btn-cancel { .modal-btn-cancel {
flex: 1 1 0; flex: 0 1 140px;
background: var(--color-surface-strong); background: var(--color-surface-strong);
color: var(--color-text-soft); color: var(--color-text-soft);
border-color: var(--color-border); border-color: var(--color-border);
@@ -1059,7 +1068,7 @@ body {
} }
.modal-btn-save { .modal-btn-save {
flex: 1 1 0; flex: 0 1 140px;
background: var(--color-header); background: var(--color-header);
color: #fff; color: #fff;
box-shadow: 0 10px 22px rgba(47, 153, 115, 0.2); box-shadow: 0 10px 22px rgba(47, 153, 115, 0.2);
@@ -1070,6 +1079,8 @@ body {
} }
.modal-btn-delete { .modal-btn-delete {
flex: 0 0 132px;
min-width: 132px;
background: rgba(198, 71, 56, 0.12); background: rgba(198, 71, 56, 0.12);
color: #a33427; color: #a33427;
border-color: rgba(198, 71, 56, 0.2); border-color: rgba(198, 71, 56, 0.2);

View File

@@ -4,6 +4,7 @@ let selectedDept = '전체';
let editingMembers = []; let editingMembers = [];
let collapsedUnits = new Set(); let collapsedUnits = new Set();
let isListMode = false; let isListMode = false;
let isListDetailModal = false;
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.'; let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
let photoPreviewObjectUrl = null; let photoPreviewObjectUrl = null;
let seatMapLayoutCache = null; let seatMapLayoutCache = null;
@@ -485,6 +486,24 @@ function createNodeDOM(node, parentId) {
return nodeItem; return nodeItem;
} }
function applyMemberPlacementFromTarget(member, targetLevel, targetName, targetMember = null) {
const targetLevelIndex = levelOrder.indexOf(targetLevel);
if (targetLevelIndex === -1) {
return member;
}
for (let index = 0; index <= targetLevelIndex; index += 1) {
member[levelOrder[index]] = targetMember ? (targetMember[levelOrder[index]] || '') : (index === targetLevelIndex ? targetName : member[levelOrder[index]]);
}
if (!targetMember) {
member[targetLevel] = targetName;
}
for (let index = targetLevelIndex + 1; index < levelOrder.length; index += 1) {
member[levelOrder[index]] = '';
}
rebuildMemberPath(member);
return member;
}
function drawLines() { function drawLines() {
const container = document.getElementById('tree-root'); const container = document.getElementById('tree-root');
const svg = document.getElementById('svg-canvas'); const svg = document.getElementById('svg-canvas');
@@ -628,6 +647,11 @@ function render() {
deptBox.id = deptId; deptBox.id = deptId;
deptBox.className = 'dept-box'; deptBox.className = 'dept-box';
deptBox.setAttribute('data-level', '부서'); deptBox.setAttribute('data-level', '부서');
if (isAdmin) {
deptBox.ondragover = (event) => handleDragOver(event);
deptBox.ondragleave = (event) => handleDragLeave(event);
deptBox.ondrop = (event) => handleDrop(event, '부서', deptName);
}
deptBox.innerHTML = `<div class="dept-header ${isAdmin ? 'clickable-title' : ''} ${hasMembers ? 'has-members' : ''}" ${isAdmin ? `onclick="openOrgEditModal('부서', '${jsString(deptName)}')"` : ''}>${deptName} (${totalCount})</div>`; deptBox.innerHTML = `<div class="dept-header ${isAdmin ? 'clickable-title' : ''} ${hasMembers ? 'has-members' : ''}" ${isAdmin ? `onclick="openOrgEditModal('부서', '${jsString(deptName)}')"` : ''}>${deptName} (${totalCount})</div>`;
if (hasMembers) { if (hasMembers) {
@@ -827,6 +851,53 @@ function openAddModal(event) {
openModal(null); openModal(null);
} }
function renderListViewShell(defaultDate) {
const modal = document.getElementById('modal');
modal.querySelector('.modal-content').classList.add('wide');
document.getElementById('modal-title').innerText = '인원 명단';
const fieldsArea = document.getElementById('modal-fields');
fieldsArea.className = 'flex flex-col w-full overflow-hidden';
fieldsArea.style.maxHeight = '75vh';
fieldsArea.style.overflowY = 'hidden';
fieldsArea.innerHTML = `
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-group">
<button type="button" onclick="showCurrentListView()" class="list-mode-btn">현재 명단</button>
</div>
<div class="list-toolbar-divider" aria-hidden="true"></div>
<div class="list-toolbar-group list-date-group">
<input type="date" id="list-snapshot-date" value="${escapeHtml(defaultDate)}" class="list-date-input">
<button type="button" onclick="loadSnapshotListView()" class="list-mode-btn">기준일 조회</button>
</div>
<div class="list-toolbar-divider" aria-hidden="true"></div>
<div class="list-toolbar-group list-date-group">
<input type="date" id="list-compare-from" value="${escapeHtml(defaultDate)}" class="list-date-input">
<span class="list-date-separator">~</span>
<input type="date" id="list-compare-to" value="${escapeHtml(defaultDate)}" class="list-date-input">
<button type="button" onclick="loadCompareListView()" class="list-mode-btn">변경 비교</button>
</div>
</div>
<div class="list-toolbar-row">
<input type="text" id="list-search-input" placeholder="이름 또는 부서/팀 검색 (Enter 시 이동)" class="flex-1 bg-[#f6eddd] border-2 border-[#e0d0b4] p-3 rounded-xl text-sm outline-none font-bold text-[#1f2f25] focus:border-[#d68a3a] transition-all" onkeydown="if(event.key==='Enter') handleListSearch(this.value)">
<button type="button" onclick="handleListSearch(document.getElementById('list-search-input').value)" class="bg-[#214634] text-white px-5 rounded-xl font-bold text-sm">검색</button>
</div>
<div id="list-view-status" class="list-view-status"></div>
</div>
<div id="list-table-container" class="overflow-y-auto flex-1 border rounded-xl"></div>
`;
modal.style.display = 'flex';
}
function returnToListViewModal() {
if (!isListMode) {
return;
}
isListDetailModal = false;
renderListViewShell(listViewState.snapshotDate || getDefaultHistoryDate());
renderListViewModalContent();
}
function updateParentList() { function updateParentList() {
const type = document.getElementById('new-unit-type').value; const type = document.getElementById('new-unit-type').value;
const parentSelect = document.getElementById('new-unit-parent'); const parentSelect = document.getElementById('new-unit-parent');
@@ -1159,7 +1230,8 @@ function openModal(id) {
modal.querySelector('.modal-content').classList.add('wide'); modal.querySelector('.modal-content').classList.add('wide');
fieldsArea.className = 'flex flex-col w-full'; fieldsArea.className = 'flex flex-col w-full';
fieldsArea.style.maxHeight = 'none'; fieldsArea.style.maxHeight = 'none';
fieldsArea.style.overflowY = 'visible'; fieldsArea.style.overflowY = 'auto';
isListDetailModal = isListMode;
const sourceValues = isListMode ? editingMembers : members; const sourceValues = isListMode ? editingMembers : members;
let orgFields = '<div id="modal-sec-org" class="hidden grid grid-cols-2 gap-3 modal-form-grid">'; let orgFields = '<div id="modal-sec-org" class="hidden grid grid-cols-2 gap-3 modal-form-grid">';
@@ -1279,11 +1351,17 @@ function openModal(id) {
} }
function closeModal() { function closeModal() {
if (isListMode && isListDetailModal) {
returnToListViewModal();
return;
}
resetPhotoPreviewObjectUrl(); resetPhotoPreviewObjectUrl();
document.getElementById('modal').style.display = 'none'; document.getElementById('modal').style.display = 'none';
document.getElementById('modal-fields').className = 'grid grid-cols-2 gap-x-8 gap-y-5'; document.getElementById('modal-fields').className = 'grid grid-cols-2 gap-x-8 gap-y-5';
document.getElementById('modal-fields').style.maxHeight = 'none'; document.getElementById('modal-fields').style.maxHeight = 'none';
document.getElementById('modal-fields').style.overflowY = 'visible';
document.querySelector('.modal-content').classList.remove('wide'); document.querySelector('.modal-content').classList.remove('wide');
isListDetailModal = false;
isListMode = false; isListMode = false;
} }
@@ -1337,8 +1415,7 @@ async function saveMember() {
if (!id) { if (!id) {
targetList.push(member); targetList.push(member);
} }
renderListViewTable(); returnToListViewModal();
closeModal();
return; return;
} }
@@ -1367,11 +1444,16 @@ async function deleteMember(id) {
} }
if (isListMode) { if (isListMode) {
const shouldReturnToList = isListDetailModal;
const idx = editingMembers.findIndex((member) => member._id === id); const idx = editingMembers.findIndex((member) => member._id === id);
if (idx !== -1) { if (idx !== -1) {
editingMembers.splice(idx, 1); editingMembers.splice(idx, 1);
} }
if (shouldReturnToList) {
returnToListViewModal();
} else {
renderListViewTable(); renderListViewTable();
}
return; return;
} }
@@ -1396,44 +1478,11 @@ function openListViewModal(event) {
listViewState.compareToDate = defaultDate; listViewState.compareToDate = defaultDate;
listViewState.snapshotMembers = []; listViewState.snapshotMembers = [];
listViewState.compareItems = []; listViewState.compareItems = [];
const modal = document.getElementById('modal');
modal.querySelector('.modal-content').classList.add('wide');
document.getElementById('modal-title').innerText = '인원 명단';
const fieldsArea = document.getElementById('modal-fields');
fieldsArea.className = 'flex flex-col w-full overflow-hidden';
fieldsArea.style.maxHeight = '75vh';
isListMode = true; isListMode = true;
isListDetailModal = false;
editingMembers = cloneMembers(members); editingMembers = cloneMembers(members);
fieldsArea.innerHTML = ` renderListViewShell(defaultDate);
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-group">
<button type="button" onclick="showCurrentListView()" class="list-mode-btn">현재 명단</button>
</div>
<div class="list-toolbar-divider" aria-hidden="true"></div>
<div class="list-toolbar-group list-date-group">
<input type="date" id="list-snapshot-date" value="${escapeHtml(defaultDate)}" class="list-date-input">
<button type="button" onclick="loadSnapshotListView()" class="list-mode-btn">기준일 조회</button>
</div>
<div class="list-toolbar-divider" aria-hidden="true"></div>
<div class="list-toolbar-group list-date-group">
<input type="date" id="list-compare-from" value="${escapeHtml(defaultDate)}" class="list-date-input">
<span class="list-date-separator">~</span>
<input type="date" id="list-compare-to" value="${escapeHtml(defaultDate)}" class="list-date-input">
<button type="button" onclick="loadCompareListView()" class="list-mode-btn">변경 비교</button>
</div>
</div>
<div class="list-toolbar-row">
<input type="text" id="list-search-input" placeholder="이름 또는 부서/팀 검색 (Enter 시 이동)" class="flex-1 bg-[#f6eddd] border-2 border-[#e0d0b4] p-3 rounded-xl text-sm outline-none font-bold text-[#1f2f25] focus:border-[#d68a3a] transition-all" onkeydown="if(event.key==='Enter') handleListSearch(this.value)">
<button type="button" onclick="handleListSearch(document.getElementById('list-search-input').value)" class="bg-[#214634] text-white px-5 rounded-xl font-bold text-sm">검색</button>
</div>
<div id="list-view-status" class="list-view-status"></div>
</div>
<div id="list-table-container" class="overflow-y-auto flex-1 border rounded-xl"></div>
`;
renderListViewModalContent(); renderListViewModalContent();
modal.style.display = 'flex';
} }
async function applyListViewChanges() { async function applyListViewChanges() {
@@ -1697,12 +1746,29 @@ function toggleUnitCollapse(level, name) {
let draggedGroup = null; let draggedGroup = null;
function handleListGroupDragStart(event, level, name) { function handleListGroupDragStart(event, level, name) {
draggedIdx = null;
draggedGroup = { level, name }; draggedGroup = { level, name };
event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.effectAllowed = 'move';
} }
function handleListGroupDrop(event, targetLevel, targetName) { function handleListGroupDrop(event, targetLevel, targetName) {
event.preventDefault(); event.preventDefault();
if (draggedIdx !== null) {
const movingMember = editingMembers[draggedIdx];
if (!movingMember) {
return;
}
const moved = editingMembers.splice(draggedIdx, 1)[0];
applyMemberPlacementFromTarget(moved, targetLevel, targetName, null);
let targetIdx = editingMembers.findIndex((member) => member[targetLevel] === targetName);
if (targetIdx === -1) {
targetIdx = editingMembers.length;
}
editingMembers.splice(targetIdx, 0, moved);
draggedIdx = null;
renderListViewTable();
return;
}
if (!draggedGroup || (draggedGroup.level === targetLevel && draggedGroup.name === targetName)) { if (!draggedGroup || (draggedGroup.level === targetLevel && draggedGroup.name === targetName)) {
return; return;
} }
@@ -1743,6 +1809,7 @@ function handleListSearch(value) {
let draggedIdx = null; let draggedIdx = null;
function handleListDragStart(event, index) { function handleListDragStart(event, index) {
draggedGroup = null;
draggedIdx = index; draggedIdx = index;
event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.effectAllowed = 'move';
event.target.classList.add('dragging'); event.target.classList.add('dragging');
@@ -1753,7 +1820,11 @@ function handleListDrop(event, targetIdx) {
if (draggedIdx === null || draggedIdx === targetIdx) { if (draggedIdx === null || draggedIdx === targetIdx) {
return; return;
} }
const targetMember = editingMembers[targetIdx] || null;
const moved = editingMembers.splice(draggedIdx, 1)[0]; const moved = editingMembers.splice(draggedIdx, 1)[0];
if (targetMember) {
applyMemberPlacementFromTarget(moved, levelOrder[levelOrder.length - 1], targetMember[levelOrder[levelOrder.length - 1]] || '', targetMember);
}
editingMembers.splice(targetIdx, 0, moved); editingMembers.splice(targetIdx, 0, moved);
draggedIdx = null; draggedIdx = null;
renderListViewTable(); renderListViewTable();
@@ -1804,13 +1875,10 @@ async function handleDrop(event, targetLevel, targetName) {
} }
const nextMembers = cloneMembers(members); const nextMembers = cloneMembers(members);
const moved = nextMembers[memberIndex]; const moved = nextMembers[memberIndex];
for (let index = 0; index <= targetLevelIndex; index += 1) { if (targetLevelIndex === -1) {
moved[levelOrder[index]] = targetMember ? targetMember[levelOrder[index]] : targetName; return;
} }
for (let index = targetLevelIndex + 1; index < levelOrder.length; index += 1) { applyMemberPlacementFromTarget(moved, targetLevel, targetName, targetMember);
moved[levelOrder[index]] = '';
}
rebuildMemberPath(moved);
nextMembers.splice(memberIndex, 1); nextMembers.splice(memberIndex, 1);
nextMembers.push(moved); nextMembers.push(moved);
await syncMembers(nextMembers); await syncMembers(nextMembers);
@@ -1859,10 +1927,7 @@ async function handleDropMember(event, targetId) {
} }
const moved = nextMembers[movingIdx]; const moved = nextMembers[movingIdx];
const target = nextMembers[targetIdx]; const target = nextMembers[targetIdx];
levelOrder.forEach((level) => { applyMemberPlacementFromTarget(moved, levelOrder[levelOrder.length - 1], target[levelOrder[levelOrder.length - 1]] || '', target);
moved[level] = target[level];
});
rebuildMemberPath(moved);
nextMembers.splice(movingIdx, 1); nextMembers.splice(movingIdx, 1);
targetIdx = nextMembers.findIndex((member) => member._id === targetId); targetIdx = nextMembers.findIndex((member) => member._id === targetId);
nextMembers.splice(insertAfter ? targetIdx + 1 : targetIdx, 0, moved); nextMembers.splice(insertAfter ? targetIdx + 1 : targetIdx, 0, moved);