반영보자
This commit is contained in:
parent
e1372061e1
commit
e333930c09
3 changed files with 93 additions and 30 deletions
33
api.php
33
api.php
|
|
@ -387,7 +387,7 @@ try {
|
||||||
echo json_encode(['success' => true, 'merged' => true]);
|
echo json_encode(['success' => true, 'merged' => true]);
|
||||||
} else {
|
} else {
|
||||||
// Check if a department with the same name exists under the same parent (excluding itself)
|
// Check if a department with the same name exists under the same parent (excluding itself)
|
||||||
$check_stmt = $db->prepare("SELECT id FROM departments WHERE name = ? AND (parent_id <=> ?) AND id != ?");
|
$check_stmt = $db->prepare("SELECT id FROM departments WHERE name = ? AND parent_id IS ? AND id != ?");
|
||||||
$check_stmt->execute([$new_name, $parent_id, $dept_id]);
|
$check_stmt->execute([$new_name, $parent_id, $dept_id]);
|
||||||
$existing = $check_stmt->fetch();
|
$existing = $check_stmt->fetch();
|
||||||
|
|
||||||
|
|
@ -403,18 +403,32 @@ try {
|
||||||
|
|
||||||
case 'delete_dept':
|
case 'delete_dept':
|
||||||
$dept_id = $_GET['id'];
|
$dept_id = $_GET['id'];
|
||||||
// 1. Find the '미소속' department ID
|
|
||||||
$miso_id = $db->query("SELECT id FROM departments WHERE name = '미소속'")->fetchColumn();
|
// Find FKI Root
|
||||||
|
$fki_root = $db->query("SELECT id FROM departments WHERE name = '한국경제인협회' AND parent_id IS NULL LIMIT 1")->fetch();
|
||||||
|
$fki_root_id = $fki_root ? $fki_root['id'] : null;
|
||||||
|
|
||||||
|
// 1. Find the '미소속' department under FKI Root
|
||||||
|
$miso_id = $db->query("SELECT id FROM departments WHERE name = '미소속' AND parent_id IS " . ($fki_root_id ?: 'NULL') . " LIMIT 1")->fetchColumn();
|
||||||
|
|
||||||
if (!$miso_id) {
|
if (!$miso_id) {
|
||||||
// Fallback creation if not exists
|
$db->prepare("INSERT INTO departments (name, parent_id, level) VALUES ('미소속', ?, 1)")->execute([$fki_root_id]);
|
||||||
$db->exec("INSERT INTO departments (name, level) VALUES ('미소속', 0)");
|
|
||||||
$miso_id = $db->lastInsertId();
|
$miso_id = $db->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Move members to '미소속'
|
// 2. Get current dept info for re-parenting children
|
||||||
|
$current_dept = $db->prepare("SELECT parent_id FROM departments WHERE id = ?");
|
||||||
|
$current_dept->execute([$dept_id]);
|
||||||
|
$parent_info = $current_dept->fetch();
|
||||||
|
$target_new_parent = $parent_info ? $parent_info['parent_id'] : null;
|
||||||
|
|
||||||
|
// 3. Move members to '미소속'
|
||||||
$db->prepare("UPDATE users SET department_id = ? WHERE department_id = ?")->execute([$miso_id, $dept_id]);
|
$db->prepare("UPDATE users SET department_id = ? WHERE department_id = ?")->execute([$miso_id, $dept_id]);
|
||||||
|
|
||||||
// 3. Delete the department
|
// 4. Move child departments to its grandparent (re-parenting) to avoid FK constraint violation
|
||||||
|
$db->prepare("UPDATE departments SET parent_id = ? WHERE parent_id = ?")->execute([$target_new_parent, $dept_id]);
|
||||||
|
|
||||||
|
// 5. Delete the department
|
||||||
$db->prepare("DELETE FROM departments WHERE id = ?")->execute([$dept_id]);
|
$db->prepare("DELETE FROM departments WHERE id = ?")->execute([$dept_id]);
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|
@ -582,6 +596,11 @@ try {
|
||||||
$updates[] = "department_id = ?";
|
$updates[] = "department_id = ?";
|
||||||
$params[] = $newDeptId;
|
$params[] = $newDeptId;
|
||||||
}
|
}
|
||||||
|
$newPosition = $data['position'] ?? null;
|
||||||
|
if ($newPosition) {
|
||||||
|
$updates[] = "position = ?";
|
||||||
|
$params[] = $newPosition;
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($updates)) {
|
if (empty($updates)) {
|
||||||
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
||||||
|
|
|
||||||
|
|
@ -88,10 +88,6 @@
|
||||||
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl">
|
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h3 class="text-2xl font-black text-slate-900" x-text="isEdit ? '부서 정보 수정' : '신규 부서 생성'"></h3>
|
<h3 class="text-2xl font-black text-slate-900" x-text="isEdit ? '부서 정보 수정' : '신규 부서 생성'"></h3>
|
||||||
<button x-show="isEdit" type="button" @click="deleteDept()"
|
|
||||||
class="text-xs font-bold text-rose-500 hover:text-rose-700 bg-rose-50 px-3 py-1.5 rounded-lg border border-rose-100 transition-colors">
|
|
||||||
부서 제거
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submitDept" class="space-y-4">
|
<form @submit.prevent="submitDept" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -115,6 +111,13 @@
|
||||||
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold"
|
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold"
|
||||||
x-text="isEdit ? '수정 완료' : '부서 생성'"></button>
|
x-text="isEdit ? '수정 완료' : '부서 생성'"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div x-show="isEdit" class="pt-4 mt-4 border-t border-slate-100">
|
||||||
|
<button type="button" @click="deleteDept()"
|
||||||
|
class="w-full py-3 text-sm font-bold text-rose-500 hover:text-white hover:bg-rose-500 border border-rose-200 rounded-xl transition-all">
|
||||||
|
이 부서 삭제하기
|
||||||
|
</button>
|
||||||
|
<p class="text-[10px] text-slate-400 mt-2 text-center">※ 삭제 시 소속 인원은 '미소속'으로 이동됩니다.</p>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -245,12 +248,22 @@
|
||||||
deleteDept() {
|
deleteDept() {
|
||||||
if (confirm(`정말로 '${this.formData.name}' 부서를 제거하시겠습니까?\n이 명령을 수행하면 소속된 부서원들은 자동으로 '[한국경제인협회] - 미소속' 부서로 이동됩니다.\n이 작업은 되돌릴 수 없습니다.`)) {
|
if (confirm(`정말로 '${this.formData.name}' 부서를 제거하시겠습니까?\n이 명령을 수행하면 소속된 부서원들은 자동으로 '[한국경제인협회] - 미소속' 부서로 이동됩니다.\n이 작업은 되돌릴 수 없습니다.`)) {
|
||||||
fetch(`api.php?action=delete_dept&id=${this.formData.id}`)
|
fetch(`api.php?action=delete_dept&id=${this.formData.id}`)
|
||||||
.then(res => res.json())
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('네트워크 응답이 올바르지 않습니다.');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
alert('부서가 성공적으로 제거되었습니다.');
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
this.fetchDepts();
|
this.fetchDepts();
|
||||||
|
} else {
|
||||||
|
alert('부서 제거 중 오류가 발생했습니다: ' + (data.error || '알 수 없는 오류'));
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
alert('부서 제거 요청 중 오류가 발생했습니다.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
67
users.php
67
users.php
|
|
@ -102,7 +102,8 @@
|
||||||
<div x-show="selectedIds.length > 0" x-transition x-cloak
|
<div x-show="selectedIds.length > 0" x-transition x-cloak
|
||||||
class="fixed bottom-10 left-10 z-[200] bg-slate-900 text-white px-6 py-4 rounded-3xl flex items-center gap-6 shadow-2xl border border-white/10 animate-in fade-in slide-in-from-left-4 duration-300">
|
class="fixed bottom-10 left-10 z-[200] bg-slate-900 text-white px-6 py-4 rounded-3xl flex items-center gap-6 shadow-2xl border border-white/10 animate-in fade-in slide-in-from-left-4 duration-300">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center font-black text-sm shadow-lg shadow-blue-500/30" x-text="selectedIds.length"></div>
|
<div class="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center font-black text-sm shadow-lg shadow-blue-500/30"
|
||||||
|
x-text="selectedIds.length"></div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">Selected</span>
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">Selected</span>
|
||||||
<span class="text-xs font-bold">인원 선택됨</span>
|
<span class="text-xs font-bold">인원 선택됨</span>
|
||||||
|
|
@ -116,6 +117,7 @@
|
||||||
<option value="active">재직</option>
|
<option value="active">재직</option>
|
||||||
<option value="retired">퇴사</option>
|
<option value="retired">퇴사</option>
|
||||||
<option value="on_leave">휴직</option>
|
<option value="on_leave">휴직</option>
|
||||||
|
<option value="classification">분류</option>
|
||||||
</select>
|
</select>
|
||||||
<select x-model="bulkDeptId"
|
<select x-model="bulkDeptId"
|
||||||
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[150px]">
|
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[150px]">
|
||||||
|
|
@ -124,11 +126,16 @@
|
||||||
<option :value="dept.id" x-text="dept.name"></option>
|
<option :value="dept.id" x-text="dept.name"></option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
|
<input type="text" x-model="bulkPosition" placeholder="직위 변경..."
|
||||||
|
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 shadow-inner w-32">
|
||||||
<button @click="applyBulkUpdate"
|
<button @click="applyBulkUpdate"
|
||||||
class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">일괄 적용</button>
|
class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">일괄
|
||||||
<button @click="selectedIds = []" class="ml-2 w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 hover:text-white transition-all">
|
적용</button>
|
||||||
|
<button @click="selectedIds = []"
|
||||||
|
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 hover:text-white transition-all">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,8 +167,8 @@
|
||||||
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">연락처
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">연락처 /
|
||||||
</th>
|
사내전화</th>
|
||||||
<th @click="toggleSort('accounting_type')"
|
<th @click="toggleSort('accounting_type')"
|
||||||
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:text-blue-600 transition-colors">
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:text-blue-600 transition-colors">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|
@ -206,7 +213,16 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-sm text-slate-600 font-medium" x-text="user.email || '-'"></div>
|
<div class="text-sm text-slate-600 font-medium" x-text="user.email || '-'"></div>
|
||||||
<div class="text-xs text-slate-400" x-text="user.mobile || user.phone || '-'"></div>
|
<div class="flex flex-col gap-0.5 mt-1">
|
||||||
|
<div class="text-[10px] text-slate-400 flex items-center">
|
||||||
|
<span class="w-12 font-bold uppercase tracking-tighter">Mobile</span>
|
||||||
|
<span class="text-slate-600 font-semibold" x-text="user.mobile || '-'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] text-slate-400 flex items-center">
|
||||||
|
<span class="w-12 font-bold uppercase tracking-tighter">사내전화</span>
|
||||||
|
<span class="text-slate-600 font-semibold" x-text="user.phone || '-'"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
|
|
@ -227,6 +243,10 @@
|
||||||
<span
|
<span
|
||||||
class="px-3 py-1 text-xs font-bold rounded-full bg-amber-50 text-amber-600 border border-amber-100">휴직</span>
|
class="px-3 py-1 text-xs font-bold rounded-full bg-amber-50 text-amber-600 border border-amber-100">휴직</span>
|
||||||
</template>
|
</template>
|
||||||
|
<template x-if="user.status === 'classification'">
|
||||||
|
<span
|
||||||
|
class="px-3 py-1 text-xs font-bold rounded-full bg-indigo-50 text-indigo-600 border border-indigo-100">분류</span>
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -285,13 +305,6 @@
|
||||||
<input type="email" x-model="formData.email"
|
<input type="email" x-model="formData.email"
|
||||||
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">휴대폰</label>
|
|
||||||
<input type="text" x-model="formData.mobile"
|
|
||||||
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-6">
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">회계구분</label>
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">회계구분</label>
|
||||||
<select x-model="formData.accounting_type"
|
<select x-model="formData.accounting_type"
|
||||||
|
|
@ -300,6 +313,20 @@
|
||||||
<option value="특별회계">특별회계</option>
|
<option value="특별회계">특별회계</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">휴대폰</label>
|
||||||
|
<input type="text" x-model="formData.mobile"
|
||||||
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">사내전화</label>
|
||||||
|
<input type="text" x-model="formData.phone"
|
||||||
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">상태</label>
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">상태</label>
|
||||||
<select x-model="formData.status"
|
<select x-model="formData.status"
|
||||||
|
|
@ -307,6 +334,7 @@
|
||||||
<option value="active">재직</option>
|
<option value="active">재직</option>
|
||||||
<option value="retired">퇴사</option>
|
<option value="retired">퇴사</option>
|
||||||
<option value="on_leave">휴직</option>
|
<option value="on_leave">휴직</option>
|
||||||
|
<option value="classification">분류</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -337,7 +365,8 @@
|
||||||
selectedIds: [],
|
selectedIds: [],
|
||||||
bulkStatus: '',
|
bulkStatus: '',
|
||||||
bulkDeptId: '',
|
bulkDeptId: '',
|
||||||
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active' },
|
bulkPosition: '',
|
||||||
|
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' },
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
|
|
@ -355,7 +384,7 @@
|
||||||
result = result.filter(u => u.name.toLowerCase().includes(q) || u.emp_id.toLowerCase().includes(q) || (u.email && u.email.toLowerCase().includes(q)));
|
result = result.filter(u => u.name.toLowerCase().includes(q) || u.emp_id.toLowerCase().includes(q) || (u.email && u.email.toLowerCase().includes(q)));
|
||||||
}
|
}
|
||||||
if (!this.filterRetired) result = result.filter(u => u.status !== 'retired');
|
if (!this.filterRetired) result = result.filter(u => u.status !== 'retired');
|
||||||
if (!this.filterOnLeave) result = result.filter(u => u.status !== 'on_leave');
|
if (!this.filterOnLeave) result = result.filter(u => u.status !== 'on_leave' && u.status !== 'classification');
|
||||||
|
|
||||||
if (this.sortKey) {
|
if (this.sortKey) {
|
||||||
result.sort((a, b) => {
|
result.sort((a, b) => {
|
||||||
|
|
@ -426,20 +455,22 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
applyBulkUpdate() {
|
applyBulkUpdate() {
|
||||||
if (!this.bulkStatus && !this.bulkDeptId) return;
|
if (this.selectedIds.length === 0) return;
|
||||||
fetch('api.php?action=bulk_update_users', {
|
fetch('api.php?action=bulk_update_users', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
user_ids: this.selectedIds,
|
user_ids: this.selectedIds,
|
||||||
status: this.bulkStatus,
|
status: this.bulkStatus,
|
||||||
department_id: this.bulkDeptId
|
department_id: this.bulkDeptId,
|
||||||
|
position: this.bulkPosition
|
||||||
})
|
})
|
||||||
}).then(res => res.json()).then(data => {
|
}).then(res => res.json()).then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
this.selectedIds = [];
|
this.selectedIds = [];
|
||||||
this.bulkStatus = '';
|
this.bulkStatus = '';
|
||||||
this.bulkDeptId = '';
|
this.bulkDeptId = '';
|
||||||
|
this.bulkPosition = '';
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue