Fix organization member editing and drag sync
This commit is contained in:
@@ -4,6 +4,7 @@ let selectedDept = '전체';
|
||||
let editingMembers = [];
|
||||
let collapsedUnits = new Set();
|
||||
let isListMode = false;
|
||||
let isListDetailModal = false;
|
||||
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
|
||||
let photoPreviewObjectUrl = null;
|
||||
let seatMapLayoutCache = null;
|
||||
@@ -485,6 +486,24 @@ function createNodeDOM(node, parentId) {
|
||||
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() {
|
||||
const container = document.getElementById('tree-root');
|
||||
const svg = document.getElementById('svg-canvas');
|
||||
@@ -628,6 +647,11 @@ function render() {
|
||||
deptBox.id = deptId;
|
||||
deptBox.className = 'dept-box';
|
||||
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>`;
|
||||
|
||||
if (hasMembers) {
|
||||
@@ -827,6 +851,53 @@ function openAddModal(event) {
|
||||
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() {
|
||||
const type = document.getElementById('new-unit-type').value;
|
||||
const parentSelect = document.getElementById('new-unit-parent');
|
||||
@@ -1159,7 +1230,8 @@ function openModal(id) {
|
||||
modal.querySelector('.modal-content').classList.add('wide');
|
||||
fieldsArea.className = 'flex flex-col w-full';
|
||||
fieldsArea.style.maxHeight = 'none';
|
||||
fieldsArea.style.overflowY = 'visible';
|
||||
fieldsArea.style.overflowY = 'auto';
|
||||
isListDetailModal = isListMode;
|
||||
|
||||
const sourceValues = isListMode ? editingMembers : members;
|
||||
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() {
|
||||
if (isListMode && isListDetailModal) {
|
||||
returnToListViewModal();
|
||||
return;
|
||||
}
|
||||
resetPhotoPreviewObjectUrl();
|
||||
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').style.maxHeight = 'none';
|
||||
document.getElementById('modal-fields').style.overflowY = 'visible';
|
||||
document.querySelector('.modal-content').classList.remove('wide');
|
||||
isListDetailModal = false;
|
||||
isListMode = false;
|
||||
}
|
||||
|
||||
@@ -1337,8 +1415,7 @@ async function saveMember() {
|
||||
if (!id) {
|
||||
targetList.push(member);
|
||||
}
|
||||
renderListViewTable();
|
||||
closeModal();
|
||||
returnToListViewModal();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1367,11 +1444,16 @@ async function deleteMember(id) {
|
||||
}
|
||||
|
||||
if (isListMode) {
|
||||
const shouldReturnToList = isListDetailModal;
|
||||
const idx = editingMembers.findIndex((member) => member._id === id);
|
||||
if (idx !== -1) {
|
||||
editingMembers.splice(idx, 1);
|
||||
}
|
||||
renderListViewTable();
|
||||
if (shouldReturnToList) {
|
||||
returnToListViewModal();
|
||||
} else {
|
||||
renderListViewTable();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1396,44 +1478,11 @@ function openListViewModal(event) {
|
||||
listViewState.compareToDate = defaultDate;
|
||||
listViewState.snapshotMembers = [];
|
||||
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;
|
||||
isListDetailModal = false;
|
||||
editingMembers = cloneMembers(members);
|
||||
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>
|
||||
`;
|
||||
renderListViewShell(defaultDate);
|
||||
renderListViewModalContent();
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
async function applyListViewChanges() {
|
||||
@@ -1697,12 +1746,29 @@ function toggleUnitCollapse(level, name) {
|
||||
|
||||
let draggedGroup = null;
|
||||
function handleListGroupDragStart(event, level, name) {
|
||||
draggedIdx = null;
|
||||
draggedGroup = { level, name };
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
|
||||
function handleListGroupDrop(event, targetLevel, targetName) {
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
@@ -1743,6 +1809,7 @@ function handleListSearch(value) {
|
||||
|
||||
let draggedIdx = null;
|
||||
function handleListDragStart(event, index) {
|
||||
draggedGroup = null;
|
||||
draggedIdx = index;
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.target.classList.add('dragging');
|
||||
@@ -1753,7 +1820,11 @@ function handleListDrop(event, targetIdx) {
|
||||
if (draggedIdx === null || draggedIdx === targetIdx) {
|
||||
return;
|
||||
}
|
||||
const targetMember = editingMembers[targetIdx] || null;
|
||||
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);
|
||||
draggedIdx = null;
|
||||
renderListViewTable();
|
||||
@@ -1804,13 +1875,10 @@ async function handleDrop(event, targetLevel, targetName) {
|
||||
}
|
||||
const nextMembers = cloneMembers(members);
|
||||
const moved = nextMembers[memberIndex];
|
||||
for (let index = 0; index <= targetLevelIndex; index += 1) {
|
||||
moved[levelOrder[index]] = targetMember ? targetMember[levelOrder[index]] : targetName;
|
||||
if (targetLevelIndex === -1) {
|
||||
return;
|
||||
}
|
||||
for (let index = targetLevelIndex + 1; index < levelOrder.length; index += 1) {
|
||||
moved[levelOrder[index]] = '';
|
||||
}
|
||||
rebuildMemberPath(moved);
|
||||
applyMemberPlacementFromTarget(moved, targetLevel, targetName, targetMember);
|
||||
nextMembers.splice(memberIndex, 1);
|
||||
nextMembers.push(moved);
|
||||
await syncMembers(nextMembers);
|
||||
@@ -1859,10 +1927,7 @@ async function handleDropMember(event, targetId) {
|
||||
}
|
||||
const moved = nextMembers[movingIdx];
|
||||
const target = nextMembers[targetIdx];
|
||||
levelOrder.forEach((level) => {
|
||||
moved[level] = target[level];
|
||||
});
|
||||
rebuildMemberPath(moved);
|
||||
applyMemberPlacementFromTarget(moved, levelOrder[levelOrder.length - 1], target[levelOrder[levelOrder.length - 1]] || '', target);
|
||||
nextMembers.splice(movingIdx, 1);
|
||||
targetIdx = nextMembers.findIndex((member) => member._id === targetId);
|
||||
nextMembers.splice(insertAfter ? targetIdx + 1 : targetIdx, 0, moved);
|
||||
|
||||
Reference in New Issue
Block a user