feat(users): organically link Laptop Assets and User Management with multi-asset assigned/temporary laptops support
This commit is contained in:
parent
304c5e87ab
commit
d11dc0983f
2 changed files with 184 additions and 36 deletions
36
api.php
36
api.php
|
|
@ -139,6 +139,7 @@ FROM departments d JOIN dept_path dp ON d.parent_id = dp.id
|
|||
)
|
||||
SELECT u.*, dp.path as dept_name,
|
||||
(SELECT asset_tag FROM laptop_assets WHERE current_user_id = u.id ORDER BY id DESC LIMIT 1) as laptop_tag,
|
||||
(SELECT GROUP_CONCAT(asset_tag, ',') FROM laptop_assets WHERE current_user_id = u.id) as laptop_tags,
|
||||
(SELECT purchase_date FROM laptop_assets WHERE current_user_id = u.id ORDER BY id DESC LIMIT 1) as laptop_purchase_date,
|
||||
(SELECT m.model_name FROM laptop_assets a JOIN laptop_models m ON a.model_id = m.id WHERE a.current_user_id = u.id ORDER
|
||||
BY a.id DESC LIMIT 1) as laptop_model_name,
|
||||
|
|
@ -327,6 +328,41 @@ WHERE a.rental_user_id = ?";
|
|||
echo json_encode($stmt->fetchAll());
|
||||
break;
|
||||
|
||||
case 'get_user_laptops':
|
||||
$user_id = (int) $_GET['user_id'];
|
||||
$query = "SELECT a.*, m.model_name, m.manufacturer
|
||||
FROM laptop_assets a
|
||||
JOIN laptop_models m ON a.model_id = m.id
|
||||
WHERE a.current_user_id = ?";
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->execute([$user_id]);
|
||||
echo json_encode($stmt->fetchAll());
|
||||
break;
|
||||
|
||||
case 'unassign_laptop':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$asset_id = (int)$data['id'];
|
||||
|
||||
// Get asset details to log
|
||||
$old_stmt = $db->prepare("SELECT assigned_user_name FROM laptop_assets WHERE id = ?");
|
||||
$old_stmt->execute([$asset_id]);
|
||||
$old_name = $old_stmt->fetchColumn() ?: '';
|
||||
|
||||
// Update to stock
|
||||
$stmt = $db->prepare("UPDATE laptop_assets SET
|
||||
current_user_id = NULL,
|
||||
status = 'stock',
|
||||
assigned_user_name = '업무용(TEMP_44666b)'
|
||||
WHERE id = ?");
|
||||
$stmt->execute([$asset_id]);
|
||||
|
||||
// Log history
|
||||
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date, note) VALUES (?, 'assignment', '재고(반납)', ?, '직원 페이지에서 배정 해제됨')");
|
||||
$log_stmt->execute([$asset_id, date('Y-m-d')]);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'update_asset_remarks':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$stmt = $db->prepare("UPDATE laptop_assets SET remarks = ? WHERE id = ?");
|
||||
|
|
|
|||
184
users.php
184
users.php
|
|
@ -298,22 +298,23 @@
|
|||
<span x-text="user.position || '-'"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap cursor-pointer relative z-10"
|
||||
<td class="px-6 py-4 cursor-pointer relative z-10"
|
||||
@click="editUser(user)">
|
||||
<template x-if="user.laptop_tag">
|
||||
<div>
|
||||
<div class="text-[11px] font-black text-blue-600 bg-blue-50 px-2 py-0.5 rounded inline-block mb-1"
|
||||
x-text="user.laptop_tag"></div>
|
||||
<div class="text-[10px] text-slate-500 font-bold">
|
||||
<span
|
||||
x-text="user.laptop_purchase_date ? user.laptop_purchase_date.substring(0,4) : ''"></span>
|
||||
<span x-show="user.laptop_purchase_date && user.laptop_model_name"> /
|
||||
</span>
|
||||
<span x-text="user.laptop_model_name || ''"></span>
|
||||
<template x-if="user.laptop_tags">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex flex-wrap gap-1 max-w-[200px]">
|
||||
<template x-for="tag in user.laptop_tags.split(',')">
|
||||
<span class="text-[10px] font-black text-blue-600 bg-blue-50/80 px-2 py-0.5 rounded-lg border border-blue-100 shadow-sm"
|
||||
x-text="tag"></span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-400 font-medium truncate max-w-[200px]"
|
||||
:title="user.laptop_model_name"
|
||||
x-text="user.laptop_model_name ? (user.laptop_purchase_date ? user.laptop_purchase_date.substring(0,4) + ' / ' + user.laptop_model_name : user.laptop_model_name) : ''">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!user.laptop_tag">
|
||||
<template x-if="!user.laptop_tags">
|
||||
<span class="text-[10px] text-slate-300 italic">미배정</span>
|
||||
</template>
|
||||
</td>
|
||||
|
|
@ -530,32 +531,85 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Laptop Info Section (Read-only) -->
|
||||
<template x-if="isEdit && formData.laptop_tag">
|
||||
<div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4 mt-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
|
||||
<h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">사용 중인 노트북 정보</h4>
|
||||
<!-- Laptop Info Section (Dynamic & Interactive) -->
|
||||
<template x-if="isEdit">
|
||||
<div class="mt-6 border-t border-slate-100 pt-6 space-y-6">
|
||||
<!-- 정식 배정 노트북 -->
|
||||
<div class="bg-blue-50/40 p-6 rounded-[2rem] border border-blue-100/40 space-y-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse"></div>
|
||||
<h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">정식 배정 노트북 정보</h4>
|
||||
</div>
|
||||
</div>
|
||||
<template x-if="assignedLaptops.length === 0">
|
||||
<div class="text-center py-4 bg-white/50 rounded-2xl border border-dashed border-slate-200">
|
||||
<p class="text-xs text-slate-400 italic">현재 배정된 정식 노트북이 없습니다.</p>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="assignedLaptops.length > 0">
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<template x-for="laptop in assignedLaptops" :key="laptop.id">
|
||||
<div class="px-5 py-4 bg-white border border-slate-200/60 rounded-2xl flex items-center justify-between shadow-sm">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs font-black text-blue-600 bg-blue-50 px-2 py-0.5 rounded-md border border-blue-100" x-text="laptop.asset_tag"></span>
|
||||
<span class="text-sm font-bold text-slate-800" x-text="laptop.model_name"></span>
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-400 font-bold uppercase tracking-wider">
|
||||
<span x-text="laptop.manufacturer"></span>
|
||||
<span x-show="laptop.purchase_date"> • </span>
|
||||
<span x-text="laptop.purchase_date"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" @click="unassignPermanentLaptop(laptop)"
|
||||
class="px-3.5 py-1.5 bg-rose-50 hover:bg-rose-100 text-rose-600 text-xs font-bold rounded-xl transition-all border border-rose-100 active:scale-95">
|
||||
배정 해제
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label
|
||||
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">자산번호</label>
|
||||
<div class="px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold text-blue-600"
|
||||
x-text="formData.laptop_tag"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">모델명</label>
|
||||
<div class="px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold text-slate-700"
|
||||
x-text="formData.laptop_model_name || '-'"></div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">취득일</label>
|
||||
<div class="px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold text-slate-700"
|
||||
x-text="formData.laptop_purchase_date || '-'"></div>
|
||||
|
||||
<!-- 단기 임대 노트북 -->
|
||||
<div class="bg-emerald-50/40 p-6 rounded-[2rem] border border-emerald-100/40 space-y-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<h4 class="text-xs font-black text-emerald-600 uppercase tracking-widest">단기 임대 노트북 정보</h4>
|
||||
</div>
|
||||
</div>
|
||||
<template x-if="temporaryLaptops.length === 0">
|
||||
<div class="text-center py-4 bg-white/50 rounded-2xl border border-dashed border-slate-200">
|
||||
<p class="text-xs text-slate-400 italic">현재 임대 중인 단기 노트북이 없습니다.</p>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="temporaryLaptops.length > 0">
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<template x-for="laptop in temporaryLaptops" :key="laptop.id">
|
||||
<div class="px-5 py-4 bg-white border border-slate-200/60 rounded-2xl flex items-center justify-between shadow-sm">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs font-black text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-md border border-emerald-100" x-text="laptop.asset_tag"></span>
|
||||
<span class="text-sm font-bold text-slate-800" x-text="laptop.model_name"></span>
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-400 font-bold uppercase tracking-wider">
|
||||
<span x-text="laptop.manufacturer"></span>
|
||||
<span x-show="laptop.rental_start_date"> • 시작일: </span>
|
||||
<span x-text="laptop.rental_start_date"></span>
|
||||
<span x-show="laptop.rental_end_scheduled"> • 반납예정일: </span>
|
||||
<span x-text="laptop.rental_end_scheduled"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" @click="returnTemporaryLaptop(laptop)"
|
||||
class="px-3.5 py-1.5 bg-amber-50 hover:bg-amber-100 text-amber-600 text-xs font-bold rounded-xl transition-all border border-amber-100 active:scale-95">
|
||||
반납 처리
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -719,6 +773,8 @@
|
|||
showRestoreModal: false,
|
||||
showExportModal: false,
|
||||
userRentals: [],
|
||||
assignedLaptops: [],
|
||||
temporaryLaptops: [],
|
||||
archivedUsers: [],
|
||||
selectedColumns: ['name', 'emp_id', 'dept_name', 'position', 'email', 'mobile', 'status', 'laptop_tag'],
|
||||
columnOptions: {
|
||||
|
|
@ -892,6 +948,62 @@
|
|||
this.isEdit = true;
|
||||
this.formData = { ...user };
|
||||
this.showModal = true;
|
||||
this.assignedLaptops = [];
|
||||
this.temporaryLaptops = [];
|
||||
this.fetchAssignedLaptops(user.id);
|
||||
this.fetchTemporaryLaptops(user.id);
|
||||
},
|
||||
|
||||
fetchAssignedLaptops(userId) {
|
||||
fetch(`api.php?action=get_user_laptops&user_id=${userId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
this.assignedLaptops = data;
|
||||
});
|
||||
},
|
||||
|
||||
fetchTemporaryLaptops(userId) {
|
||||
fetch(`api.php?action=get_user_rentals&user_id=${userId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
this.temporaryLaptops = data;
|
||||
});
|
||||
},
|
||||
|
||||
unassignPermanentLaptop(laptop) {
|
||||
window.showConfirm(`[${laptop.asset_tag}] 정식 배정 노트북을 배정 해제하시겠습니까?`, () => {
|
||||
fetch('api.php?action=unassign_laptop', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: laptop.id })
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.success) {
|
||||
window.showAlert('배정이 해제되었습니다.', '성공', 'success');
|
||||
this.fetchAssignedLaptops(this.formData.id);
|
||||
this.fetchUsers(); // Refresh the main user list table
|
||||
} else {
|
||||
window.showAlert(data.error || '오류가 발생했습니다.', '실패', 'error');
|
||||
}
|
||||
});
|
||||
}, '배정 해제 확인');
|
||||
},
|
||||
|
||||
returnTemporaryLaptop(laptop) {
|
||||
window.showConfirm(`[${laptop.asset_tag}] 단기 임대 노트북을 반납 처리하시겠습니까?`, () => {
|
||||
fetch('api.php?action=return_rental', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: laptop.id })
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.success) {
|
||||
window.showAlert('반납 처리가 완료되었습니다.', '성공', 'success');
|
||||
this.fetchTemporaryLaptops(this.formData.id);
|
||||
this.fetchUsers(); // Refresh the main user list table
|
||||
} else {
|
||||
window.showAlert(data.error || '오류가 발생했습니다.', '실패', 'error');
|
||||
}
|
||||
});
|
||||
}, '단기 임대 반납 확인');
|
||||
},
|
||||
|
||||
submitUser() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue