업로드

This commit is contained in:
NAS-Admin 2026-03-04 21:15:35 +09:00
parent a6b56d379f
commit 04b7897904
8 changed files with 563 additions and 107 deletions

17
add_disposal_columns.php Normal file
View file

@ -0,0 +1,17 @@
<?php
require_once 'config.php';
try {
$db->exec("ALTER TABLE laptop_assets ADD COLUMN disposal_recipient TEXT");
} catch (Exception $e) {
echo "disposal_recipient already exists or error: " . $e->getMessage() . "\n";
}
try {
$db->exec("ALTER TABLE laptop_assets ADD COLUMN disposal_date TEXT");
} catch (Exception $e) {
echo "disposal_date already exists or error: " . $e->getMessage() . "\n";
}
echo "Database updated successfully.\n";
?>

52
api.php
View file

@ -78,8 +78,9 @@ try {
break; break;
case 'get_dashboard_stats': case 'get_dashboard_stats':
// Laptops // Laptops
$laptop_total = $db->query("SELECT COUNT(*) FROM laptop_assets")->fetchColumn(); $laptop_total = $db->query("SELECT COUNT(*) FROM laptop_assets WHERE status != 'disposed'")->fetchColumn();
$laptop_assigned = $db->query("SELECT COUNT(*) FROM laptop_assets WHERE status = 'assigned'")->fetchColumn(); $laptop_assigned = $db->query("SELECT COUNT(*) FROM laptop_assets WHERE status = 'assigned'")->fetchColumn();
$laptop_disposed = $db->query("SELECT COUNT(*) FROM laptop_assets WHERE status = 'disposed'")->fetchColumn();
// Cards // Cards
$card_total = $db->query("SELECT COUNT(*) FROM access_cards")->fetchColumn(); $card_total = $db->query("SELECT COUNT(*) FROM access_cards")->fetchColumn();
@ -94,7 +95,7 @@ try {
$users_special = $db->query("SELECT COUNT(*) FROM users WHERE accounting_type = '특별회계'")->fetchColumn(); $users_special = $db->query("SELECT COUNT(*) FROM users WHERE accounting_type = '특별회계'")->fetchColumn();
echo json_encode([ echo json_encode([
'laptops' => ['total' => (int) $laptop_total, 'assigned' => (int) $laptop_assigned], 'laptops' => ['total' => (int) $laptop_total, 'assigned' => (int) $laptop_assigned, 'disposed' => (int) $laptop_disposed],
'cards' => ['total' => (int) $card_total, 'assigned' => (int) $card_assigned], 'cards' => ['total' => (int) $card_total, 'assigned' => (int) $card_assigned],
'mfp' => ['total' => (int) $mfp_total, 'active' => (int) $mfp_active], 'mfp' => ['total' => (int) $mfp_total, 'active' => (int) $mfp_active],
'users' => ['general' => (int) $users_general, 'special' => (int) $users_special] 'users' => ['general' => (int) $users_general, 'special' => (int) $users_special]
@ -439,17 +440,27 @@ ORDER BY path";
case 'add_laptop_asset': case 'add_laptop_asset':
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
$asset_tag = trim($data['asset_tag'] ?? '');
// Duplicate check
$check = $db->prepare("SELECT id FROM laptop_assets WHERE asset_tag = ?");
$check->execute([$asset_tag]);
if ($check->fetch()) {
echo json_encode(['success' => false, 'error' => "이미 존재하는 자산번호({$asset_tag})입니다."]);
break;
}
$stmt = $db->prepare("INSERT INTO laptop_assets ( $stmt = $db->prepare("INSERT INTO laptop_assets (
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,
asset_status, assigned_user_name, fixed_ip, options, power_rating, manufacturer, vendor, product_name, remarks, asset_status, assigned_user_name, fixed_ip, options, power_rating, manufacturer, vendor, product_name, remarks,
last_confirmed_date last_confirmed_date, disposal_recipient, disposal_date, real_user, disposal_user_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([ $stmt->execute([
$data['asset_tag'], $data['asset_tag'],
$data['model_id'], $data['model_id'],
$data['current_user_id'] ?: null, $data['current_user_id'] ?: null,
$data['current_user_id'] ? 'assigned' : 'stock', $data['status'] ?: ($data['current_user_id'] ? 'assigned' : 'stock'),
$data['serial_number'], $data['serial_number'],
$data['purchase_date'], $data['purchase_date'],
$data['ip_address'] ?? null, $data['ip_address'] ?? null,
@ -469,7 +480,11 @@ last_confirmed_date
$data['vendor'] ?? '', $data['vendor'] ?? '',
$data['product_name'] ?? '', $data['product_name'] ?? '',
$data['remarks'] ?? '', $data['remarks'] ?? '',
$data['last_confirmed_date'] ?? null $data['last_confirmed_date'] ?? null,
$data['disposal_recipient'] ?? null,
$data['disposal_date'] ?? null,
$data['real_user'] ?? null,
$data['disposal_user_id'] ?? null
]); ]);
$new_asset_id = $db->lastInsertId(); $new_asset_id = $db->lastInsertId();
if ($data['assigned_user_name']) { if ($data['assigned_user_name']) {
@ -497,17 +512,28 @@ last_confirmed_date
$data['assigned_user_name'] = $u_stmt->fetchColumn() ?: ''; $data['assigned_user_name'] = $u_stmt->fetchColumn() ?: '';
} }
$asset_tag = trim($data['asset_tag'] ?? '');
$asset_id = $data['id'];
// Duplicate check (excluding current asset)
$check = $db->prepare("SELECT id FROM laptop_assets WHERE asset_tag = ? AND id != ?");
$check->execute([$asset_tag, $asset_id]);
if ($check->fetch()) {
echo json_encode(['success' => false, 'error' => "이미 존재하는 자산번호({$asset_tag})입니다."]);
break;
}
$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 = ?,
asset_status = ?, assigned_user_name = ?, fixed_ip = ?, options = ?, power_rating = ?, manufacturer = ?, vendor = ?, asset_status = ?, assigned_user_name = ?, fixed_ip = ?, options = ?, power_rating = ?, manufacturer = ?, vendor = ?,
product_name = ?, remarks = ?, last_confirmed_date = ? product_name = ?, remarks = ?, last_confirmed_date = ?, disposal_recipient = ?, disposal_date = ?, real_user = ?, disposal_user_id = ?
WHERE id = ?"); WHERE id = ?");
$stmt->execute([ $stmt->execute([
$data['asset_tag'], $data['asset_tag'],
$data['model_id'], $data['model_id'],
$data['current_user_id'] ?: null, $data['current_user_id'] ?: null,
$data['current_user_id'] ? 'assigned' : 'stock', $data['status'] ?: ($data['current_user_id'] ? 'assigned' : 'stock'),
$data['serial_number'], $data['serial_number'],
$data['purchase_date'], $data['purchase_date'],
$data['ip_address'] ?? null, $data['ip_address'] ?? null,
@ -528,6 +554,10 @@ WHERE id = ?");
$data['product_name'] ?? '', $data['product_name'] ?? '',
$data['remarks'] ?? '', $data['remarks'] ?? '',
$data['last_confirmed_date'] ?? null, $data['last_confirmed_date'] ?? null,
$data['disposal_recipient'] ?? null,
$data['disposal_date'] ?? null,
$data['real_user'] ?? null,
$data['disposal_user_id'] ?? null,
$data['id'] $data['id']
]); ]);
@ -642,10 +672,14 @@ LIMIT 1")->fetchColumn();
if ($newStatus) { if ($newStatus) {
$updates[] = "status = ?"; $updates[] = "status = ?";
$params[] = $newStatus; $params[] = $newStatus;
// 만약 상태를 'stock'(재고)으로 변경하는 경우 배정 정보도 초기화 // 만약 상태를 'stock'(재고)으로 변경하는 경우 '업무용'으로 설정, 'disposed'(매각)인 경우 완전 초기화
if ($newStatus === 'stock') { if ($newStatus === 'stock') {
$updates[] = "current_user_id = NULL";
$updates[] = "assigned_user_name = '업무용(TEMP_44666b)'";
} elseif ($newStatus === 'disposed') {
$updates[] = "current_user_id = NULL"; $updates[] = "current_user_id = NULL";
$updates[] = "assigned_user_name = NULL"; $updates[] = "assigned_user_name = NULL";
$updates[] = "disposal_date = '" . date('Y-m-d') . "'";
} }
} }
if ($newModelId) { if ($newModelId) {

BIN
assets.db

Binary file not shown.

View file

@ -1,11 +1,4 @@
<?php <?php
require_once 'config.php'; $db = new PDO('sqlite:d:\Docker\jasan\assets.db');
$tables = ['users', 'departments', 'laptop_assets', 'laptop_models', 'access_cards', 'mfp_accounts']; $cols = $db->query("PRAGMA table_info(laptop_assets)")->fetchAll(PDO::FETCH_ASSOC);
foreach ($tables as $table) { echo json_encode($cols, JSON_PRETTY_PRINT);
echo "--- Table: $table ---\n";
$q = $db->query("PRAGMA table_info($table)");
while ($row = $q->fetch()) {
print_r($row);
}
}
?>

View file

@ -189,18 +189,57 @@
<form @submit.prevent="submitRental" class="space-y-6 overflow-y-auto pr-2 no-scrollbar"> <form @submit.prevent="submitRental" class="space-y-6 overflow-y-auto pr-2 no-scrollbar">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div class="relative" x-on:click.outside="showUserDropdown = false">
<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.user_id" required @change="updateUserName()" <div class="relative">
class="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm"> <input type="text" x-model="userSearchQuery"
<option value="">직원 선택</option> @focus="showUserDropdown = true; userSearchQuery = ''" @input="showUserDropdown = true"
<template x-for="user in users" :key="user.id"> placeholder="직원 검색 (이름, 사번)"
<option :value="user.id" class="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm text-slate-700">
x-text="user.name + ' (' + (user.dept_name ? user.dept_name.split(' > ').pop() : '미소속') + ')'">
</option> <div class="absolute right-3 top-3.5 flex items-center gap-2">
<button type="button" x-show="userSearchQuery" @click="clearUser()"
class="text-slate-400 hover:text-slate-600">
<svg class="w-4 h-4" 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>
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<!-- User Search Dropdown -->
<div x-show="showUserDropdown" x-transition
class="absolute z-[120] mt-2 w-full bg-white rounded-[1.5rem] shadow-2xl border border-slate-100 max-h-60 overflow-y-auto no-scrollbar">
<div class="p-2 border-b border-slate-50 sticky top-0 bg-white/90 backdrop-blur-sm">
<div class="text-[9px] font-black text-slate-400 uppercase px-2 py-1">Search Results
</div>
</div>
<div class="py-1">
<template x-for="user in filteredUsers" :key="user.id">
<div @click="selectUser(user)"
class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
<div>
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600"
x-text="user.name"></div>
<div class="text-[10px] text-slate-400 font-medium"
x-text="user.dept_name || '부서 정보 없음'"></div>
</div>
<div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400"
x-text="user.emp_id"></div>
</div>
</template> </template>
</select> <div x-show="filteredUsers.length === 0" class="px-4 py-8 text-center">
<p class="text-xs text-slate-400 italic">검색 결과가 없습니다.</p>
</div>
</div>
</div>
</div> </div>
<div> <div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 시작일</label> <label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 시작일</label>
@ -276,6 +315,31 @@
rental_reason: '', rental_reason: '',
items: [''] items: ['']
}, },
userSearchQuery: '',
showUserDropdown: false,
get filteredUsers() {
if (!this.userSearchQuery) return this.users.slice(0, 50);
const q = this.userSearchQuery.toLowerCase();
return this.users.filter(u =>
(u.name && u.name.toLowerCase().includes(q)) ||
(u.emp_id && u.emp_id.toLowerCase().includes(q)) ||
(u.dept_name && u.dept_name.toLowerCase().includes(q))
);
},
selectUser(user) {
this.formData.user_id = user.id;
this.userSearchQuery = `${user.name} (${user.emp_id})`;
this.showUserDropdown = false;
this.updateUserName();
},
clearUser() {
this.formData.user_id = '';
this.userSearchQuery = '';
this.updateUserName();
},
init() { init() {
this.fetchRentals(); this.fetchRentals();
@ -315,6 +379,7 @@
rental_reason: '', rental_reason: '',
items: [''] items: ['']
}; };
this.userSearchQuery = '';
this.showAddModal = true; this.showAddModal = true;
}, },

View file

@ -65,6 +65,40 @@
</div> </div>
</header> </header>
<!-- Tabs for filtering -->
<div class="mb-6 flex items-center justify-between border-b border-slate-200">
<div class="flex gap-8">
<button @click="currentTab = 'all'"
class="pb-4 text-sm font-black transition-all relative"
:class="currentTab === 'all' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'">
전체 자산
<div x-show="currentTab === 'all'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
</button>
<button @click="currentTab = 'stock'"
class="pb-4 text-sm font-black transition-all relative"
:class="currentTab === 'stock' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'">
재고 현황
<div x-show="currentTab === 'stock'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
</button>
<button @click="currentTab = 'assigned'"
class="pb-4 text-sm font-black transition-all relative"
:class="currentTab === 'assigned' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'">
지급 완료
<div x-show="currentTab === 'assigned'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
</button>
<button @click="currentTab = 'disposed'"
class="pb-4 text-sm font-black transition-all relative"
:class="currentTab === 'disposed' ? 'text-rose-600' : 'text-slate-400 hover:text-slate-600'">
매각 자산
<div x-show="currentTab === 'disposed'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-rose-600 rounded-t-full"></div>
</button>
</div>
<div class="pb-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">
Total: <span class="text-slate-900" x-text="filteredAssets.length"></span> Items
</div>
</div>
<!-- Bulk Actions (Fixed Floating Bar) --> <!-- Bulk Actions (Fixed Floating Bar) -->
<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">
@ -83,6 +117,7 @@
<option value="">상태 변경...</option> <option value="">상태 변경...</option>
<option value="assigned">지급됨(assigned)</option> <option value="assigned">지급됨(assigned)</option>
<option value="stock">재고(stock)</option> <option value="stock">재고(stock)</option>
<option value="disposed">매각됨(disposed)</option>
</select> </select>
<select x-model="bulkModelId" <select x-model="bulkModelId"
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-[200px]"> 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-[200px]">
@ -199,6 +234,18 @@
<td class="px-6 py-4 whitespace-nowrap text-xs text-slate-500" <td class="px-6 py-4 whitespace-nowrap text-xs text-slate-500"
x-text="asset.last_confirmed_date || '-'"></td> x-text="asset.last_confirmed_date || '-'"></td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<template x-if="asset.status === 'disposed'">
<div class="flex flex-col gap-1">
<span class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter bg-rose-50 text-rose-600 border-rose-100 w-fit">매각됨</span>
<div class="text-[10px] font-bold text-slate-400 group-hover:text-slate-600">
매각: <span x-text="asset.disposal_recipient"></span><br>
실사용: <span x-text="asset.real_user || '-'"></span><br>
일자: <span x-text="asset.disposal_date"></span>
</div>
</div>
</template>
<template x-if="asset.status !== 'disposed'">
<div>
<span x-show="asset.user_name || asset.assigned_user_name" <span x-show="asset.user_name || asset.assigned_user_name"
:class="( (asset.user_name && asset.user_name.includes('업무용')) || (asset.assigned_user_name && asset.assigned_user_name.includes('업무용')) ) ? 'bg-amber-50 text-amber-600 border-amber-100' : 'bg-blue-50 text-blue-600 border-blue-100'" :class="( (asset.user_name && asset.user_name.includes('업무용')) || (asset.assigned_user_name && asset.assigned_user_name.includes('업무용')) ) ? 'bg-amber-50 text-amber-600 border-amber-100' : 'bg-blue-50 text-blue-600 border-blue-100'"
class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter" class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter"
@ -207,6 +254,8 @@
class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter bg-slate-50 text-slate-500 border-slate-100"> class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter bg-slate-50 text-slate-500 border-slate-100">
STOCK STOCK
</span> </span>
</div>
</template>
</td> </td>
</tr> </tr>
</template> </template>
@ -233,8 +282,32 @@
<div x-show="showModal" @click="showModal = false" class="fixed inset-0 modal-bg transition-opacity"></div> <div x-show="showModal" @click="showModal = false" class="fixed inset-0 modal-bg transition-opacity"></div>
<div x-show="showModal" <div x-show="showModal"
class="bg-white rounded-3xl p-8 max-w-4xl w-full relative z-[111] shadow-2xl overflow-y-auto max-h-[90vh]"> class="bg-white rounded-3xl p-8 max-w-4xl w-full relative z-[111] shadow-2xl overflow-y-auto max-h-[90vh]">
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-8">
<div class="flex items-center gap-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>
<!-- 자산 운영 상태 (버튼형 선택기) -->
<div class="flex bg-slate-100 p-1 rounded-2xl">
<button type="button" @click="setStatus('stock')"
:class="formData.status === 'stock' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full" :class="formData.status === 'stock' ? 'bg-blue-600' : 'bg-slate-300'"></div>
재고
</button>
<button type="button" @click="setStatus('assigned')"
:class="formData.status === 'assigned' ? 'bg-white text-indigo-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full" :class="formData.status === 'assigned' ? 'bg-indigo-600' : 'bg-slate-300'"></div>
직원배정
</button>
<button type="button" @click="setStatus('disposed')"
:class="formData.status === 'disposed' ? 'bg-white text-rose-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2">
<div class="w-1.5 h-1.5 rounded-full" :class="formData.status === 'disposed' ? 'bg-rose-600' : 'bg-slate-300'"></div>
매각
</button>
</div>
</div>
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600"> <button @click="showModal = false" class="text-slate-400 hover:text-slate-600">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@ -249,23 +322,71 @@
<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>
<input type="text" x-model="formData.asset_tag" required <input type="text" x-model="formData.asset_tag" required
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 font-bold"> :class="isAssetTagDuplicate ? 'border-rose-500 ring-2 ring-rose-200' : 'border-slate-200'"
class="w-full px-4 py-3 bg-slate-50 border rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all font-bold">
<template x-if="isAssetTagDuplicate">
<p class="text-[10px] text-rose-500 font-bold mt-1.5 ml-1 animate-pulse italic">이미 등록된 자산번호입니다.</p>
</template>
</div> </div>
<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.model_id" required @change="applyModelDefaults" <select x-model="formData.model_id" required @change="applyModelDefaults"
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 appearance-none"> 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 appearance-none font-bold text-sm text-slate-700">
<option value="">모델 선택</option> <option value="">모델 선택</option>
<template x-for="model in models" :key="model.id"> <template x-for="model in models" :key="model.id">
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name"> <option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name"></option>
</option>
</template> </template>
</select> </select>
</div> </div>
<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>
<input type="text" x-model="formData.serial_number" <input type="text" x-model="formData.serial_number" placeholder="S/N 입력"
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 font-bold text-sm">
</div>
</div>
<!-- 매각 정보 (상태가 매각일 때만 노출) -->
<div x-show="formData.status === 'disposed'" x-transition
class="bg-rose-50 p-6 rounded-3xl border border-rose-100 space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="relative" x-on:click.outside="showDisposalDropdown = false">
<label class="block text-[10px] font-black text-rose-400 uppercase mb-2 ml-1">매각 대상자 (사내 직원)</label>
<div class="relative">
<input type="text" x-model="disposalSearchQuery"
@focus="showDisposalDropdown = true; disposalSearchQuery = ''"
@input="showDisposalDropdown = true" placeholder="매각 받은 직원 검색"
class="w-full px-4 py-3 bg-white border border-rose-200 rounded-xl focus:ring-2 focus:ring-rose-500 outline-none text-sm font-bold text-slate-700">
<div class="absolute right-3 top-3.5">
<svg class="w-4 h-4 text-rose-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<!-- Disposal Search Dropdown -->
<div x-show="showDisposalDropdown" x-transition
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar">
<template x-for="user in filteredDisposalUsers" :key="user.id">
<div @click="selectDisposalUser(user)" class="px-4 py-3 hover:bg-rose-50 cursor-pointer flex items-center justify-between transition-colors group">
<div>
<div class="text-sm font-bold text-slate-700 group-hover:text-rose-600" x-text="user.name"></div>
<div class="text-[10px] text-slate-400 font-medium" x-text="user.dept_name || '부서 정보 없음'"></div>
</div>
<div class="text-[10px] font-black text-slate-300 group-hover:text-rose-400" x-text="user.emp_id"></div>
</div>
</template>
</div>
</div>
<div>
<label class="block text-[10px] font-black text-rose-400 uppercase mb-2 ml-1">실제 사용자</label>
<input type="text" x-model="formData.real_user" placeholder="실제 기기 사용자 입력"
class="w-full px-4 py-3 bg-white border border-rose-200 rounded-xl focus:ring-2 focus:ring-rose-500 outline-none text-sm font-bold text-slate-700">
</div>
<div>
<label class="block text-[10px] font-black text-rose-400 uppercase mb-2 ml-1">매각 날짜</label>
<input type="date" x-model="formData.disposal_date"
class="w-full px-4 py-3 bg-white border border-rose-200 rounded-xl focus:ring-2 focus:ring-rose-500 outline-none text-sm font-bold text-slate-700">
</div>
</div> </div>
</div> </div>
@ -273,46 +394,64 @@
<div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4"> <div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4">
<div class="flex items-center gap-2 mb-2"> <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="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
<h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">Individual Asset Info <h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">Individual Asset Info</h4>
</h4>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="relative" x-on:click.outside="showUserDropdown = false">
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">
배정 사용자
<span x-show="formData.status === 'assigned' && !formData.current_user_id" class="text-rose-500 ml-2 animate-pulse font-bold">[값이 없음]</span>
</label>
<div class="relative">
<input type="text" x-model="userSearchQuery"
@focus="showUserDropdown = true; userSearchQuery = ''"
@input="showUserDropdown = true" placeholder="사용자 검색 (이름, 사번)"
:disabled="formData.status !== 'assigned'"
:class="formData.status !== 'assigned' ? 'bg-slate-100 border-dashed cursor-not-allowed opacity-75' : 'bg-white'"
class="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
<div class="absolute right-3 top-3.5 flex items-center gap-2">
<button type="button" x-show="userSearchQuery && formData.status === 'assigned'" @click="clearUser()" class="text-slate-400 hover:text-slate-600">
<svg class="w-4 h-4" 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>
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<!-- User Search Dropdown -->
<div x-show="showUserDropdown" x-transition
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar">
<template x-for="user in filteredUsers" :key="user.id">
<div @click="selectUser(user)" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
<div> <div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">배정 <div class="text-sm font-bold text-slate-700 group-hover:text-blue-600" x-text="user.name"></div>
사용자</label> <div class="text-[10px] text-slate-400 font-medium" x-text="user.dept_name || '부서 정보 없음'"></div>
<select x-model="formData.current_user_id" @change="updateAssignedUserName()" </div>
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"> <div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400" x-text="user.emp_id"></div>
<option value="">미배정 (재고)</option> </div>
<template x-for="user in allUsers" :key="user.id">
<option :value="user.id" x-text="user.name + ' (' + user.emp_id + ')'"></option>
</template> </template>
</select> </div>
</div> </div>
<div> <div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP <label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP 주소</label>
주소</label>
<input type="text" x-model="formData.ip_address" placeholder="192.168.x.x" <input type="text" x-model="formData.ip_address" placeholder="192.168.x.x"
@focus="if(!formData.ip_address) formData.ip_address = '192.168.'"
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">
</div> </div>
<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>
<input type="text" x-model="formData.asset_status" placeholder="예: 정상" <input type="text" x-model="formData.asset_status" placeholder="예: 정상"
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">
</div> </div>
<div>
<label
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1 text-blue-600">실사
확인일</label>
<input type="date" x-model="formData.last_confirmed_date"
class="w-full px-4 py-3 bg-white border border-blue-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-blue-600">
</div>
</div> </div>
</div> </div>
<hr class="border-slate-100"> <hr class="border-slate-100">
<!-- 상세 사양 섹션 (모델에서 기본값 상속 가능) --> <!-- 상세 사양 섹션 -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h4 class="text-sm font-black text-slate-400 uppercase tracking-widest">Hardware Specifications</h4> <h4 class="text-sm font-black text-slate-400 uppercase tracking-widest">Hardware Specifications</h4>
<button type="button" @click="applyModelDefaults" x-show="formData.model_id" <button type="button" @click="applyModelDefaults" x-show="formData.model_id"
@ -324,18 +463,15 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">CPU</label> <label class="block text-xs font-bold text-slate-500 uppercase mb-2">CPU</label>
<input type="text" x-model="formData.cpu" <input type="text" x-model="formData.cpu" class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium">
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
</div> </div>
<div> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">NPU</label> <label class="block text-xs font-bold text-slate-500 uppercase mb-2">NPU</label>
<input type="text" x-model="formData.npu" <input type="text" x-model="formData.npu" class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium">
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
</div> </div>
<div> <div>
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">RAM</label> <label class="block text-xs font-bold text-slate-500 uppercase mb-2">RAM</label>
<input type="text" x-model="formData.ram" <input type="text" x-model="formData.ram" class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium">
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
</div> </div>
</div> </div>
@ -343,19 +479,15 @@
<div class="bg-slate-50 p-4 rounded-xl space-y-3"> <div class="bg-slate-50 p-4 rounded-xl space-y-3">
<label class="block text-[10px] font-black text-slate-400 uppercase">Storage 0</label> <label class="block text-[10px] font-black text-slate-400 uppercase">Storage 0</label>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<input type="text" x-model="formData.hdd0_model" placeholder="모델명/타입" <input type="text" x-model="formData.hdd0_model" placeholder="모델명/타입" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium"> <input type="text" x-model="formData.hdd0_capacity" placeholder="용량" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
<input type="text" x-model="formData.hdd0_capacity" placeholder="용량"
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
</div> </div>
</div> </div>
<div class="bg-slate-50 p-4 rounded-xl space-y-3"> <div class="bg-slate-50 p-4 rounded-xl space-y-3">
<label class="block text-[10px] font-black text-slate-400 uppercase">Storage 1</label> <label class="block text-[10px] font-black text-slate-400 uppercase">Storage 1</label>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<input type="text" x-model="formData.hdd1_model" placeholder="모델명/타입" <input type="text" x-model="formData.hdd1_model" placeholder="모델명/타입" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium"> <input type="text" x-model="formData.hdd1_capacity" placeholder="용량" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
<input type="text" x-model="formData.hdd1_capacity" placeholder="용량"
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
</div> </div>
</div> </div>
</div> </div>
@ -364,8 +496,7 @@
<div class="bg-slate-50 p-6 rounded-[2rem] border border-slate-100 space-y-4"> <div class="bg-slate-50 p-6 rounded-[2rem] border border-slate-100 space-y-4">
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<div class="w-1.5 h-1.5 rounded-full bg-slate-400"></div> <div class="w-1.5 h-1.5 rounded-full bg-slate-400"></div>
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">Model Base Info (Read <h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">Model Base Info (Read Only)</h4>
Only)</h4>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
@ -393,11 +524,8 @@
</div> </div>
<div class="pt-4 flex gap-4"> <div class="pt-4 flex gap-4">
<button type="button" @click="showModal = false" <button type="button" @click="showModal = false" class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold hover:bg-slate-200 transition-all">취소</button>
class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold hover:bg-slate-200 transition-all">취소</button> <button type="submit" class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all" x-text="isEdit ? '수정 완료' : '자산 등록하기'"></button>
<button type="submit"
class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all"
x-text="isEdit ? '수정 완료' : '자산 등록하기'"></button>
</div> </div>
</form> </form>
</div> </div>
@ -418,14 +546,87 @@
searchQuery: '', searchQuery: '',
sortKey: 'asset_tag', sortKey: 'asset_tag',
sortOrder: 'asc', sortOrder: 'asc',
currentTab: 'all',
formData: { formData: {
id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock', id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock',
serial_number: '', purchase_date: '', ip_address: '', serial_number: '', purchase_date: '', last_confirmed_date: '', ip_address: '',
cpu: '', npu: '', ram: '', cpu: '', npu: '', ram: '',
hdd0_model: '', hdd0_capacity: '', hdd0_model: '', hdd0_capacity: '',
hdd1_model: '', hdd1_capacity: '', hdd1_model: '', hdd1_capacity: '',
asset_status: '', assigned_user_name: '', fixed_ip: '', options: '', power_rating: '', asset_status: '', assigned_user_name: '', fixed_ip: '', options: '', power_rating: '',
manufacturer: '', vendor: '', product_name: '', remarks: '' manufacturer: '', vendor: '', product_name: '', remarks: '',
disposal_recipient: '', disposal_date: '', real_user: '', disposal_user_id: ''
},
userSearchQuery: '',
showUserDropdown: false,
disposalSearchQuery: '',
showDisposalDropdown: false,
get filteredDisposalUsers() {
if (!this.disposalSearchQuery) return this.allUsers.slice(0, 50);
const q = this.disposalSearchQuery.toLowerCase();
return this.allUsers.filter(u =>
(u.name && u.name.toLowerCase().includes(q)) ||
(u.emp_id && u.emp_id.toLowerCase().includes(q))
);
},
selectDisposalUser(user) {
this.formData.disposal_user_id = user.id;
this.formData.disposal_recipient = user.name;
this.disposalSearchQuery = `${user.name} (${user.emp_id})`;
this.showDisposalDropdown = false;
},
setStatus(status) {
const prevStatus = this.formData.status;
this.formData.status = status;
if (status === 'stock') {
this.formData.assigned_user_name = '업무용(TEMP_44666b)';
this.formData.current_user_id = '';
this.userSearchQuery = '업무용(TEMP_44666b)';
} else if (status === 'assigned') {
this.formData.assigned_user_name = '';
this.formData.current_user_id = '';
this.userSearchQuery = '';
} else if (status === 'disposed') {
if (!this.formData.disposal_date) {
this.formData.disposal_date = new Date().toISOString().split('T')[0];
}
}
},
get filteredUsers() {
if (!this.userSearchQuery) return this.allUsers.slice(0, 50);
const q = this.userSearchQuery.toLowerCase();
return this.allUsers.filter(u =>
(u.name && u.name.toLowerCase().includes(q)) ||
(u.emp_id && u.emp_id.toLowerCase().includes(q)) ||
(u.dept_name && u.dept_name.toLowerCase().includes(q))
);
},
selectUser(user) {
this.formData.current_user_id = user.id;
this.userSearchQuery = `${user.name} (${user.emp_id})`;
this.showUserDropdown = false;
this.updateAssignedUserName();
// Do not auto-change status to assigned here because user might be in stock mode
},
clearUser() {
this.formData.current_user_id = '';
this.userSearchQuery = '';
this.updateAssignedUserName();
if (this.formData.status === 'assigned') {
this.formData.status = 'stock';
}
},
get isAssetTagDuplicate() {
const tag = (this.formData.asset_tag || '').trim();
if (!tag) return false;
return this.assets.some(a => a.asset_tag === tag && a.id !== this.formData.id);
}, },
init() { init() {
@ -478,6 +679,20 @@
get filteredAssets() { get filteredAssets() {
let result = [...this.assets]; let result = [...this.assets];
// Tab filtering
if (this.currentTab === 'stock') {
result = result.filter(a => a.status === 'stock');
} else if (this.currentTab === 'assigned') {
result = result.filter(a => a.status === 'assigned');
} else if (this.currentTab === 'disposed') {
result = result.filter(a => a.status === 'disposed');
} else {
// 'all' tab usually excludes disposed unless specifically asked,
// but user said "managed separately", so let's exclude disposed from 'all'
result = result.filter(a => a.status !== 'disposed');
}
if (this.searchQuery) { if (this.searchQuery) {
const q = this.searchQuery.toLowerCase(); const q = this.searchQuery.toLowerCase();
result = result.filter(a => (a.asset_tag && a.asset_tag.toLowerCase().includes(q)) || result = result.filter(a => (a.asset_tag && a.asset_tag.toLowerCase().includes(q)) ||
@ -485,7 +700,8 @@
(a.manufacturer && a.manufacturer.toLowerCase().includes(q)) || (a.manufacturer && a.manufacturer.toLowerCase().includes(q)) ||
(a.user_name && a.user_name.toLowerCase().includes(q)) || (a.user_name && a.user_name.toLowerCase().includes(q)) ||
(a.ip_address && a.ip_address.toLowerCase().includes(q)) || (a.ip_address && a.ip_address.toLowerCase().includes(q)) ||
(a.serial_number && a.serial_number.toLowerCase().includes(q)) (a.serial_number && a.serial_number.toLowerCase().includes(q)) ||
(a.disposal_recipient && a.disposal_recipient.toLowerCase().includes(q))
); );
} }
return result; return result;
@ -544,21 +760,50 @@
this.isEdit = false; this.isEdit = false;
this.formData = { this.formData = {
id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock', id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock',
serial_number: '', purchase_date: new Date().toISOString().split('T')[0], last_confirmed_date: '', ip_address: '', serial_number: '', purchase_date: new Date().toISOString().split('T')[0], last_confirmed_date: '', ip_address: '192.168.',
cpu: '', npu: '', ram: '', hdd0_model: '', hdd0_capacity: '', hdd1_model: '', hdd1_capacity: '', cpu: '', npu: '', ram: '', hdd0_model: '', hdd0_capacity: '', hdd1_model: '', hdd1_capacity: '',
asset_status: '', assigned_user_name: '', fixed_ip: '', options: '', power_rating: '', asset_status: '', assigned_user_name: '', fixed_ip: '', options: '', power_rating: '',
manufacturer: '', vendor: '', product_name: '', remarks: '' manufacturer: '', vendor: '', product_name: '', remarks: '',
disposal_recipient: '', disposal_date: '', real_user: '', disposal_user_id: ''
}; };
this.userSearchQuery = '';
this.disposalSearchQuery = '';
this.showModal = true; this.showModal = true;
this.setStatus('stock'); // Default to 재고
}, },
editAsset(asset) { editAsset(asset) {
this.isEdit = true; this.isEdit = true;
this.formData = { ...asset }; this.formData = { ...asset };
if (!this.formData.ip_address) this.formData.ip_address = '192.168.';
if (asset.current_user_id) {
const user = this.allUsers.find(u => u.id == asset.current_user_id);
this.userSearchQuery = user ? `${user.name} (${user.emp_id})` : asset.user_name;
} else {
this.userSearchQuery = asset.assigned_user_name || '';
}
if (asset.status === 'disposed' && asset.disposal_recipient) {
this.disposalSearchQuery = asset.disposal_recipient;
} else {
this.disposalSearchQuery = '';
}
this.showModal = true; this.showModal = true;
}, },
submitAsset() { submitAsset() {
// 유효성 검사 (직원배정 시 사용자 필수)
if (this.formData.status === 'assigned' && !this.formData.current_user_id) {
window.showAlert('직원배정 상태인 경우 반드시 사용자를 선택해야 합니다.', '입력 오류', 'error');
return;
}
if (this.formData.status === 'disposed' && !this.formData.disposal_recipient) {
window.showAlert('매각 상태인 경우 매각 대상자를 입력해야 합니다.', '입력 오류', 'error');
return;
}
const action = this.isEdit ? 'update_laptop_asset' : 'add_laptop_asset'; const action = this.isEdit ? 'update_laptop_asset' : 'add_laptop_asset';
fetch(`api.php?action=${action}`, { fetch(`api.php?action=${action}`, {
method: 'POST', method: 'POST',

View file

@ -184,18 +184,57 @@
<form @submit.prevent="submitRental" class="space-y-5"> <form @submit.prevent="submitRental" class="space-y-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div class="relative" x-on:click.outside="showRentalUserDropdown = false">
<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="rentalUserId" required @change="updateRentalName()" <div class="relative">
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 font-bold text-sm"> <input type="text" x-model="rentalUserSearchQuery"
<option value="">직원 선택</option> @focus="showRentalUserDropdown = true; rentalUserSearchQuery = ''"
<template x-for="user in allUsers" :key="user.id"> @input="showRentalUserDropdown = true" placeholder="직원 검색 (이름, 사번)"
<option :value="user.id" 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 font-bold text-sm text-slate-700">
x-text="user.name + ' (' + (user.dept_name ? user.dept_name.split(' > ').pop() : '미소속') + ')'">
</option> <div class="absolute right-3 top-3.5 flex items-center gap-2">
<button type="button" x-show="rentalUserSearchQuery" @click="clearRentalUser()"
class="text-slate-400 hover:text-slate-600">
<svg class="w-4 h-4" 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>
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<!-- Rental User Search Dropdown -->
<div x-show="showRentalUserDropdown" x-transition
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto no-scrollbar">
<div class="p-2 border-b border-slate-50 sticky top-0 bg-white/90 backdrop-blur-sm">
<div class="text-[9px] font-black text-slate-400 uppercase px-2 py-1">Search Results
</div>
</div>
<div class="py-1">
<template x-for="user in filteredRentalUsers" :key="user.id">
<div @click="selectRentalUser(user)"
class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
<div>
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600"
x-text="user.name"></div>
<div class="text-[10px] text-slate-400 font-medium"
x-text="user.dept_name || '부서 정보 없음'"></div>
</div>
<div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400"
x-text="user.emp_id"></div>
</div>
</template> </template>
</select> <div x-show="filteredRentalUsers.length === 0" class="px-4 py-8 text-center">
<p class="text-xs text-slate-400 italic">검색 결과가 없습니다.</p>
</div>
</div>
</div>
</div> </div>
<div> <div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 시작일</label> <label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 시작일</label>
@ -441,6 +480,31 @@
rentalPeripherals: [], rentalPeripherals: [],
allUsers: [], allUsers: [],
history: { rental: [], assignment: [] }, history: { rental: [], assignment: [] },
rentalUserSearchQuery: '',
showRentalUserDropdown: false,
get filteredRentalUsers() {
if (!this.rentalUserSearchQuery) return this.allUsers.slice(0, 50);
const q = this.rentalUserSearchQuery.toLowerCase();
return this.allUsers.filter(u =>
(u.name && u.name.toLowerCase().includes(q)) ||
(u.emp_id && u.emp_id.toLowerCase().includes(q)) ||
(u.dept_name && u.dept_name.toLowerCase().includes(q))
);
},
selectRentalUser(user) {
this.rentalUserId = user.id;
this.rentalUserSearchQuery = `${user.name} (${user.emp_id})`;
this.showRentalUserDropdown = false;
this.updateRentalName();
},
clearRentalUser() {
this.rentalUserId = '';
this.rentalUserSearchQuery = '';
this.updateRentalName();
},
init() { init() {
this.fetchAssets(); this.fetchAssets();
@ -490,6 +554,12 @@
openRentalModal(asset) { openRentalModal(asset) {
this.selectedAsset = asset; this.selectedAsset = asset;
this.rentalUserId = asset.rental_user_id || ''; this.rentalUserId = asset.rental_user_id || '';
if (this.rentalUserId) {
const user = this.allUsers.find(u => u.id == this.rentalUserId);
this.rentalUserSearchQuery = user ? `${user.name} (${user.emp_id})` : asset.rental_user_name;
} else {
this.rentalUserSearchQuery = '';
}
this.rentalName = asset.rental_user_name || ''; this.rentalName = asset.rental_user_name || '';
this.rentalStartDate = asset.rental_start_date || new Date().toISOString().split('T')[0]; this.rentalStartDate = asset.rental_start_date || new Date().toISOString().split('T')[0];
this.rentalEndDate = asset.rental_end_scheduled || ''; this.rentalEndDate = asset.rental_end_scheduled || '';

32
update_schema_v2.php Normal file
View file

@ -0,0 +1,32 @@
<?php
try {
$db = new PDO('sqlite:d:\Docker\jasan\assets.db');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 실제 사용자 (real_user) 컬럼 추가
try {
$db->exec("ALTER TABLE laptop_assets ADD COLUMN real_user TEXT");
echo "Column 'real_user' added successfully.\n";
} catch (PDOException $e) {
if (strpos($e->getMessage(), 'duplicate column name') !== false) {
echo "Column 'real_user' already exists.\n";
} else {
throw $e;
}
}
// 매각 대상자 ID (disposal_user_id) 컬럼 추가
try {
$db->exec("ALTER TABLE laptop_assets ADD COLUMN disposal_user_id INTEGER");
echo "Column 'disposal_user_id' added successfully.\n";
} catch (PDOException $e) {
if (strpos($e->getMessage(), 'duplicate column name') !== false) {
echo "Column 'disposal_user_id' already exists.\n";
} else {
throw $e;
}
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}