마감
This commit is contained in:
parent
5e7a53e731
commit
3e1cd3adab
5 changed files with 321 additions and 18 deletions
100
api.php
100
api.php
|
|
@ -488,6 +488,15 @@ last_confirmed_date
|
||||||
$old_stmt->execute([$data['id']]);
|
$old_stmt->execute([$data['id']]);
|
||||||
$old_asset = $old_stmt->fetch();
|
$old_asset = $old_stmt->fetch();
|
||||||
|
|
||||||
|
// Auto-relocate based on status if needed (optional)
|
||||||
|
|
||||||
|
// If assigned_user_name is missing but current_user_id exists, try to fill it for logging
|
||||||
|
if (empty($data['assigned_user_name']) && !empty($data['current_user_id'])) {
|
||||||
|
$u_stmt = $db->prepare("SELECT name FROM users WHERE id = ?");
|
||||||
|
$u_stmt->execute([$data['current_user_id']]);
|
||||||
|
$data['assigned_user_name'] = $u_stmt->fetchColumn() ?: '';
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $db->prepare("UPDATE laptop_assets SET
|
$stmt = $db->prepare("UPDATE laptop_assets SET
|
||||||
asset_tag = ?, model_id = ?, current_user_id = ?, status = ?, serial_number = ?, purchase_date = ?, ip_address = ?,
|
asset_tag = ?, model_id = ?, current_user_id = ?, status = ?, serial_number = ?, purchase_date = ?, ip_address = ?,
|
||||||
cpu = ?, npu = ?, hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?, ram = ?,
|
cpu = ?, npu = ?, hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?, ram = ?,
|
||||||
|
|
@ -523,10 +532,11 @@ WHERE id = ?");
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Auto-log assignment change
|
// Auto-log assignment change
|
||||||
if ($data['assigned_user_name'] && $data['assigned_user_name'] !== ($old_asset['assigned_user_name'] ?? '')) {
|
$new_name = $data['assigned_user_name'] ?: '';
|
||||||
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?,
|
$old_name = $old_asset['assigned_user_name'] ?? '';
|
||||||
'assignment', ?, ?)");
|
if ($new_name !== $old_name) {
|
||||||
$log_stmt->execute([$data['id'], $data['assigned_user_name'], date('Y-m-d')]);
|
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'assignment', ?, ?)");
|
||||||
|
$log_stmt->execute([$data['id'], $new_name ?: '재고(반납)', date('Y-m-d')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|
@ -632,6 +642,11 @@ LIMIT 1")->fetchColumn();
|
||||||
if ($newStatus) {
|
if ($newStatus) {
|
||||||
$updates[] = "status = ?";
|
$updates[] = "status = ?";
|
||||||
$params[] = $newStatus;
|
$params[] = $newStatus;
|
||||||
|
// 만약 상태를 'stock'(재고)으로 변경하는 경우 배정 정보도 초기화
|
||||||
|
if ($newStatus === 'stock') {
|
||||||
|
$updates[] = "current_user_id = NULL";
|
||||||
|
$updates[] = "assigned_user_name = NULL";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($newModelId) {
|
if ($newModelId) {
|
||||||
$updates[] = "model_id = ?";
|
$updates[] = "model_id = ?";
|
||||||
|
|
@ -799,6 +814,83 @@ LIMIT 1")->fetchColumn();
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'archive_users':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$userIds = $data['user_ids'];
|
||||||
|
if (empty($userIds)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No users selected']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->beginTransaction();
|
||||||
|
try {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
|
||||||
|
// Copy to archive
|
||||||
|
$db->prepare("INSERT INTO users_archive (id, emp_id, name, department_id, position, email, phone, mobile, accounting_type, status)
|
||||||
|
SELECT id, emp_id, name, department_id, position, email, phone, mobile, accounting_type, status FROM users WHERE id IN ($placeholders)")
|
||||||
|
->execute($userIds);
|
||||||
|
// Delete from original
|
||||||
|
$db->prepare("DELETE FROM users WHERE id IN ($placeholders)")->execute($userIds);
|
||||||
|
$db->commit();
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$db->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_archived_users':
|
||||||
|
$query = "SELECT u.*, dp.path as dept_name
|
||||||
|
FROM users_archive u
|
||||||
|
LEFT JOIN (
|
||||||
|
WITH RECURSIVE dept_path(id, path) AS (
|
||||||
|
SELECT id, name FROM departments WHERE parent_id IS NULL
|
||||||
|
UNION ALL
|
||||||
|
SELECT d.id, dp.path || ' > ' || d.name
|
||||||
|
FROM departments d JOIN dept_path dp ON d.parent_id = dp.id
|
||||||
|
) SELECT * FROM dept_path
|
||||||
|
) dp ON u.department_id = dp.id
|
||||||
|
ORDER BY u.archived_at DESC";
|
||||||
|
echo json_encode($db->query($query)->fetchAll());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'restore_users':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$userIds = $data['user_ids'];
|
||||||
|
if (empty($userIds)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No users selected']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->beginTransaction();
|
||||||
|
try {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
|
||||||
|
// Restore to users
|
||||||
|
$db->prepare("INSERT INTO users (id, emp_id, name, department_id, position, email, phone, mobile, accounting_type, status)
|
||||||
|
SELECT id, emp_id, name, department_id, position, email, phone, mobile, accounting_type, status FROM users_archive WHERE id IN ($placeholders)")
|
||||||
|
->execute($userIds);
|
||||||
|
// Delete from archive
|
||||||
|
$db->prepare("DELETE FROM users_archive WHERE id IN ($placeholders)")->execute($userIds);
|
||||||
|
$db->commit();
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$db->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bulk_delete_users':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$userIds = $data['user_ids'];
|
||||||
|
if (empty($userIds)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No users selected']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$placeholders = implode(',', array_fill(0, count($userIds), '?'));
|
||||||
|
$db->prepare("DELETE FROM users WHERE id IN ($placeholders)")->execute($userIds);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'get_general_rentals':
|
case 'get_general_rentals':
|
||||||
$query = "SELECT r.*, GROUP_CONCAT(i.item_name, ', ') as items
|
$query = "SELECT r.*, GROUP_CONCAT(i.item_name, ', ') as items
|
||||||
FROM general_rentals r
|
FROM general_rentals r
|
||||||
|
|
|
||||||
BIN
assets.db
BIN
assets.db
Binary file not shown.
9
fix_missing_column.php
Normal file
9
fix_missing_column.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
require_once 'config.php';
|
||||||
|
try {
|
||||||
|
$db->exec("ALTER TABLE laptop_assets ADD COLUMN last_confirmed_date TEXT");
|
||||||
|
echo "Column 'last_confirmed_date' added successfully to laptop_assets table.\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error or column already exists: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
?>
|
||||||
58
laptops.php
58
laptops.php
|
|
@ -38,6 +38,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="relative group">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-blue-500 transition-colors"
|
||||||
|
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input type="text" x-model="searchQuery"
|
||||||
|
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm shadow-sm"
|
||||||
|
placeholder="자산번호, 모델명, 사용자, IP 검색...">
|
||||||
|
</div>
|
||||||
<button @click="exportToExcel()"
|
<button @click="exportToExcel()"
|
||||||
class="bg-white border border-slate-200 text-slate-700 px-5 py-2.5 rounded-xl font-bold hover:bg-slate-50 transition-all flex items-center shadow-sm text-sm">
|
class="bg-white border border-slate-200 text-slate-700 px-5 py-2.5 rounded-xl font-bold hover:bg-slate-50 transition-all flex items-center shadow-sm text-sm">
|
||||||
<svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -94,7 +106,7 @@
|
||||||
|
|
||||||
<!-- Laptop List Table -->
|
<!-- Laptop List Table -->
|
||||||
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden"
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden"
|
||||||
x-show="!loading && assets.length > 0">
|
x-show="!loading && filteredAssets.length > 0">
|
||||||
<table class="min-w-full divide-y divide-slate-100">
|
<table class="min-w-full divide-y divide-slate-100">
|
||||||
<thead class="bg-slate-50/50">
|
<thead class="bg-slate-50/50">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -205,9 +217,14 @@
|
||||||
<div x-show="loading" class="p-20 flex justify-center">
|
<div x-show="loading" class="p-20 flex justify-center">
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="!loading && assets.length === 0"
|
<div x-show="!loading && filteredAssets.length === 0"
|
||||||
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300">
|
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300">
|
||||||
|
<template x-if="searchQuery">
|
||||||
|
<p class="text-slate-400 font-medium italic">검색 결과와 일치하는 자산이 없습니다.</p>
|
||||||
|
</template>
|
||||||
|
<template x-if="!searchQuery">
|
||||||
<p class="text-slate-400 font-medium italic">등록된 노트북 자산이 없습니다.</p>
|
<p class="text-slate-400 font-medium italic">등록된 노트북 자산이 없습니다.</p>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
@ -263,7 +280,7 @@
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">배정
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">배정
|
||||||
사용자</label>
|
사용자</label>
|
||||||
<select x-model="formData.current_user_id"
|
<select x-model="formData.current_user_id" @change="updateAssignedUserName()"
|
||||||
class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
|
class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
|
||||||
<option value="">미배정 (재고)</option>
|
<option value="">미배정 (재고)</option>
|
||||||
<template x-for="user in allUsers" :key="user.id">
|
<template x-for="user in allUsers" :key="user.id">
|
||||||
|
|
@ -398,6 +415,7 @@
|
||||||
selectedIds: [],
|
selectedIds: [],
|
||||||
bulkStatus: '',
|
bulkStatus: '',
|
||||||
bulkModelId: '',
|
bulkModelId: '',
|
||||||
|
searchQuery: '',
|
||||||
sortKey: 'asset_tag',
|
sortKey: 'asset_tag',
|
||||||
sortOrder: 'asc',
|
sortOrder: 'asc',
|
||||||
formData: {
|
formData: {
|
||||||
|
|
@ -458,8 +476,23 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get filteredAssets() {
|
||||||
|
let result = [...this.assets];
|
||||||
|
if (this.searchQuery) {
|
||||||
|
const q = this.searchQuery.toLowerCase();
|
||||||
|
result = result.filter(a => (a.asset_tag && a.asset_tag.toLowerCase().includes(q)) ||
|
||||||
|
(a.model_name && a.model_name.toLowerCase().includes(q)) ||
|
||||||
|
(a.manufacturer && a.manufacturer.toLowerCase().includes(q)) ||
|
||||||
|
(a.user_name && a.user_name.toLowerCase().includes(q)) ||
|
||||||
|
(a.ip_address && a.ip_address.toLowerCase().includes(q)) ||
|
||||||
|
(a.serial_number && a.serial_number.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
get sortedAssets() {
|
get sortedAssets() {
|
||||||
return [...this.assets].sort((a, b) => {
|
return this.filteredAssets.sort((a, b) => {
|
||||||
let v1 = a[this.sortKey] || '';
|
let v1 = a[this.sortKey] || '';
|
||||||
let v2 = b[this.sortKey] || '';
|
let v2 = b[this.sortKey] || '';
|
||||||
if (this.sortOrder === 'asc') return v1 > v2 ? 1 : -1;
|
if (this.sortOrder === 'asc') return v1 > v2 ? 1 : -1;
|
||||||
|
|
@ -533,12 +566,29 @@
|
||||||
body: JSON.stringify(this.formData)
|
body: JSON.stringify(this.formData)
|
||||||
}).then(res => res.json()).then(data => {
|
}).then(res => res.json()).then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
window.showAlert(this.isEdit ? '자산 정보가 수정되었습니다.' : '새 자산이 등록되었습니다.', '성공', 'success');
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
this.fetchAssets();
|
this.fetchAssets();
|
||||||
|
} else {
|
||||||
|
window.showAlert(data.error || '처리 중 오류가 발생했습니다.', '오류', 'error');
|
||||||
}
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
window.showAlert('서버 통신 오류가 발생했습니다.', '시스템 오류', 'error');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateAssignedUserName() {
|
||||||
|
if (!this.formData.current_user_id) {
|
||||||
|
this.formData.assigned_user_name = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const user = this.allUsers.find(u => u.id == this.formData.current_user_id);
|
||||||
|
if (user) {
|
||||||
|
this.formData.assigned_user_name = user.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
exportToExcel() {
|
exportToExcel() {
|
||||||
if (this.assets.length === 0) return;
|
if (this.assets.length === 0) return;
|
||||||
const headers = ['자산관리번호', '모델명', '제조사', '프로세서', 'RAM', 'SSD', 'IP주소', '시리얼', '사용자', '상태', '취득일'];
|
const headers = ['자산관리번호', '모델명', '제조사', '프로세서', 'RAM', 'SSD', 'IP주소', '시리얼', '사용자', '상태', '취득일'];
|
||||||
|
|
|
||||||
152
users.php
152
users.php
|
|
@ -71,6 +71,7 @@
|
||||||
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm"
|
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm"
|
||||||
placeholder="이름, 사번, 이메일 검색...">
|
placeholder="이름, 사번, 이메일 검색...">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<button @click="openAddModal()"
|
<button @click="openAddModal()"
|
||||||
class="bg-blue-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all flex items-center shrink-0">
|
class="bg-blue-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all flex items-center shrink-0">
|
||||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -79,6 +80,15 @@
|
||||||
</svg>
|
</svg>
|
||||||
신규 등록
|
신규 등록
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="openRestoreModal()"
|
||||||
|
class="bg-slate-800 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-slate-200 hover:bg-slate-900 transition-all flex items-center shrink-0">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||||
|
</svg>
|
||||||
|
불러오기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -139,6 +149,26 @@
|
||||||
<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">일괄
|
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>
|
적용</button>
|
||||||
|
|
||||||
|
<div class="h-8 w-px bg-white/10 mx-1"></div>
|
||||||
|
|
||||||
|
<button @click="archiveUsers"
|
||||||
|
class="bg-emerald-600 hover:bg-emerald-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-emerald-500/20 active:scale-95 flex items-center gap-2">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
||||||
|
</svg>
|
||||||
|
내보내기
|
||||||
|
</button>
|
||||||
|
<button @click="bulkDelete"
|
||||||
|
class="bg-rose-600 hover:bg-rose-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-rose-500/20 active:scale-95 flex items-center gap-2">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
|
||||||
<button @click="selectedIds = []"
|
<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">
|
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">
|
||||||
|
|
@ -532,6 +562,67 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Restore (Import) Modal -->
|
||||||
|
<div x-show="showRestoreModal" x-cloak class="fixed inset-0 z-[120] flex items-center justify-center p-4">
|
||||||
|
<div class="fixed inset-0 modal-bg" @click="showRestoreModal = false"></div>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-[2.5rem] p-8 max-w-2xl w-full relative z-[121] shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
|
||||||
|
<div class="flex justify-between items-start mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-black text-slate-900">아카이브에서 불러오기</h3>
|
||||||
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">Select Archived Employees
|
||||||
|
to Restore</p>
|
||||||
|
</div>
|
||||||
|
<button @click="showRestoreModal = false" class="text-slate-300 hover:text-slate-900 transition-colors">
|
||||||
|
<svg class="w-8 h-8" 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" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto pr-2 no-scrollbar">
|
||||||
|
<template x-if="archivedUsers.length === 0">
|
||||||
|
<div class="text-center py-20">
|
||||||
|
<div class="w-16 h-16 bg-slate-50 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg class="w-8 h-8 text-slate-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-slate-400 font-bold">내보내기된 직원이 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-3">
|
||||||
|
<template x-for="user in archivedUsers" :key="user.id">
|
||||||
|
<div
|
||||||
|
class="p-4 bg-slate-50 border border-slate-100 rounded-2xl flex items-center justify-between group hover:bg-white hover:shadow-lg hover:border-blue-200 transition-all">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 bg-white rounded-xl flex items-center justify-center shadow-sm text-slate-400 group-hover:text-blue-500 transition-colors">
|
||||||
|
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-black text-slate-900" x-text="user.name"></div>
|
||||||
|
<div class="text-[10px] font-bold text-slate-400"
|
||||||
|
x-text="user.dept_name + ' / ' + user.position"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="restoreUser(user.id)"
|
||||||
|
class="px-4 py-2 bg-slate-900 text-white text-[11px] font-black rounded-xl hover:bg-blue-600 transition-all active:scale-95">
|
||||||
|
불러오기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function userManagement() {
|
function userManagement() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -551,7 +642,9 @@
|
||||||
bulkDeptId: '',
|
bulkDeptId: '',
|
||||||
bulkPosition: '',
|
bulkPosition: '',
|
||||||
showRentalListModal: false,
|
showRentalListModal: false,
|
||||||
|
showRestoreModal: false,
|
||||||
userRentals: [],
|
userRentals: [],
|
||||||
|
archivedUsers: [],
|
||||||
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' },
|
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' },
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
@ -765,6 +858,65 @@
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.isEdit = false;
|
this.isEdit = false;
|
||||||
this.formData = { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' };
|
this.formData = { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' };
|
||||||
|
},
|
||||||
|
|
||||||
|
archiveUsers() {
|
||||||
|
window.showConfirm(`선택한 ${this.selectedIds.length}명의 직원을 내보내기 하시겠습니까?\n내보내기된 정보는 '불러오기' 메뉴에서 확인 가능합니다.`, () => {
|
||||||
|
fetch('api.php?action=archive_users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ user_ids: this.selectedIds })
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.showAlert('선택한 인원이 아카이브로 내보내기 되었습니다.', '내보내기 완료', 'success');
|
||||||
|
this.selectedIds = [];
|
||||||
|
this.fetchUsers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, '내보내기 확인');
|
||||||
|
},
|
||||||
|
|
||||||
|
bulkDelete() {
|
||||||
|
window.showConfirm(`선택한 ${this.selectedIds.length}명의 직원을 영구 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`, () => {
|
||||||
|
fetch('api.php?action=bulk_delete_users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ user_ids: this.selectedIds })
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.showAlert('영구 삭제되었습니다.', '삭제 완료', 'success');
|
||||||
|
this.selectedIds = [];
|
||||||
|
this.fetchUsers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, '영구 삭제 확인');
|
||||||
|
},
|
||||||
|
|
||||||
|
openRestoreModal() {
|
||||||
|
this.fetchArchivedUsers();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchArchivedUsers() {
|
||||||
|
fetch('api.php?action=get_archived_users')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
this.archivedUsers = data;
|
||||||
|
this.showRestoreModal = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
restoreUser(id) {
|
||||||
|
fetch('api.php?action=restore_users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ user_ids: [id] })
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.showAlert('직원 정보가 복구되었습니다.', '불러오기 성공', 'success');
|
||||||
|
this.fetchArchivedUsers();
|
||||||
|
this.fetchUsers();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue