업로드
This commit is contained in:
parent
a6b56d379f
commit
04b7897904
8 changed files with 563 additions and 107 deletions
17
add_disposal_columns.php
Normal file
17
add_disposal_columns.php
Normal 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
52
api.php
|
|
@ -78,8 +78,9 @@ try {
|
|||
break;
|
||||
case 'get_dashboard_stats':
|
||||
// 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_disposed = $db->query("SELECT COUNT(*) FROM laptop_assets WHERE status = 'disposed'")->fetchColumn();
|
||||
|
||||
// Cards
|
||||
$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();
|
||||
|
||||
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],
|
||||
'mfp' => ['total' => (int) $mfp_total, 'active' => (int) $mfp_active],
|
||||
'users' => ['general' => (int) $users_general, 'special' => (int) $users_special]
|
||||
|
|
@ -439,17 +440,27 @@ ORDER BY path";
|
|||
|
||||
case 'add_laptop_asset':
|
||||
$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 (
|
||||
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,
|
||||
asset_status, assigned_user_name, fixed_ip, options, power_rating, manufacturer, vendor, product_name, remarks,
|
||||
last_confirmed_date
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
last_confirmed_date, disposal_recipient, disposal_date, real_user, disposal_user_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([
|
||||
$data['asset_tag'],
|
||||
$data['model_id'],
|
||||
$data['current_user_id'] ?: null,
|
||||
$data['current_user_id'] ? 'assigned' : 'stock',
|
||||
$data['status'] ?: ($data['current_user_id'] ? 'assigned' : 'stock'),
|
||||
$data['serial_number'],
|
||||
$data['purchase_date'],
|
||||
$data['ip_address'] ?? null,
|
||||
|
|
@ -469,7 +480,11 @@ last_confirmed_date
|
|||
$data['vendor'] ?? '',
|
||||
$data['product_name'] ?? '',
|
||||
$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();
|
||||
if ($data['assigned_user_name']) {
|
||||
|
|
@ -497,17 +512,28 @@ last_confirmed_date
|
|||
$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
|
||||
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 = ?,
|
||||
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 = ?");
|
||||
$stmt->execute([
|
||||
$data['asset_tag'],
|
||||
$data['model_id'],
|
||||
$data['current_user_id'] ?: null,
|
||||
$data['current_user_id'] ? 'assigned' : 'stock',
|
||||
$data['status'] ?: ($data['current_user_id'] ? 'assigned' : 'stock'),
|
||||
$data['serial_number'],
|
||||
$data['purchase_date'],
|
||||
$data['ip_address'] ?? null,
|
||||
|
|
@ -528,6 +554,10 @@ WHERE id = ?");
|
|||
$data['product_name'] ?? '',
|
||||
$data['remarks'] ?? '',
|
||||
$data['last_confirmed_date'] ?? null,
|
||||
$data['disposal_recipient'] ?? null,
|
||||
$data['disposal_date'] ?? null,
|
||||
$data['real_user'] ?? null,
|
||||
$data['disposal_user_id'] ?? null,
|
||||
$data['id']
|
||||
]);
|
||||
|
||||
|
|
@ -642,10 +672,14 @@ LIMIT 1")->fetchColumn();
|
|||
if ($newStatus) {
|
||||
$updates[] = "status = ?";
|
||||
$params[] = $newStatus;
|
||||
// 만약 상태를 'stock'(재고)으로 변경하는 경우 배정 정보도 초기화
|
||||
// 만약 상태를 'stock'(재고)으로 변경하는 경우 '업무용'으로 설정, 'disposed'(매각)인 경우 완전 초기화
|
||||
if ($newStatus === 'stock') {
|
||||
$updates[] = "current_user_id = NULL";
|
||||
$updates[] = "assigned_user_name = '업무용(TEMP_44666b)'";
|
||||
} elseif ($newStatus === 'disposed') {
|
||||
$updates[] = "current_user_id = NULL";
|
||||
$updates[] = "assigned_user_name = NULL";
|
||||
$updates[] = "disposal_date = '" . date('Y-m-d') . "'";
|
||||
}
|
||||
}
|
||||
if ($newModelId) {
|
||||
|
|
|
|||
BIN
assets.db
BIN
assets.db
Binary file not shown.
|
|
@ -1,11 +1,4 @@
|
|||
<?php
|
||||
require_once 'config.php';
|
||||
$tables = ['users', 'departments', 'laptop_assets', 'laptop_models', 'access_cards', 'mfp_accounts'];
|
||||
foreach ($tables as $table) {
|
||||
echo "--- Table: $table ---\n";
|
||||
$q = $db->query("PRAGMA table_info($table)");
|
||||
while ($row = $q->fetch()) {
|
||||
print_r($row);
|
||||
}
|
||||
}
|
||||
?>
|
||||
$db = new PDO('sqlite:d:\Docker\jasan\assets.db');
|
||||
$cols = $db->query("PRAGMA table_info(laptop_assets)")->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode($cols, JSON_PRETTY_PRINT);
|
||||
|
|
@ -189,18 +189,57 @@
|
|||
|
||||
<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>
|
||||
<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>
|
||||
<select x-model="formData.user_id" required @change="updateUserName()"
|
||||
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">
|
||||
<option value="">직원 선택</option>
|
||||
<template x-for="user in users" :key="user.id">
|
||||
<option :value="user.id"
|
||||
x-text="user.name + ' (' + (user.dept_name ? user.dept_name.split(' > ').pop() : '미소속') + ')'">
|
||||
</option>
|
||||
<div class="relative">
|
||||
<input type="text" x-model="userSearchQuery"
|
||||
@focus="showUserDropdown = true; userSearchQuery = ''" @input="showUserDropdown = true"
|
||||
placeholder="직원 검색 (이름, 사번)"
|
||||
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">
|
||||
|
||||
<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>
|
||||
</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>
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 시작일</label>
|
||||
|
|
@ -276,6 +315,31 @@
|
|||
rental_reason: '',
|
||||
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() {
|
||||
this.fetchRentals();
|
||||
|
|
@ -315,6 +379,7 @@
|
|||
rental_reason: '',
|
||||
items: ['']
|
||||
};
|
||||
this.userSearchQuery = '';
|
||||
this.showAddModal = true;
|
||||
},
|
||||
|
||||
|
|
|
|||
357
laptops.php
357
laptops.php
|
|
@ -65,6 +65,40 @@
|
|||
</div>
|
||||
</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) -->
|
||||
<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">
|
||||
|
|
@ -83,6 +117,7 @@
|
|||
<option value="">상태 변경...</option>
|
||||
<option value="assigned">지급됨(assigned)</option>
|
||||
<option value="stock">재고(stock)</option>
|
||||
<option value="disposed">매각됨(disposed)</option>
|
||||
</select>
|
||||
<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]">
|
||||
|
|
@ -199,6 +234,18 @@
|
|||
<td class="px-6 py-4 whitespace-nowrap text-xs text-slate-500"
|
||||
x-text="asset.last_confirmed_date || '-'"></td>
|
||||
<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"
|
||||
: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"
|
||||
|
|
@ -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">
|
||||
STOCK
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</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"
|
||||
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>
|
||||
|
||||
<!-- 자산 운영 상태 (버튼형 선택기) -->
|
||||
<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">
|
||||
<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"
|
||||
|
|
@ -249,23 +322,71 @@
|
|||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">자산관리번호</label>
|
||||
<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>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 모델 연동</label>
|
||||
<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>
|
||||
<template x-for="model in models" :key="model.id">
|
||||
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name">
|
||||
</option>
|
||||
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">시리얼 번호</label>
|
||||
<input type="text" x-model="formData.serial_number"
|
||||
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">
|
||||
<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 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>
|
||||
|
||||
|
|
@ -273,46 +394,64 @@
|
|||
<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="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>
|
||||
<h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">Individual Asset Info</h4>
|
||||
</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>
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">배정
|
||||
사용자</label>
|
||||
<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">
|
||||
<option value="">미배정 (재고)</option>
|
||||
<template x-for="user in allUsers" :key="user.id">
|
||||
<option :value="user.id" x-text="user.name + ' (' + user.emp_id + ')'"></option>
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP
|
||||
주소</label>
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP 주소</label>
|
||||
<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">
|
||||
</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>
|
||||
<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">
|
||||
</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>
|
||||
|
||||
<hr class="border-slate-100">
|
||||
|
||||
<!-- 상세 사양 섹션 (모델에서 기본값 상속 가능) -->
|
||||
<!-- 상세 사양 섹션 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<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"
|
||||
|
|
@ -324,18 +463,15 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">CPU</label>
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">NPU</label>
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">RAM</label>
|
||||
<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">
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -343,19 +479,15 @@
|
|||
<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>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<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">
|
||||
<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_model" 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 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>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<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">
|
||||
<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_model" 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>
|
||||
|
|
@ -364,8 +496,7 @@
|
|||
<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="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
|
||||
Only)</h4>
|
||||
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">Model Base Info (Read Only)</h4>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
|
|
@ -393,11 +524,8 @@
|
|||
</div>
|
||||
|
||||
<div class="pt-4 flex gap-4">
|
||||
<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>
|
||||
<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="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>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -418,14 +546,87 @@
|
|||
searchQuery: '',
|
||||
sortKey: 'asset_tag',
|
||||
sortOrder: 'asc',
|
||||
currentTab: 'all',
|
||||
formData: {
|
||||
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: '',
|
||||
hdd0_model: '', hdd0_capacity: '',
|
||||
hdd1_model: '', hdd1_capacity: '',
|
||||
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() {
|
||||
|
|
@ -478,6 +679,20 @@
|
|||
|
||||
get filteredAssets() {
|
||||
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) {
|
||||
const q = this.searchQuery.toLowerCase();
|
||||
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.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))
|
||||
(a.serial_number && a.serial_number.toLowerCase().includes(q)) ||
|
||||
(a.disposal_recipient && a.disposal_recipient.toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
return result;
|
||||
|
|
@ -544,21 +760,50 @@
|
|||
this.isEdit = false;
|
||||
this.formData = {
|
||||
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: '',
|
||||
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.setStatus('stock'); // Default to 재고
|
||||
},
|
||||
|
||||
editAsset(asset) {
|
||||
this.isEdit = true;
|
||||
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;
|
||||
},
|
||||
|
||||
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';
|
||||
fetch(`api.php?action=${action}`, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
88
rental.php
88
rental.php
|
|
@ -184,18 +184,57 @@
|
|||
|
||||
<form @submit.prevent="submitRental" class="space-y-5">
|
||||
<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>
|
||||
<select x-model="rentalUserId" required @change="updateRentalName()"
|
||||
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">
|
||||
<option value="">직원 선택</option>
|
||||
<template x-for="user in allUsers" :key="user.id">
|
||||
<option :value="user.id"
|
||||
x-text="user.name + ' (' + (user.dept_name ? user.dept_name.split(' > ').pop() : '미소속') + ')'">
|
||||
</option>
|
||||
<div class="relative">
|
||||
<input type="text" x-model="rentalUserSearchQuery"
|
||||
@focus="showRentalUserDropdown = true; rentalUserSearchQuery = ''"
|
||||
@input="showRentalUserDropdown = true" placeholder="직원 검색 (이름, 사번)"
|
||||
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">
|
||||
|
||||
<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>
|
||||
</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>
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 시작일</label>
|
||||
|
|
@ -441,6 +480,31 @@
|
|||
rentalPeripherals: [],
|
||||
allUsers: [],
|
||||
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() {
|
||||
this.fetchAssets();
|
||||
|
|
@ -490,6 +554,12 @@
|
|||
openRentalModal(asset) {
|
||||
this.selectedAsset = asset;
|
||||
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.rentalStartDate = asset.rental_start_date || new Date().toISOString().split('T')[0];
|
||||
this.rentalEndDate = asset.rental_end_scheduled || '';
|
||||
|
|
|
|||
32
update_schema_v2.php
Normal file
32
update_schema_v2.php
Normal 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";
|
||||
}
|
||||
Loading…
Reference in a new issue