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 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 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 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
|
(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,
|
BY a.id DESC LIMIT 1) as laptop_model_name,
|
||||||
|
|
@ -327,6 +328,41 @@ WHERE a.rental_user_id = ?";
|
||||||
echo json_encode($stmt->fetchAll());
|
echo json_encode($stmt->fetchAll());
|
||||||
break;
|
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':
|
case 'update_asset_remarks':
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
$stmt = $db->prepare("UPDATE laptop_assets SET remarks = ? WHERE id = ?");
|
$stmt = $db->prepare("UPDATE laptop_assets SET remarks = ? WHERE id = ?");
|
||||||
|
|
|
||||||
176
users.php
176
users.php
|
|
@ -298,22 +298,23 @@
|
||||||
<span x-text="user.position || '-'"></span>
|
<span x-text="user.position || '-'"></span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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)">
|
@click="editUser(user)">
|
||||||
<template x-if="user.laptop_tag">
|
<template x-if="user.laptop_tags">
|
||||||
<div>
|
<div class="flex flex-col gap-1.5">
|
||||||
<div class="text-[11px] font-black text-blue-600 bg-blue-50 px-2 py-0.5 rounded inline-block mb-1"
|
<div class="flex flex-wrap gap-1 max-w-[200px]">
|
||||||
x-text="user.laptop_tag"></div>
|
<template x-for="tag in user.laptop_tags.split(',')">
|
||||||
<div class="text-[10px] text-slate-500 font-bold">
|
<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"
|
||||||
<span
|
x-text="tag"></span>
|
||||||
x-text="user.laptop_purchase_date ? user.laptop_purchase_date.substring(0,4) : ''"></span>
|
</template>
|
||||||
<span x-show="user.laptop_purchase_date && user.laptop_model_name"> /
|
</div>
|
||||||
</span>
|
<div class="text-[10px] text-slate-400 font-medium truncate max-w-[200px]"
|
||||||
<span x-text="user.laptop_model_name || ''"></span>
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!user.laptop_tag">
|
<template x-if="!user.laptop_tags">
|
||||||
<span class="text-[10px] text-slate-300 italic">미배정</span>
|
<span class="text-[10px] text-slate-300 italic">미배정</span>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -530,33 +531,86 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Laptop Info Section (Read-only) -->
|
<!-- Laptop Info Section (Dynamic & Interactive) -->
|
||||||
<template x-if="isEdit && formData.laptop_tag">
|
<template x-if="isEdit">
|
||||||
<div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4 mt-6">
|
<div class="mt-6 border-t border-slate-100 pt-6 space-y-6">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<!-- 정식 배정 노트북 -->
|
||||||
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
|
<div class="bg-blue-50/40 p-6 rounded-[2rem] border border-blue-100/40 space-y-4">
|
||||||
<h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">사용 중인 노트북 정보</h4>
|
<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>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
</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>
|
||||||
<label
|
<div class="flex items-center gap-2 mb-1">
|
||||||
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">자산번호</label>
|
<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>
|
||||||
<div class="px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold text-blue-600"
|
<span class="text-sm font-bold text-slate-800" x-text="laptop.model_name"></span>
|
||||||
x-text="formData.laptop_tag"></div>
|
|
||||||
</div>
|
</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="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>
|
||||||
<label
|
<div class="flex items-center gap-2 mb-1">
|
||||||
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">모델명</label>
|
<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>
|
||||||
<div class="px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold text-slate-700"
|
<span class="text-sm font-bold text-slate-800" x-text="laptop.model_name"></span>
|
||||||
x-text="formData.laptop_model_name || '-'"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="text-[10px] text-slate-400 font-bold uppercase tracking-wider">
|
||||||
<label
|
<span x-text="laptop.manufacturer"></span>
|
||||||
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">취득일</label>
|
<span x-show="laptop.rental_start_date"> • 시작일: </span>
|
||||||
<div class="px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold text-slate-700"
|
<span x-text="laptop.rental_start_date"></span>
|
||||||
x-text="formData.laptop_purchase_date || '-'"></div>
|
<span x-show="laptop.rental_end_scheduled"> • 반납예정일: </span>
|
||||||
|
<span x-text="laptop.rental_end_scheduled"></span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="pt-6 flex gap-4">
|
<div class="pt-6 flex gap-4">
|
||||||
|
|
@ -719,6 +773,8 @@
|
||||||
showRestoreModal: false,
|
showRestoreModal: false,
|
||||||
showExportModal: false,
|
showExportModal: false,
|
||||||
userRentals: [],
|
userRentals: [],
|
||||||
|
assignedLaptops: [],
|
||||||
|
temporaryLaptops: [],
|
||||||
archivedUsers: [],
|
archivedUsers: [],
|
||||||
selectedColumns: ['name', 'emp_id', 'dept_name', 'position', 'email', 'mobile', 'status', 'laptop_tag'],
|
selectedColumns: ['name', 'emp_id', 'dept_name', 'position', 'email', 'mobile', 'status', 'laptop_tag'],
|
||||||
columnOptions: {
|
columnOptions: {
|
||||||
|
|
@ -892,6 +948,62 @@
|
||||||
this.isEdit = true;
|
this.isEdit = true;
|
||||||
this.formData = { ...user };
|
this.formData = { ...user };
|
||||||
this.showModal = true;
|
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() {
|
submitUser() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue