오호
This commit is contained in:
parent
82191ed161
commit
9584156f9a
8 changed files with 1618 additions and 190 deletions
298
api.php
298
api.php
|
|
@ -101,6 +101,37 @@ try {
|
||||||
echo json_encode($db->query($query)->fetchAll());
|
echo json_encode($db->query($query)->fetchAll());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'get_rental_laptops':
|
||||||
|
$query = "SELECT a.*, m.model_name, m.manufacturer, m.product_name
|
||||||
|
FROM laptop_assets a
|
||||||
|
LEFT JOIN laptop_models m ON a.model_id = m.id
|
||||||
|
WHERE a.assigned_user_name LIKE '%업무용%'";
|
||||||
|
echo json_encode($db->query($query)->fetchAll());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_rental':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$stmt = $db->prepare("UPDATE laptop_assets SET rental_user_name = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$data['rental_user_name'], $data['id']]);
|
||||||
|
|
||||||
|
// Log the rental action
|
||||||
|
if ($data['rental_user_name']) {
|
||||||
|
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'rental', ?, ?)");
|
||||||
|
$log_stmt->execute([$data['id'], $data['rental_user_name'], date('Y-m-d')]);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_asset_history':
|
||||||
|
$asset_id = $_GET['asset_id'];
|
||||||
|
$rental_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'rental' ORDER BY id DESC LIMIT 20")->fetchAll();
|
||||||
|
$assign_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'assignment' ORDER BY id DESC LIMIT 20")->fetchAll();
|
||||||
|
echo json_encode([
|
||||||
|
'rental' => $rental_logs,
|
||||||
|
'assignment' => $assign_logs
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'get_cards':
|
case 'get_cards':
|
||||||
echo json_encode($db->query("SELECT * FROM access_cards")->fetchAll());
|
echo json_encode($db->query("SELECT * FROM access_cards")->fetchAll());
|
||||||
break;
|
break;
|
||||||
|
|
@ -143,15 +174,66 @@ try {
|
||||||
|
|
||||||
case 'add_model':
|
case 'add_model':
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
$stmt = $db->prepare("INSERT INTO laptop_models (model_name, manufacturer, specs) VALUES (?, ?, ?)");
|
$stmt = $db->prepare("INSERT INTO laptop_models (
|
||||||
$stmt->execute([$data['model_name'], $data['manufacturer'], $data['specs'] ?? '']);
|
model_name, manufacturer, specs, cpu, npu, hdd0_model, hdd0_capacity,
|
||||||
|
hdd1_model, hdd1_capacity, ram, asset_status, assigned_user, fixed_ip,
|
||||||
|
remarks, options, power_rating, purchase_date, vendor, product_name
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([
|
||||||
|
$data['model_name'],
|
||||||
|
$data['manufacturer'],
|
||||||
|
$data['specs'] ?? '',
|
||||||
|
$data['cpu'] ?? '',
|
||||||
|
$data['npu'] ?? '',
|
||||||
|
$data['hdd0_model'] ?? '',
|
||||||
|
$data['hdd0_capacity'] ?? '',
|
||||||
|
$data['hdd1_model'] ?? '',
|
||||||
|
$data['hdd1_capacity'] ?? '',
|
||||||
|
$data['ram'] ?? '',
|
||||||
|
$data['asset_status'] ?? '',
|
||||||
|
$data['assigned_user'] ?? '',
|
||||||
|
$data['fixed_ip'] ?? '',
|
||||||
|
$data['remarks'] ?? '',
|
||||||
|
$data['options'] ?? '',
|
||||||
|
$data['power_rating'] ?? '',
|
||||||
|
$data['purchase_date'] ?? '',
|
||||||
|
$data['vendor'] ?? '',
|
||||||
|
$data['product_name'] ?? ''
|
||||||
|
]);
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'update_model':
|
case 'update_model':
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
$stmt = $db->prepare("UPDATE laptop_models SET model_name = ?, manufacturer = ?, specs = ? WHERE id = ?");
|
$stmt = $db->prepare("UPDATE laptop_models SET
|
||||||
$stmt->execute([$data['model_name'], $data['manufacturer'], $data['specs'] ?? '', $data['id']]);
|
model_name = ?, manufacturer = ?, specs = ?, cpu = ?, npu = ?,
|
||||||
|
hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?,
|
||||||
|
ram = ?, asset_status = ?, assigned_user = ?, fixed_ip = ?,
|
||||||
|
remarks = ?, options = ?, power_rating = ?, purchase_date = ?,
|
||||||
|
vendor = ?, product_name = ?
|
||||||
|
WHERE id = ?");
|
||||||
|
$stmt->execute([
|
||||||
|
$data['model_name'],
|
||||||
|
$data['manufacturer'],
|
||||||
|
$data['specs'] ?? '',
|
||||||
|
$data['cpu'] ?? '',
|
||||||
|
$data['npu'] ?? '',
|
||||||
|
$data['hdd0_model'] ?? '',
|
||||||
|
$data['hdd0_capacity'] ?? '',
|
||||||
|
$data['hdd1_model'] ?? '',
|
||||||
|
$data['hdd1_capacity'] ?? '',
|
||||||
|
$data['ram'] ?? '',
|
||||||
|
$data['asset_status'] ?? '',
|
||||||
|
$data['assigned_user'] ?? '',
|
||||||
|
$data['fixed_ip'] ?? '',
|
||||||
|
$data['remarks'] ?? '',
|
||||||
|
$data['options'] ?? '',
|
||||||
|
$data['power_rating'] ?? '',
|
||||||
|
$data['purchase_date'] ?? '',
|
||||||
|
$data['vendor'] ?? '',
|
||||||
|
$data['product_name'] ?? '',
|
||||||
|
$data['id']
|
||||||
|
]);
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -172,22 +254,11 @@ try {
|
||||||
|
|
||||||
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);
|
||||||
$stmt = $db->prepare("INSERT INTO laptop_assets (asset_tag, model_id, current_user_id, status, serial_number, purchase_date, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
$stmt = $db->prepare("INSERT INTO laptop_assets (
|
||||||
$stmt->execute([
|
asset_tag, model_id, current_user_id, status, serial_number, purchase_date, ip_address,
|
||||||
$data['asset_tag'],
|
cpu, npu, hdd0_model, hdd0_capacity, hdd1_model, hdd1_capacity, ram,
|
||||||
$data['model_id'],
|
asset_status, assigned_user_name, fixed_ip, options, power_rating, manufacturer, vendor, product_name, remarks, last_confirmed_date
|
||||||
$data['current_user_id'] ?: null,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$data['current_user_id'] ? 'assigned' : 'stock',
|
|
||||||
$data['serial_number'],
|
|
||||||
$data['purchase_date'],
|
|
||||||
$data['ip_address'] ?? null
|
|
||||||
]);
|
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'update_laptop_asset':
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$stmt = $db->prepare("UPDATE laptop_assets SET asset_tag = ?, model_id = ?, current_user_id = ?, status = ?, serial_number = ?, purchase_date = ?, ip_address = ? WHERE id = ?");
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$data['asset_tag'],
|
$data['asset_tag'],
|
||||||
$data['model_id'],
|
$data['model_id'],
|
||||||
|
|
@ -196,8 +267,79 @@ try {
|
||||||
$data['serial_number'],
|
$data['serial_number'],
|
||||||
$data['purchase_date'],
|
$data['purchase_date'],
|
||||||
$data['ip_address'] ?? null,
|
$data['ip_address'] ?? null,
|
||||||
|
$data['cpu'] ?? '',
|
||||||
|
$data['npu'] ?? '',
|
||||||
|
$data['hdd0_model'] ?? '',
|
||||||
|
$data['hdd0_capacity'] ?? '',
|
||||||
|
$data['hdd1_model'] ?? '',
|
||||||
|
$data['hdd1_capacity'] ?? '',
|
||||||
|
$data['ram'] ?? '',
|
||||||
|
$data['asset_status'] ?? '',
|
||||||
|
$data['assigned_user_name'] ?? '',
|
||||||
|
$data['fixed_ip'] ?? '',
|
||||||
|
$data['options'] ?? '',
|
||||||
|
$data['power_rating'] ?? '',
|
||||||
|
$data['manufacturer'] ?? '',
|
||||||
|
$data['vendor'] ?? '',
|
||||||
|
$data['product_name'] ?? '',
|
||||||
|
$data['remarks'] ?? '',
|
||||||
|
$data['last_confirmed_date'] ?? null
|
||||||
|
]);
|
||||||
|
$new_asset_id = $db->lastInsertId();
|
||||||
|
if ($data['assigned_user_name']) {
|
||||||
|
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'assignment', ?, ?)");
|
||||||
|
$log_stmt->execute([$new_asset_id, $data['assigned_user_name'], date('Y-m-d')]);
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_laptop_asset':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
// Get old data for logging comparison
|
||||||
|
$old_stmt = $db->prepare("SELECT assigned_user_name FROM laptop_assets WHERE id = ?");
|
||||||
|
$old_stmt->execute([$data['id']]);
|
||||||
|
$old_asset = $old_stmt->fetch();
|
||||||
|
|
||||||
|
$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 = ?
|
||||||
|
WHERE id = ?");
|
||||||
|
$stmt->execute([
|
||||||
|
$data['asset_tag'],
|
||||||
|
$data['model_id'],
|
||||||
|
$data['current_user_id'] ?: null,
|
||||||
|
$data['current_user_id'] ? 'assigned' : 'stock',
|
||||||
|
$data['serial_number'],
|
||||||
|
$data['purchase_date'],
|
||||||
|
$data['ip_address'] ?? null,
|
||||||
|
$data['cpu'] ?? '',
|
||||||
|
$data['npu'] ?? '',
|
||||||
|
$data['hdd0_model'] ?? '',
|
||||||
|
$data['hdd0_capacity'] ?? '',
|
||||||
|
$data['hdd1_model'] ?? '',
|
||||||
|
$data['hdd1_capacity'] ?? '',
|
||||||
|
$data['ram'] ?? '',
|
||||||
|
$data['asset_status'] ?? '',
|
||||||
|
$data['assigned_user_name'] ?? '',
|
||||||
|
$data['fixed_ip'] ?? '',
|
||||||
|
$data['options'] ?? '',
|
||||||
|
$data['power_rating'] ?? '',
|
||||||
|
$data['manufacturer'] ?? '',
|
||||||
|
$data['vendor'] ?? '',
|
||||||
|
$data['product_name'] ?? '',
|
||||||
|
$data['remarks'] ?? '',
|
||||||
|
$data['last_confirmed_date'] ?? null,
|
||||||
$data['id']
|
$data['id']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Auto-log assignment change
|
||||||
|
if ($data['assigned_user_name'] && $data['assigned_user_name'] !== ($old_asset['assigned_user_name'] ?? '')) {
|
||||||
|
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'assignment', ?, ?)");
|
||||||
|
$log_stmt->execute([$data['id'], $data['assigned_user_name'], date('Y-m-d')]);
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -231,6 +373,122 @@ try {
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'bulk_update_laptops':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$ids = $data['ids'];
|
||||||
|
$newStatus = $data['status'] ?? null;
|
||||||
|
$newModelId = $data['model_id'] ?? null;
|
||||||
|
if (empty($ids)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No items selected']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$updates = [];
|
||||||
|
$params = [];
|
||||||
|
if ($newStatus) {
|
||||||
|
$updates[] = "status = ?";
|
||||||
|
$params[] = $newStatus;
|
||||||
|
}
|
||||||
|
if ($newModelId) {
|
||||||
|
$updates[] = "model_id = ?";
|
||||||
|
$params[] = $newModelId;
|
||||||
|
}
|
||||||
|
if (empty($updates)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$sql = "UPDATE laptop_assets SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute(array_merge($params, $ids));
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bulk_update_cards':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$ids = $data['ids'];
|
||||||
|
$newStatus = $data['status'] ?? null;
|
||||||
|
$newCardType = $data['card_type'] ?? null;
|
||||||
|
if (empty($ids)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No items selected']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$updates = [];
|
||||||
|
$params = [];
|
||||||
|
if ($newStatus) {
|
||||||
|
$updates[] = "status = ?";
|
||||||
|
$params[] = $newStatus;
|
||||||
|
}
|
||||||
|
if ($newCardType) {
|
||||||
|
$updates[] = "card_type = ?";
|
||||||
|
$params[] = $newCardType;
|
||||||
|
}
|
||||||
|
if (empty($updates)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$sql = "UPDATE access_cards SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute(array_merge($params, $ids));
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bulk_update_mfp':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$ids = $data['ids'];
|
||||||
|
$newStatus = $data['status'] ?? null;
|
||||||
|
$newPurpose = $data['purpose'] ?? null;
|
||||||
|
if (empty($ids)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No items selected']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$updates = [];
|
||||||
|
$params = [];
|
||||||
|
if ($newStatus) {
|
||||||
|
$updates[] = "status = ?";
|
||||||
|
$params[] = $newStatus;
|
||||||
|
}
|
||||||
|
if ($newPurpose) {
|
||||||
|
$updates[] = "purpose = ?";
|
||||||
|
$params[] = $newPurpose;
|
||||||
|
}
|
||||||
|
if (empty($updates)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$sql = "UPDATE mfp_accounts SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute(array_merge($params, $ids));
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'bulk_update_models':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$ids = $data['ids'];
|
||||||
|
$newManufacturer = $data['manufacturer'] ?? null;
|
||||||
|
$newProductName = $data['product_name'] ?? null;
|
||||||
|
if (empty($ids)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No items selected']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$updates = [];
|
||||||
|
$params = [];
|
||||||
|
if ($newManufacturer) {
|
||||||
|
$updates[] = "manufacturer = ?";
|
||||||
|
$params[] = $newManufacturer;
|
||||||
|
}
|
||||||
|
if ($newProductName) {
|
||||||
|
$updates[] = "product_name = ?";
|
||||||
|
$params[] = $newProductName;
|
||||||
|
}
|
||||||
|
if (empty($updates)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$sql = "UPDATE laptop_models SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute(array_merge($params, $ids));
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'bulk_update_users':
|
case 'bulk_update_users':
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
$userIds = $data['user_ids'];
|
$userIds = $data['user_ids'];
|
||||||
|
|
|
||||||
85
cards.php
85
cards.php
|
|
@ -31,16 +31,51 @@
|
||||||
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">출입증 관리</h2>
|
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">출입증 관리</h2>
|
||||||
<p class="text-slate-500 mt-1">보안실 연동 임시 출입증 및 NFC ID 관리</p>
|
<p class="text-slate-500 mt-1">보안실 연동 임시 출입증 및 NFC ID 관리</p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="showModal = true"
|
<button
|
||||||
|
@click="showModal = true; isEdit = false; formData = { id: '', card_number: '', card_type: '', nfc_id: '' }"
|
||||||
class="bg-emerald-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-emerald-200 hover:bg-emerald-700 transition-all">신규
|
class="bg-emerald-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-emerald-200 hover:bg-emerald-700 transition-all">신규
|
||||||
카드 등록</button>
|
카드 등록</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Bulk Actions -->
|
||||||
|
<div x-show="selectedIds.length > 0" x-transition x-cloak
|
||||||
|
class="mb-6 bg-slate-900 text-white px-6 py-3 rounded-2xl flex items-center justify-between shadow-xl">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-emerald-400 font-black" x-text="selectedIds.length"></span>
|
||||||
|
<span class="text-xs font-bold text-slate-400">개 선택됨</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-4 w-px bg-slate-700"></div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-xs font-bold text-slate-400 uppercase">일괄 변경:</span>
|
||||||
|
<select x-model="bulkStatus"
|
||||||
|
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-emerald-500">
|
||||||
|
<option value="">상태 변경...</option>
|
||||||
|
<option value="available">가용됨(available)</option>
|
||||||
|
<option value="in_use">사용중(in_use)</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" x-model="bulkCardType" placeholder="카드 종류 일괄 입력..."
|
||||||
|
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-emerald-500 w-48">
|
||||||
|
<button @click="applyBulkUpdate"
|
||||||
|
class="bg-emerald-500 hover:bg-emerald-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="selectedIds = []" class="text-slate-400 hover:text-white transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Card List Table -->
|
<!-- Card List Table -->
|
||||||
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden" x-show="cards.length > 0">
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden" x-show="cards.length > 0">
|
||||||
<table class="min-w-full divide-y divide-slate-100">
|
<table class="min-w-full divide-y divide-slate-100">
|
||||||
<thead class="bg-slate-50/50">
|
<thead class="bg-slate-50/50">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class="px-6 py-4 text-left w-10">
|
||||||
|
<input type="checkbox" @change="toggleSelectAll($event.target.checked)"
|
||||||
|
class="w-4 h-4 rounded border-slate-300 text-emerald-600 focus:ring-emerald-500">
|
||||||
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">카드 번호
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">카드 번호
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">카드 종류
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">카드 종류
|
||||||
|
|
@ -52,19 +87,23 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-slate-50">
|
<tbody class="divide-y divide-slate-50">
|
||||||
<template x-for="card in cards" :key="card.card_number">
|
<template x-for="card in cards" :key="card.id">
|
||||||
<tr @click="editCard(card)" class="hover:bg-slate-50/80 transition-colors cursor-pointer group">
|
<tr class="hover:bg-slate-50/80 transition-colors group">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4">
|
||||||
|
<input type="checkbox" :value="card.id" x-model="selectedIds"
|
||||||
|
class="w-4 h-4 rounded border-slate-300 text-emerald-600 focus:ring-emerald-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editCard(card)">
|
||||||
<span class="text-sm font-black text-emerald-600 px-2.5 py-1 bg-emerald-50 rounded-lg"
|
<span class="text-sm font-black text-emerald-600 px-2.5 py-1 bg-emerald-50 rounded-lg"
|
||||||
x-text="card.card_number"></span>
|
x-text="card.card_number"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editCard(card)">
|
||||||
<div class="text-sm font-bold text-slate-900" x-text="card.card_type"></div>
|
<div class="text-sm font-bold text-slate-900" x-text="card.card_type"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editCard(card)">
|
||||||
<div class="text-xs font-mono text-slate-500" x-text="card.nfc_id"></div>
|
<div class="text-xs font-mono text-slate-500" x-text="card.nfc_id"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editCard(card)">
|
||||||
<span
|
<span
|
||||||
:class="card.status === 'available' ? 'bg-emerald-100 text-emerald-600' : 'bg-amber-100 text-amber-600'"
|
:class="card.status === 'available' ? 'bg-emerald-100 text-emerald-600' : 'bg-amber-100 text-amber-600'"
|
||||||
class="px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-tighter"
|
class="px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-tighter"
|
||||||
|
|
@ -106,7 +145,13 @@
|
||||||
<script>
|
<script>
|
||||||
function cardManagement() {
|
function cardManagement() {
|
||||||
return {
|
return {
|
||||||
cards: [], showModal: false, isEdit: false, formData: { id: '', card_number: '', card_type: '', nfc_id: '' },
|
cards: [],
|
||||||
|
showModal: false,
|
||||||
|
isEdit: false,
|
||||||
|
selectedIds: [],
|
||||||
|
bulkStatus: '',
|
||||||
|
bulkCardType: '',
|
||||||
|
formData: { id: '', card_number: '', card_type: '', nfc_id: '' },
|
||||||
init() { this.fetchCards(); },
|
init() { this.fetchCards(); },
|
||||||
fetchCards() {
|
fetchCards() {
|
||||||
const scrollPos = window.scrollY;
|
const scrollPos = window.scrollY;
|
||||||
|
|
@ -115,6 +160,30 @@
|
||||||
this.$nextTick(() => window.scrollTo(0, scrollPos));
|
this.$nextTick(() => window.scrollTo(0, scrollPos));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
toggleSelectAll(checked) {
|
||||||
|
this.selectedIds = checked ? this.cards.map(c => c.id) : [];
|
||||||
|
},
|
||||||
|
applyBulkUpdate() {
|
||||||
|
if (this.selectedIds.length === 0) return;
|
||||||
|
if (!this.bulkStatus && !this.bulkCardType) { alert('변경할 항목을 선택해주세요.'); return; }
|
||||||
|
|
||||||
|
fetch('api.php?action=bulk_update_cards', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ids: this.selectedIds,
|
||||||
|
status: this.bulkStatus,
|
||||||
|
card_type: this.bulkCardType
|
||||||
|
})
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.selectedIds = [];
|
||||||
|
this.bulkStatus = '';
|
||||||
|
this.bulkCardType = '';
|
||||||
|
this.fetchCards();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
editCard(card) {
|
editCard(card) {
|
||||||
this.isEdit = true;
|
this.isEdit = true;
|
||||||
this.formData = { ...card };
|
this.formData = { ...card };
|
||||||
|
|
|
||||||
90
db_init.php
90
db_init.php
|
|
@ -201,20 +201,49 @@ try {
|
||||||
echo "<p class='success'>✔ " . count($users_raw) . " users processed.</p>";
|
echo "<p class='success'>✔ " . count($users_raw) . " users processed.</p>";
|
||||||
|
|
||||||
// 5. 노트북 모델 & 자산 등록
|
// 5. 노트북 모델 & 자산 등록
|
||||||
echo "<p class='info'>⏳ Processing Laptop Assets...</p>";
|
echo "<p class='info'>⏳ Processing Laptop Assets with Detailed Specs...</p>";
|
||||||
|
|
||||||
// 테이블 스키마에 스펙 컬럼이 없다면 추가 (최초 1회만)
|
// 테이블 스키마 상세 사양 컬럼 확장
|
||||||
try {
|
$spec_columns = [
|
||||||
$db->exec("ALTER TABLE laptop_assets ADD COLUMN cpu TEXT");
|
'cpu' => 'TEXT',
|
||||||
$db->exec("ALTER TABLE laptop_assets ADD COLUMN ram TEXT");
|
'npu' => 'TEXT',
|
||||||
$db->exec("ALTER TABLE laptop_assets ADD COLUMN storage TEXT");
|
'hdd0_model' => 'TEXT',
|
||||||
} catch (Exception $e) { /* 이미 존재하면 무시 */
|
'hdd0_capacity' => 'TEXT',
|
||||||
|
'hdd1_model' => 'TEXT',
|
||||||
|
'hdd1_capacity' => 'TEXT',
|
||||||
|
'ram' => 'TEXT',
|
||||||
|
'asset_status' => 'TEXT',
|
||||||
|
'assigned_user_name' => 'TEXT',
|
||||||
|
'fixed_ip' => 'TEXT',
|
||||||
|
'options' => 'TEXT',
|
||||||
|
'power_rating' => 'TEXT',
|
||||||
|
'manufacturer' => 'TEXT',
|
||||||
|
'vendor' => 'TEXT',
|
||||||
|
'product_name' => 'TEXT'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($spec_columns as $col => $type) {
|
||||||
|
try {
|
||||||
|
$db->exec("ALTER TABLE laptop_assets ADD COLUMN $col $type");
|
||||||
|
} catch (Exception $e) { /* 컬럼 이미 존재 시 무시 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$laptops_raw = getCSV($files['laptops']);
|
$laptops_raw = getCSV($files['laptops']);
|
||||||
$model_map = [];
|
$model_map = [];
|
||||||
$model_stmt = $db->prepare("INSERT INTO laptop_models (model_name, manufacturer) VALUES (?, ?)");
|
$model_stmt = $db->prepare("INSERT INTO laptop_models (model_name, manufacturer) VALUES (?, ?)");
|
||||||
$asset_stmt = $db->prepare("INSERT INTO laptop_assets (asset_tag, model_id, current_user_id, status, serial_number, purchase_date, cpu, ram, storage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
||||||
|
// 확장된 INSERT 문
|
||||||
|
$asset_sql = "INSERT INTO laptop_assets (
|
||||||
|
asset_tag, model_id, current_user_id, status, serial_number, purchase_date, remarks,
|
||||||
|
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
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?, ?, ?, ?,
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?
|
||||||
|
)";
|
||||||
|
$asset_stmt = $db->prepare($asset_sql);
|
||||||
|
|
||||||
foreach ($laptops_raw as $row) {
|
foreach ($laptops_raw as $row) {
|
||||||
$model_name = trim($row['자산명'] ?? $row['모델명'] ?? 'Unknown Model');
|
$model_name = trim($row['자산명'] ?? $row['모델명'] ?? 'Unknown Model');
|
||||||
|
|
@ -233,22 +262,20 @@ try {
|
||||||
if (count($candidates) === 1) {
|
if (count($candidates) === 1) {
|
||||||
$current_user_id = $candidates[0]['id'];
|
$current_user_id = $candidates[0]['id'];
|
||||||
} else {
|
} else {
|
||||||
// 사번으로 매칭 시도
|
|
||||||
foreach ($candidates as $c) {
|
foreach ($candidates as $c) {
|
||||||
if ($raw_emp_id && $c['emp_id'] === $raw_emp_id) {
|
if ($raw_emp_id && $c['emp_id'] === $raw_emp_id) {
|
||||||
$current_user_id = $c['id'];
|
$current_user_id = $c['id'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 사번 매칭 실패시 첫 번째 후보 (또는 로직 고도화 가능)
|
|
||||||
if (!$current_user_id)
|
if (!$current_user_id)
|
||||||
$current_user_id = $candidates[0]['id'];
|
$current_user_id = $candidates[0]['id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$status = $current_user_id ? 'assigned' : 'stock';
|
$internal_status = $current_user_id ? 'assigned' : 'stock';
|
||||||
if (strpos($user_name, '업무용') !== false || strpos($user_name, '임시') !== false) {
|
if (strpos($user_name, '업무용') !== false || strpos($user_name, '임시') !== false) {
|
||||||
$status = 'stock'; // 공용/업무용은 재고로 간주하거나 별도 상태 부여 가능
|
$internal_status = 'stock';
|
||||||
}
|
}
|
||||||
|
|
||||||
$p_date = trim($row['취득일자'] ?? '');
|
$p_date = trim($row['취득일자'] ?? '');
|
||||||
|
|
@ -258,24 +285,49 @@ try {
|
||||||
$p_date = "{$parts[2]}-{$parts[0]}-{$parts[1]}";
|
$p_date = "{$parts[2]}-{$parts[0]}-{$parts[1]}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 스펙 정보 추출
|
// 상세 사양 매핑
|
||||||
$cpu = $row['CPU'] ?? $row['프로세서'] ?? '';
|
$cpu = $row['CPU'] ?? $row['프로세서'] ?? '';
|
||||||
$ram = $row['RAM'] ?? '';
|
$npu = $row['NPU'] ?? '';
|
||||||
$storage = $row['HDD 0 / 설치 값'] ?? '';
|
$hdd0_model = $row['HDD 0 / 타입'] ?? '';
|
||||||
|
$hdd0_capacity = $row['HDD 0 / 설치 값'] ?? '';
|
||||||
|
$hdd1_model = $row['HDD 1 / 타입'] ?? '';
|
||||||
|
$hdd1_capacity = $row['HDD 1 / 설치 값'] ?? '';
|
||||||
|
$ram_val = $row['RAM'] ?? '';
|
||||||
|
$asset_status = $row['자산상태'] ?? '';
|
||||||
|
$assigned_user_name = $row['배정 사용자'] ?? '';
|
||||||
|
$fixed_ip = $row['고정IP'] ?? $row['고정 IP'] ?? '';
|
||||||
|
$options = $row['옵션'] ?? '';
|
||||||
|
$power_rating = $row['정격입력/출력'] ?? '';
|
||||||
|
$vendor = $row['판매사'] ?? '';
|
||||||
|
$product_name = $row['품명'] ?? '';
|
||||||
|
$remarks = $row['비고'] ?? $row['특기사항'] ?? '';
|
||||||
|
|
||||||
$asset_stmt->execute([
|
$asset_stmt->execute([
|
||||||
$row['자산관리번호'],
|
$row['자산관리번호'],
|
||||||
$model_map[$model_name],
|
$model_map[$model_name],
|
||||||
$current_user_id,
|
$current_user_id,
|
||||||
$status,
|
$internal_status,
|
||||||
$row['시리얼넘버/EX'],
|
$row['시리얼넘버/EX'],
|
||||||
$p_date,
|
$p_date,
|
||||||
|
$remarks,
|
||||||
$cpu,
|
$cpu,
|
||||||
$ram,
|
$npu,
|
||||||
$storage
|
$hdd0_model,
|
||||||
|
$hdd0_capacity,
|
||||||
|
$hdd1_model,
|
||||||
|
$hdd1_capacity,
|
||||||
|
$ram_val,
|
||||||
|
$asset_status,
|
||||||
|
$assigned_user_name,
|
||||||
|
$fixed_ip,
|
||||||
|
$options,
|
||||||
|
$power_rating,
|
||||||
|
$manufacturer,
|
||||||
|
$vendor,
|
||||||
|
$product_name
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
echo "<p class='success'>✔ Laptop assets imported with specs.</p>";
|
echo "<p class='success'>✔ Laptop assets imported with all detailed specs.</p>";
|
||||||
|
|
||||||
// 6. 기타 계정 정보
|
// 6. 기타 계정 정보
|
||||||
$mfp_raw = getCSV($files['mfp']);
|
$mfp_raw = getCSV($files['mfp']);
|
||||||
|
|
|
||||||
380
laptops.php
380
laptops.php
|
|
@ -52,42 +52,125 @@
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Bulk Actions -->
|
||||||
|
<div x-show="selectedIds.length > 0" x-transition x-cloak
|
||||||
|
class="mb-6 bg-slate-900 text-white px-6 py-3 rounded-2xl flex items-center justify-between shadow-xl">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-blue-400 font-black" x-text="selectedIds.length"></span>
|
||||||
|
<span class="text-xs font-bold text-slate-400">개 선택됨</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-4 w-px bg-slate-700"></div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-xs font-bold text-slate-400 uppercase">일괄 변경:</span>
|
||||||
|
<select x-model="bulkStatus"
|
||||||
|
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-500">
|
||||||
|
<option value="">상태 변경...</option>
|
||||||
|
<option value="assigned">지급됨(assigned)</option>
|
||||||
|
<option value="stock">재고(stock)</option>
|
||||||
|
</select>
|
||||||
|
<select x-model="bulkModelId"
|
||||||
|
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-500">
|
||||||
|
<option value="">모델 변경...</option>
|
||||||
|
<template x-for="model in models" :key="model.id">
|
||||||
|
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name">
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
<button @click="applyBulkUpdate"
|
||||||
|
class="bg-blue-500 hover:bg-blue-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="selectedIds = []" class="text-slate-400 hover:text-white transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Laptop List Table -->
|
<!-- Laptop List Table -->
|
||||||
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden"
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden"
|
||||||
x-show="!loading && assets.length > 0">
|
x-show="!loading && assets.length > 0">
|
||||||
<table class="min-w-full divide-y divide-slate-100">
|
<table class="min-w-full divide-y divide-slate-100">
|
||||||
<thead class="bg-slate-50/50">
|
<thead class="bg-slate-50/50">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">자산번호
|
<th class="px-6 py-4 text-left">
|
||||||
|
<input type="checkbox" @change="toggleSelectAll($event.target.checked)"
|
||||||
|
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
|
||||||
|
</th>
|
||||||
|
<th @click="sortBy('asset_tag')"
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
|
||||||
|
자산번호 <span x-show="sortKey === 'asset_tag'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</th>
|
||||||
|
<th @click="sortBy('purchase_date')"
|
||||||
|
class="px-4 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
|
||||||
|
취득년 <span x-show="sortKey === 'purchase_date'"
|
||||||
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</th>
|
||||||
|
<th @click="sortBy('model_name')"
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
|
||||||
|
기기 정보 / 사양 <span x-show="sortKey === 'model_name'"
|
||||||
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</th>
|
||||||
|
<th @click="sortBy('ip_address')"
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
|
||||||
|
IP / 시리얼 <span x-show="sortKey === 'ip_address'"
|
||||||
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</th>
|
||||||
|
<th @click="sortBy('user_name')"
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
|
||||||
|
배정 사용자 <span x-show="sortKey === 'user_name'"
|
||||||
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</th>
|
||||||
|
<th @click="sortBy('last_confirmed_date')"
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
|
||||||
|
확인일 <span x-show="sortKey === 'last_confirmed_date'"
|
||||||
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</th>
|
||||||
|
<th @click="sortBy('status')"
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
|
||||||
|
상태 <span x-show="sortKey === 'status'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">기기 정보 / 사양</th>
|
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">IP 주소 / 시리얼</th>
|
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">배정 사용자</th>
|
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">취득일</th>
|
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">상태</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-slate-50">
|
<tbody class="divide-y divide-slate-50">
|
||||||
<template x-for="asset in assets" :key="asset.asset_tag">
|
<template x-for="asset in sortedAssets" :key="asset.id">
|
||||||
<tr @click="editAsset(asset)"
|
<tr class="hover:bg-slate-50/80 transition-colors group">
|
||||||
class="hover:bg-slate-50/80 transition-colors cursor-pointer group">
|
<td class="px-6 py-4">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<input type="checkbox" :value="asset.id" x-model="selectedIds"
|
||||||
|
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editAsset(asset)">
|
||||||
<span
|
<span
|
||||||
class="text-sm font-black text-slate-900 px-2.5 py-1 bg-slate-100 rounded-lg group-hover:bg-white transition-colors"
|
class="text-sm font-black text-slate-900 px-2.5 py-1 bg-slate-100 rounded-lg group-hover:bg-white transition-colors"
|
||||||
x-text="asset.asset_tag"></span>
|
x-text="asset.asset_tag"></span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap">
|
||||||
|
<span class="text-xs font-black text-blue-600 bg-blue-50 px-2 py-1 rounded"
|
||||||
|
x-text="asset.purchase_date ? asset.purchase_date.substring(0,4) : '-'"></span>
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-[10px] font-bold text-slate-900" x-text="asset.model_name"></div>
|
<div class="text-[11px] font-black text-slate-800" x-text="asset.model_name"></div>
|
||||||
<div class="text-[9px] font-black text-blue-500 uppercase tracking-widest" x-text="asset.manufacturer"></div>
|
<div class="text-[9px] font-black text-slate-400 uppercase tracking-widest mt-0.5"
|
||||||
|
x-text="asset.manufacturer"></div>
|
||||||
<div class="text-[10px] text-slate-400 mt-1 space-y-0.5">
|
<div class="text-[10px] text-slate-400 mt-1 space-y-0.5">
|
||||||
<div x-show="asset.processor" class="flex items-center"><span class="w-8 shrink-0">CPU</span> <span x-text="asset.processor"></span></div>
|
<div x-show="asset.processor" class="flex items-center"><span
|
||||||
<div x-show="asset.ram" class="flex items-center"><span class="w-8 shrink-0">RAM</span> <span x-text="asset.ram"></span></div>
|
class="w-8 shrink-0">CPU</span> <span x-text="asset.processor"></span></div>
|
||||||
<div x-show="asset.storage" class="flex items-center"><span class="w-8 shrink-0">DISK</span> <span x-text="asset.storage"></span></div>
|
<div x-show="asset.ram" class="flex items-center"><span
|
||||||
|
class="w-8 shrink-0">RAM</span>
|
||||||
|
<span x-text="asset.ram"></span>
|
||||||
|
</div>
|
||||||
|
<div x-show="asset.storage" class="flex items-center"><span
|
||||||
|
class="w-8 shrink-0">DISK</span>
|
||||||
|
<span x-text="asset.storage"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-xs font-black text-slate-900 mb-1" x-text="asset.ip_address || '-'"></div>
|
<div class="text-xs font-black text-slate-900 mb-1" x-text="asset.ip_address || '-'">
|
||||||
<div class="text-[10px] font-mono text-slate-400" x-text="asset.serial_number || '-'"></div>
|
</div>
|
||||||
|
<div class="text-[10px] font-mono text-slate-400" x-text="asset.serial_number || '-'">
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="flex items-center" x-show="asset.user_name">
|
<div class="flex items-center" x-show="asset.user_name">
|
||||||
|
|
@ -97,13 +180,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div x-show="!asset.user_name" class="text-xs text-slate-400 font-medium">배정 대기</div>
|
<div x-show="!asset.user_name" class="text-xs text-slate-400 font-medium">배정 대기</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500"
|
<td class="px-6 py-4 whitespace-nowrap text-xs text-slate-500"
|
||||||
x-text="asset.purchase_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">
|
||||||
<span
|
<span x-show="asset.user_name || asset.assigned_user_name"
|
||||||
:class="asset.status === 'assigned' ? 'bg-blue-50 text-blue-600 border-blue-100' : 'bg-slate-50 text-slate-500 border-slate-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"
|
||||||
x-text="asset.status === 'assigned' ? 'ASIGNED' : 'STOCK'"></span>
|
x-text="( (asset.user_name && asset.user_name.includes('업무용')) || (asset.assigned_user_name && asset.assigned_user_name.includes('업무용')) ) ? '업무용' : '직원배정'"></span>
|
||||||
|
<span x-show="!asset.user_name && !asset.assigned_user_name"
|
||||||
|
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>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -124,7 +211,7 @@
|
||||||
<div x-show="showModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
<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-lg w-full relative z-[111] shadow-2xl overflow-hidden">
|
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-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>
|
||||||
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600">
|
<button @click="showModal = false" class="text-slate-400 hover:text-slate-600">
|
||||||
|
|
@ -135,30 +222,17 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="submitAsset" class="space-y-5">
|
<form @submit.prevent="submitAsset" class="space-y-6">
|
||||||
<div class="grid grid-cols-2 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">자산관리번호</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">
|
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">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">IP 주소</label>
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 모델 연동</label>
|
||||||
<input type="text" x-model="formData.ip_address" placeholder="192.168.x.x"
|
<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">
|
|
||||||
</div>
|
|
||||||
</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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 모델 연동</label>
|
|
||||||
<div class="relative">
|
|
||||||
<select x-model="formData.model_id" 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 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">
|
||||||
<option value="">모델 선택</option>
|
<option value="">모델 선택</option>
|
||||||
<template x-for="model in models" :key="model.id">
|
<template x-for="model in models" :key="model.id">
|
||||||
|
|
@ -166,44 +240,123 @@
|
||||||
</option>
|
</option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
<div
|
</div>
|
||||||
class="absolute inset-y-0 right-0 pr-4 flex items-center pointer-events-none text-slate-400">
|
<div>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">시리얼 번호</label>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<input type="text" x-model="formData.serial_number"
|
||||||
d="M19 9l-7 7-7-7" />
|
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">
|
||||||
</svg>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">배정 사용자</label>
|
||||||
|
<select x-model="formData.current_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 transition-all">
|
||||||
|
<option value="">미배정 (재고)</option>
|
||||||
|
<template x-for="user in allUsers" :key="user.id">
|
||||||
|
<option :value="user.id" x-text="user.name + ' (' + user.emp_id + ')'"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">고정 IP 주소</label>
|
||||||
|
<input type="text" x-model="formData.ip_address" placeholder="192.168.x.x"
|
||||||
|
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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">취득일</label>
|
||||||
|
<input type="date" x-model="formData.purchase_date"
|
||||||
|
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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2 text-blue-600">실사
|
||||||
|
확인일</label>
|
||||||
|
<input type="date" x-model="formData.last_confirmed_date"
|
||||||
|
class="w-full px-4 py-3 bg-blue-50 border border-blue-100 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
|
||||||
|
</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"
|
||||||
|
class="text-[10px] font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded hover:bg-blue-100 transition-colors">
|
||||||
|
모델 기본값 불러오기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
</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">
|
||||||
|
</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">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<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">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 text-[10px] text-blue-500 font-bold bg-blue-50 px-3 py-1.5 rounded-lg border border-blue-100"
|
<div class="bg-slate-50 p-4 rounded-xl space-y-3">
|
||||||
x-show="formData.model_id">
|
<label class="block text-[10px] font-black text-slate-400 uppercase">Storage 1</label>
|
||||||
<span class="uppercase">Hardware Specs:</span> <span
|
<div class="grid grid-cols-2 gap-2">
|
||||||
x-text="getModelSpecs(formData.model_id) || '사양 정보 없음'"></span>
|
<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>
|
||||||
|
|
||||||
|
<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">노트북 상태</label>
|
||||||
|
<input type="text" x-model="formData.asset_status"
|
||||||
|
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">정격입출력</label>
|
||||||
|
<input type="text" x-model="formData.power_rating"
|
||||||
|
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">옵션</label>
|
||||||
|
<input type="text" x-model="formData.options"
|
||||||
|
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
|
||||||
</div>
|
</div>
|
||||||
</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.current_user_id"
|
<textarea x-model="formData.remarks" rows="2" 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 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 resize-none transition-all"></textarea>
|
||||||
<option value="">미배정 (재고)</option>
|
|
||||||
<template x-for="user in allUsers" :key="user.id">
|
|
||||||
<option :value="user.id" x-text="user.name + ' (' + user.emp_id + ')'"></option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="pt-4 flex gap-4">
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">취득일</label>
|
|
||||||
<input type="date" x-model="formData.purchase_date"
|
|
||||||
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">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-4 flex gap-3">
|
|
||||||
<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"
|
<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"
|
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>
|
x-text="isEdit ? '수정 완료' : '자산 등록하기'"></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -218,7 +371,20 @@
|
||||||
loading: true,
|
loading: true,
|
||||||
showModal: false,
|
showModal: false,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
formData: { id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock', serial_number: '', purchase_date: '', ip_address: '' },
|
selectedIds: [],
|
||||||
|
bulkStatus: '',
|
||||||
|
bulkModelId: '',
|
||||||
|
sortKey: 'asset_tag',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
formData: {
|
||||||
|
id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock',
|
||||||
|
serial_number: '', purchase_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: ''
|
||||||
|
},
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.fetchAssets();
|
this.fetchAssets();
|
||||||
|
|
@ -226,6 +392,32 @@
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleSelectAll(checked) {
|
||||||
|
this.selectedIds = checked ? this.assets.map(a => a.id) : [];
|
||||||
|
},
|
||||||
|
|
||||||
|
applyBulkUpdate() {
|
||||||
|
if (this.selectedIds.length === 0) return;
|
||||||
|
if (!this.bulkStatus && !this.bulkModelId) { alert('변경할 항목을 선택해주세요.'); return; }
|
||||||
|
|
||||||
|
fetch('api.php?action=bulk_update_laptops', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ids: this.selectedIds,
|
||||||
|
status: this.bulkStatus,
|
||||||
|
model_id: this.bulkModelId
|
||||||
|
})
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.selectedIds = [];
|
||||||
|
this.bulkStatus = '';
|
||||||
|
this.bulkModelId = '';
|
||||||
|
this.fetchAssets();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
fetchAssets() {
|
fetchAssets() {
|
||||||
const scrollPos = window.scrollY;
|
const scrollPos = window.scrollY;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
@ -236,6 +428,24 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get sortedAssets() {
|
||||||
|
return [...this.assets].sort((a, b) => {
|
||||||
|
let v1 = a[this.sortKey] || '';
|
||||||
|
let v2 = b[this.sortKey] || '';
|
||||||
|
if (this.sortOrder === 'asc') return v1 > v2 ? 1 : -1;
|
||||||
|
return v1 < v2 ? 1 : -1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
sortBy(key) {
|
||||||
|
if (this.sortKey === key) {
|
||||||
|
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
this.sortKey = key;
|
||||||
|
this.sortOrder = 'asc';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
fetchModels() {
|
fetchModels() {
|
||||||
fetch('api.php?action=get_models').then(res => res.json()).then(data => this.models = data);
|
fetch('api.php?action=get_models').then(res => res.json()).then(data => this.models = data);
|
||||||
},
|
},
|
||||||
|
|
@ -244,14 +454,38 @@
|
||||||
fetch('api.php?action=get_users').then(res => res.json()).then(data => this.allUsers = data);
|
fetch('api.php?action=get_users').then(res => res.json()).then(data => this.allUsers = data);
|
||||||
},
|
},
|
||||||
|
|
||||||
getModelSpecs(modelId) {
|
applyModelDefaults() {
|
||||||
const model = this.models.find(m => m.id == modelId);
|
if (!this.formData.model_id) return;
|
||||||
return model ? model.specs : '';
|
const model = this.models.find(m => m.id == this.formData.model_id);
|
||||||
|
if (model) {
|
||||||
|
// 모델에 정의된 사양을 자산 폼에 기본값으로 복사
|
||||||
|
this.formData.cpu = model.cpu || this.formData.cpu;
|
||||||
|
this.formData.npu = model.npu || this.formData.npu;
|
||||||
|
this.formData.ram = model.ram || this.formData.ram;
|
||||||
|
this.formData.hdd0_model = model.hdd0_model || this.formData.hdd0_model;
|
||||||
|
this.formData.hdd0_capacity = model.hdd0_capacity || this.formData.hdd0_capacity;
|
||||||
|
this.formData.hdd1_model = model.hdd1_model || this.formData.hdd1_model;
|
||||||
|
this.formData.hdd1_capacity = model.hdd1_capacity || this.formData.hdd1_capacity;
|
||||||
|
this.formData.power_rating = model.power_rating || this.formData.power_rating;
|
||||||
|
this.formData.options = model.options || this.formData.options;
|
||||||
|
this.formData.manufacturer = model.manufacturer;
|
||||||
|
this.formData.product_name = model.product_name || this.formData.product_name;
|
||||||
|
this.formData.vendor = model.vendor || this.formData.vendor;
|
||||||
|
if (!this.isEdit) {
|
||||||
|
this.formData.remarks = model.remarks || this.formData.remarks;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
openAddModal() {
|
openAddModal() {
|
||||||
this.isEdit = false;
|
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], ip_address: '' };
|
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: '',
|
||||||
|
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: ''
|
||||||
|
};
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
86
mfp.php
86
mfp.php
|
|
@ -31,16 +31,51 @@
|
||||||
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">복합기 계정 관리</h2>
|
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">복합기 계정 관리</h2>
|
||||||
<p class="text-slate-500 mt-1">임시/공용 복합기 계정 및 비밀번호 관리</p>
|
<p class="text-slate-500 mt-1">임시/공용 복합기 계정 및 비밀번호 관리</p>
|
||||||
</div>
|
</div>
|
||||||
<button @click="showModal = true"
|
<button
|
||||||
|
@click="showModal = true; isEdit = false; formData = { id: '', account_id: '', account_pw: '', purpose: '' }"
|
||||||
class="bg-amber-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-amber-200 hover:bg-amber-700 transition-all">신규
|
class="bg-amber-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-amber-200 hover:bg-amber-700 transition-all">신규
|
||||||
계정 등록</button>
|
계정 등록</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Bulk Actions -->
|
||||||
|
<div x-show="selectedIds.length > 0" x-transition x-cloak
|
||||||
|
class="mb-6 bg-slate-900 text-white px-6 py-3 rounded-2xl flex items-center justify-between shadow-xl">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-amber-400 font-black" x-text="selectedIds.length"></span>
|
||||||
|
<span class="text-xs font-bold text-slate-400">개 선택됨</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-4 w-px bg-slate-700"></div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-xs font-bold text-slate-400 uppercase">일괄 변경:</span>
|
||||||
|
<select x-model="bulkStatus"
|
||||||
|
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-amber-500">
|
||||||
|
<option value="">상태 변경...</option>
|
||||||
|
<option value="active">사용중(active)</option>
|
||||||
|
<option value="inactive">비활성(inactive)</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" x-model="bulkPurpose" placeholder="사용 목적 일괄 입력..."
|
||||||
|
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-amber-500 w-48">
|
||||||
|
<button @click="applyBulkUpdate"
|
||||||
|
class="bg-amber-500 hover:bg-amber-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="selectedIds = []" class="text-slate-400 hover:text-white transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- MFP List Table -->
|
<!-- MFP List Table -->
|
||||||
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden" x-show="mfpList.length > 0">
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden" x-show="mfpList.length > 0">
|
||||||
<table class="min-w-full divide-y divide-slate-100">
|
<table class="min-w-full divide-y divide-slate-100">
|
||||||
<thead class="bg-slate-50/50">
|
<thead class="bg-slate-50/50">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class="px-6 py-4 text-left w-10">
|
||||||
|
<input type="checkbox" @change="toggleSelectAll($event.target.checked)"
|
||||||
|
class="w-4 h-4 rounded border-slate-300 text-amber-600 focus:ring-amber-500">
|
||||||
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">계정 ID
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">계정 ID
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
|
@ -52,9 +87,13 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-slate-50">
|
<tbody class="divide-y divide-slate-50">
|
||||||
<template x-for="mfp in mfpList" :key="mfp.account_id">
|
<template x-for="mfp in mfpList" :key="mfp.id">
|
||||||
<tr @click="editMfp(mfp)" class="hover:bg-slate-50/80 transition-colors cursor-pointer group">
|
<tr class="hover:bg-slate-50/80 transition-colors group">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4">
|
||||||
|
<input type="checkbox" :value="mfp.id" x-model="selectedIds"
|
||||||
|
class="w-4 h-4 rounded border-slate-300 text-amber-600 focus:ring-amber-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editMfp(mfp)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="p-2 bg-amber-50 rounded-lg text-amber-600 mr-3">
|
<div class="p-2 bg-amber-50 rounded-lg text-amber-600 mr-3">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -65,14 +104,15 @@
|
||||||
<span class="text-sm font-black text-slate-900" x-text="mfp.account_id"></span>
|
<span class="text-sm font-black text-slate-900" x-text="mfp.account_id"></span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editMfp(mfp)">
|
||||||
<span
|
<span
|
||||||
class="px-2 py-1 bg-slate-100 border border-slate-200 rounded text-xs font-mono font-bold text-slate-700"
|
class="px-2 py-1 bg-slate-100 border border-slate-200 rounded text-xs font-mono font-bold text-slate-700"
|
||||||
x-text="mfp.account_pw"></span>
|
x-text="mfp.account_pw"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500" x-text="mfp.purpose || '-'">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 cursor-pointer"
|
||||||
|
@click="editMfp(mfp)" x-text="mfp.purpose || '-'">
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editMfp(mfp)">
|
||||||
<span
|
<span
|
||||||
class="px-3 py-1 bg-emerald-100 text-emerald-600 rounded-full text-[10px] font-black uppercase tracking-tighter"
|
class="px-3 py-1 bg-emerald-100 text-emerald-600 rounded-full text-[10px] font-black uppercase tracking-tighter"
|
||||||
x-text="mfp.status"></span>
|
x-text="mfp.status"></span>
|
||||||
|
|
@ -112,7 +152,13 @@
|
||||||
<script>
|
<script>
|
||||||
function mfpManagement() {
|
function mfpManagement() {
|
||||||
return {
|
return {
|
||||||
mfpList: [], showModal: false, isEdit: false, formData: { id: '', account_id: '', account_pw: '', purpose: '' },
|
mfpList: [],
|
||||||
|
showModal: false,
|
||||||
|
isEdit: false,
|
||||||
|
selectedIds: [],
|
||||||
|
bulkStatus: '',
|
||||||
|
bulkPurpose: '',
|
||||||
|
formData: { id: '', account_id: '', account_pw: '', purpose: '' },
|
||||||
init() { this.fetchMfp(); },
|
init() { this.fetchMfp(); },
|
||||||
fetchMfp() {
|
fetchMfp() {
|
||||||
const scrollPos = window.scrollY;
|
const scrollPos = window.scrollY;
|
||||||
|
|
@ -121,6 +167,30 @@
|
||||||
this.$nextTick(() => window.scrollTo(0, scrollPos));
|
this.$nextTick(() => window.scrollTo(0, scrollPos));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
toggleSelectAll(checked) {
|
||||||
|
this.selectedIds = checked ? this.mfpList.map(m => m.id) : [];
|
||||||
|
},
|
||||||
|
applyBulkUpdate() {
|
||||||
|
if (this.selectedIds.length === 0) return;
|
||||||
|
if (!this.bulkStatus && !this.bulkPurpose) { alert('변경할 항목을 선택해주세요.'); return; }
|
||||||
|
|
||||||
|
fetch('api.php?action=bulk_update_mfp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ids: this.selectedIds,
|
||||||
|
status: this.bulkStatus,
|
||||||
|
purpose: this.bulkPurpose
|
||||||
|
})
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.selectedIds = [];
|
||||||
|
this.bulkStatus = '';
|
||||||
|
this.bulkPurpose = '';
|
||||||
|
this.fetchMfp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
editMfp(mfp) {
|
editMfp(mfp) {
|
||||||
this.isEdit = true;
|
this.isEdit = true;
|
||||||
this.formData = { ...mfp };
|
this.formData = { ...mfp };
|
||||||
|
|
|
||||||
559
models.php
559
models.php
|
|
@ -4,7 +4,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>DB 관리 - 노트북 모델 | FKI ASSET</title>
|
<title>DB 관리 | FKI ASSET</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
@ -25,100 +25,449 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-[#f8fafc] text-slate-800" x-data="modelManagement()">
|
<body class="bg-[#f8fafc] text-slate-800" x-data="dbManagement()">
|
||||||
|
|
||||||
<?php include 'nav.php'; ?>
|
<?php include 'nav.php'; ?>
|
||||||
|
|
||||||
<main class="max-w-[1600px] mx-auto p-8 pt-10">
|
<main class="max-w-[1600px] mx-auto p-8 pt-10">
|
||||||
<header class="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<header class="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">DB 관리: 노트북 모델</h2>
|
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">DB 마스터 관리</h2>
|
||||||
<p class="text-slate-500 mt-1">자산 등록 시 사용될 하드웨어 모델 정보 및 상세 사양 관리</p>
|
<p class="text-slate-500 mt-1">시스템 운영을 위한 기초 자산 모델 및 조직 정보를 관리합니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button x-show="currentTab === 'models'" @click="openAddModal()"
|
||||||
|
class="bg-blue-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all">신규
|
||||||
|
모델 등록</button>
|
||||||
|
<button x-show="currentTab === 'depts'" @click="openAddDeptModal()"
|
||||||
|
class="bg-blue-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all">신규
|
||||||
|
부서 생성</button>
|
||||||
</div>
|
</div>
|
||||||
<button @click="openAddModal()"
|
|
||||||
class="bg-blue-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all">신규
|
|
||||||
모델 등록</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Model List Table -->
|
<!-- Tabs -->
|
||||||
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden">
|
<div class="flex items-center space-x-2 mb-8 bg-slate-100 p-1.5 rounded-2xl w-fit">
|
||||||
<table class="min-w-full divide-y divide-slate-100">
|
<button @click="currentTab = 'models'"
|
||||||
<thead class="bg-slate-50/50">
|
:class="currentTab === 'models' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'"
|
||||||
<tr>
|
class="px-6 py-2 rounded-xl text-sm font-black transition-all">노트북 모델</button>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">제조사
|
<button @click="currentTab = 'depts'"
|
||||||
</th>
|
:class="currentTab === 'depts' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'"
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">모델명
|
class="px-6 py-2 rounded-xl text-sm font-black transition-all">부서 관리</button>
|
||||||
</th>
|
</div>
|
||||||
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">상세 사양
|
|
||||||
</th>
|
<!-- Tab 1: Laptop Models -->
|
||||||
<th class="px-6 py-4 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">관리
|
<div x-show="currentTab === 'models'" x-transition>
|
||||||
</th>
|
<!-- Bulk Actions -->
|
||||||
</tr>
|
<div x-show="selectedIds.length > 0" x-transition x-cloak
|
||||||
</thead>
|
class="mb-6 bg-slate-900 text-white px-6 py-3 rounded-2xl flex items-center justify-between shadow-xl">
|
||||||
<tbody class="divide-y divide-slate-50">
|
<div class="flex items-center gap-4">
|
||||||
<template x-for="model in models" :key="model.id">
|
<div class="flex items-center gap-2">
|
||||||
<tr class="hover:bg-slate-50/80 transition-colors group">
|
<span class="text-blue-400 font-black" x-text="selectedIds.length"></span>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<span class="text-xs font-bold text-slate-400">개 선택됨</span>
|
||||||
<span
|
</div>
|
||||||
class="px-2.5 py-1 bg-slate-100 text-slate-600 rounded-lg text-xs font-black uppercase tracking-widest"
|
<div class="h-4 w-px bg-slate-700"></div>
|
||||||
x-text="model.manufacturer"></span>
|
<div class="flex items-center gap-3">
|
||||||
</td>
|
<span class="text-xs font-bold text-slate-400 uppercase">일괄 변경:</span>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<input type="text" x-model="bulkManufacturer" placeholder="제조사 일괄 변경..."
|
||||||
<span class="text-sm font-bold text-slate-900" x-text="model.model_name"></span>
|
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-500 w-40 text-white">
|
||||||
</td>
|
<input type="text" x-model="bulkProductName" placeholder="품명 일괄 변경..."
|
||||||
<td class="px-6 py-4">
|
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-500 w-40 text-white">
|
||||||
<span class="text-xs text-slate-500 line-clamp-1" x-text="model.specs || '-'"></span>
|
<button @click="applyBulkUpdate"
|
||||||
</td>
|
class="bg-blue-500 hover:bg-blue-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
</div>
|
||||||
<button @click="editModel(model)"
|
</div>
|
||||||
class="text-xs font-bold text-blue-600 hover:underline px-3 py-1 rounded-lg hover:bg-blue-50 transition-all">수정</button>
|
<button @click="selectedIds = []" class="text-slate-400 hover:text-white transition-colors">
|
||||||
</td>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Model List Table -->
|
||||||
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-x-auto no-scrollbar">
|
||||||
|
<table class="min-w-full divide-y divide-slate-100">
|
||||||
|
<thead class="bg-slate-50/50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-4 text-left w-10">
|
||||||
|
<input type="checkbox" @change="toggleSelectAll($event.target.checked)"
|
||||||
|
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
||||||
|
취득년도</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
||||||
|
취득일</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
||||||
|
제조사</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-4 text-left text-[10px] font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
모델명</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider font-mono">
|
||||||
|
CPU</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider font-mono">
|
||||||
|
RAM</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider font-mono text-blue-500">
|
||||||
|
HDD 0</th>
|
||||||
|
<th
|
||||||
|
class="px-4 py-4 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
||||||
|
정격입출력</th>
|
||||||
|
<th
|
||||||
|
class="px-6 py-4 text-right text-[10px] font-bold text-slate-500 uppercase tracking-wider whitespace-nowrap">
|
||||||
|
관리</th>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</thead>
|
||||||
</tbody>
|
<tbody class="divide-y divide-slate-50">
|
||||||
</table>
|
<template x-for="model in models" :key="model.id">
|
||||||
|
<tr class="hover:bg-slate-50/80 transition-colors group">
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<input type="checkbox" :value="model.id" x-model="selectedIds"
|
||||||
|
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-xs font-medium text-slate-500"
|
||||||
|
@click="editModel(model)">
|
||||||
|
<span
|
||||||
|
x-text="model.purchase_date ? model.purchase_date.substring(0,4) : '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-[11px] text-slate-400"
|
||||||
|
@click="editModel(model)">
|
||||||
|
<span x-text="model.purchase_date || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap cursor-pointer" @click="editModel(model)">
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-black uppercase tracking-widest"
|
||||||
|
x-text="model.manufacturer"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editModel(model)">
|
||||||
|
<span class="text-sm font-bold text-slate-800" x-text="model.model_name"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-xs font-mono text-slate-500"
|
||||||
|
@click="editModel(model)">
|
||||||
|
<span x-text="model.cpu || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-xs font-mono text-slate-600 font-bold"
|
||||||
|
@click="editModel(model)">
|
||||||
|
<span x-text="model.ram || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-xs font-mono text-blue-600 font-bold"
|
||||||
|
@click="editModel(model)">
|
||||||
|
<span x-text="model.hdd0_capacity || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-4 whitespace-nowrap cursor-pointer text-[10px] text-slate-400"
|
||||||
|
@click="editModel(model)">
|
||||||
|
<span x-text="model.power_rating || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||||
|
<button @click="editModel(model)"
|
||||||
|
class="text-[11px] font-black text-blue-600 hover:underline px-2 py-1 rounded-lg hover:bg-blue-50 transition-all uppercase">EDIT</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 2: Departments -->
|
||||||
|
<div x-show="currentTab === 'depts'" x-transition>
|
||||||
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<table class="min-w-full divide-y divide-slate-100">
|
||||||
|
<thead class="bg-slate-50/50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
부서명 (계층구조)</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
인원수</th>
|
||||||
|
<th class="px-6 py-4 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
관리</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50">
|
||||||
|
<template x-for="dept in depts" :key="dept.id">
|
||||||
|
<tr class="hover:bg-slate-50/80 transition-colors group">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div :style="'margin-left: ' + (getDeptLevel(dept.path) * 20) + 'px'"
|
||||||
|
class="flex items-center">
|
||||||
|
<div class="w-2 h-2 rounded-full mr-3"
|
||||||
|
:class="getDeptLevel(dept.path) === 0 ? 'bg-blue-600' : 'bg-slate-300'">
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-bold text-slate-900" x-text="dept.name"></span>
|
||||||
|
<span class="ml-2 text-[10px] text-slate-400 font-medium"
|
||||||
|
x-show="getDeptLevel(dept.path) > 0" x-text="dept.path"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<button @click="viewDeptMembers(dept)"
|
||||||
|
class="text-sm font-black text-blue-600 hover:underline">
|
||||||
|
<span x-text="dept.member_count || 0"></span>명 확인
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||||
|
<button @click="editDept(dept)"
|
||||||
|
class="text-xs font-bold text-slate-400 hover:text-blue-600 px-3 py-1 rounded-lg hover:bg-blue-50 transition-all">수정</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Model Modal -->
|
<!-- Model Modal -->
|
||||||
<div x-show="showModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
<div class="fixed inset-0 modal-bg" @click="showModal = false"></div>
|
<div class="fixed inset-0 modal-bg" @click="showModal = false"></div>
|
||||||
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl">
|
<div class="bg-white rounded-3xl p-8 max-w-4xl w-full relative z-[111] shadow-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<h3 class="text-2xl font-black mb-6" x-text="isEdit ? '모델 정보 수정' : '신규 모델 등록'"></h3>
|
<h3 class="text-2xl font-black mb-6" x-text="isEdit ? '모델 정보 수정' : '신규 모델 등록'"></h3>
|
||||||
<form @submit.prevent="submitModel" class="space-y-4">
|
<form @submit.prevent="submitModel" class="space-y-6">
|
||||||
<div>
|
<!-- 기본 정보 -->
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">제조사</label>
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<input type="text" x-model="formData.manufacturer" required placeholder="예: Samsung, Apple"
|
<div>
|
||||||
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">
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">제조사</label>
|
||||||
|
<input type="text" x-model="formData.manufacturer" required placeholder="예: SAMSUNG"
|
||||||
|
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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">모델명</label>
|
||||||
|
<input type="text" x-model="formData.model_name" required placeholder="예: NT750XEV"
|
||||||
|
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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">품명</label>
|
||||||
|
<input type="text" x-model="formData.product_name" 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">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">모델명</label>
|
<!-- 핵심 사양 -->
|
||||||
<input type="text" x-model="formData.model_name" required placeholder="예: Galaxy Book 4 Pro"
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
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">
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">CPU (프로세서)</label>
|
||||||
|
<input type="text" x-model="formData.cpu" placeholder="예: i5-1135G7"
|
||||||
|
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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">NPU (AI Boost)</label>
|
||||||
|
<input type="text" x-model="formData.npu" placeholder="예: Intel AI Boost"
|
||||||
|
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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">RAM</label>
|
||||||
|
<input type="text" x-model="formData.ram" placeholder="예: 16GB"
|
||||||
|
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">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 저장 장치 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 bg-slate-50 p-4 rounded-2xl">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">Storage 0
|
||||||
|
(Main)</label>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<input type="text" x-model="formData.hdd0_model" placeholder="타입/모델"
|
||||||
|
class="w-full px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm outline-none">
|
||||||
|
<input type="text" x-model="formData.hdd0_capacity" placeholder="용량 (예: 512GB)"
|
||||||
|
class="w-full px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">Storage 1
|
||||||
|
(Sub)</label>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<input type="text" x-model="formData.hdd1_model" placeholder="타입/모델"
|
||||||
|
class="w-full px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm outline-none">
|
||||||
|
<input type="text" x-model="formData.hdd1_capacity" placeholder="용량"
|
||||||
|
class="w-full px-4 py-2 bg-white border border-slate-200 rounded-lg text-sm outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 자산 정보 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 상태</label>
|
||||||
|
<input type="text" x-model="formData.asset_status" 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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">고정 IP</label>
|
||||||
|
<input type="text" x-model="formData.fixed_ip" placeholder="192.168.x.x"
|
||||||
|
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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">배정 사용자</label>
|
||||||
|
<input type="text" x-model="formData.assigned_user" 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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2 text-blue-600">취득일</label>
|
||||||
|
<input type="date" x-model="formData.purchase_date"
|
||||||
|
class="w-full px-4 py-3 bg-blue-50 border border-blue-100 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 기타 사양 -->
|
||||||
|
<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">정격입출력</label>
|
||||||
|
<input type="text" x-model="formData.power_rating" placeholder="예: 20V / 3.25A"
|
||||||
|
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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">판매사</label>
|
||||||
|
<input type="text" x-model="formData.vendor" 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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">옵션</label>
|
||||||
|
<input type="text" x-model="formData.options" 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">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">상세 사양 (CPU, RAM, SSD 등)</label>
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">비고 (특기사항)</label>
|
||||||
<textarea x-model="formData.specs" rows="3" placeholder="예: Core Ultra 7 / 32GB / 1TB"
|
<textarea x-model="formData.remarks" rows="2" 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 resize-none"></textarea>
|
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 resize-none"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-4 flex gap-3">
|
<div class="pt-4 flex gap-3">
|
||||||
<button type="button" @click="showModal = false"
|
<button type="button" @click="showModal = false"
|
||||||
class="flex-1 py-3 bg-slate-100 rounded-xl font-bold">취소</button>
|
class="flex-1 py-4 bg-slate-100 rounded-2xl font-bold">취소</button>
|
||||||
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold"
|
<button type="submit"
|
||||||
|
class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200"
|
||||||
x-text="isEdit ? '수정 완료' : '모델 등록'"></button>
|
x-text="isEdit ? '수정 완료' : '모델 등록'"></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dept Modal -->
|
||||||
|
<div x-show="showDeptModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
|
<div class="fixed inset-0 modal-bg" @click="showDeptModal = false"></div>
|
||||||
|
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl">
|
||||||
|
<h3 class="text-2xl font-black mb-6" x-text="isDeptEdit ? '부서 정보 수정' : '신규 부서 생성'"></h3>
|
||||||
|
<form @submit.prevent="submitDept" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">부서명</label>
|
||||||
|
<input type="text" x-model="deptFormData.name" 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">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">상위 부서</label>
|
||||||
|
<select x-model="deptFormData.parent_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">
|
||||||
|
<option value="">없음 (최상위)</option>
|
||||||
|
<template x-for="d in depts" :key="d.id">
|
||||||
|
<option :value="d.id" x-text="d.path" :disabled="d.id == deptFormData.id"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="pt-4 flex gap-3">
|
||||||
|
<button type="button" @click="showDeptModal = false"
|
||||||
|
class="flex-1 py-3 bg-slate-100 rounded-xl font-bold">취소</button>
|
||||||
|
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold"
|
||||||
|
x-text="isDeptEdit ? '수정 완료' : '부서 생성'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Members Modal -->
|
||||||
|
<div x-show="showMembersModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
|
<div class="fixed inset-0 modal-bg" @click="showMembersModal = false"></div>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-3xl p-8 max-w-2xl w-full relative z-[111] shadow-2xl overflow-hidden flex flex-col max-h-[80vh]">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h3 class="text-2xl font-black text-slate-900" x-text="selectedDept?.name + ' 부서원'"></h3>
|
||||||
|
<span class="text-xs font-bold text-blue-600 bg-blue-50 px-3 py-1 rounded-full"><span
|
||||||
|
x-text="members.length"></span>명</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto pr-2 no-scrollbar">
|
||||||
|
<table class="min-w-full divide-y divide-slate-100">
|
||||||
|
<thead class="bg-slate-50 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-bold text-slate-500">이름</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-bold text-slate-500">사번/직위</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-bold text-slate-500">이동</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50">
|
||||||
|
<template x-for="user in members" :key="user.id">
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3 text-sm font-bold text-slate-900" x-text="user.name"></td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="text-xs font-black text-slate-400" x-text="user.emp_id"></div>
|
||||||
|
<div class="text-[10px] text-blue-500 font-bold uppercase"
|
||||||
|
x-text="user.position || '-'"></div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
<select @change="reassignMember(user.id, $event.target.value)"
|
||||||
|
class="text-xs bg-slate-100 border-none rounded-lg px-2 py-1 focus:ring-2 focus:ring-blue-500">
|
||||||
|
<option value="">부서 이동...</option>
|
||||||
|
<template x-for="d in depts" :key="d.id">
|
||||||
|
<option :value="d.id" x-text="d.name" :disabled="d.id == selectedDept.id">
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div x-show="members.length === 0" class="py-10 text-center text-slate-400 italic text-sm">소속된 부서원이
|
||||||
|
없습니다.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 pt-6 border-t border-slate-100 flex justify-end">
|
||||||
|
<button @click="showMembersModal = false"
|
||||||
|
class="px-6 py-2.5 bg-slate-900 text-white rounded-xl font-bold">닫기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function modelManagement() {
|
function dbManagement() {
|
||||||
return {
|
return {
|
||||||
|
currentTab: 'models',
|
||||||
|
|
||||||
|
// Model Data
|
||||||
models: [],
|
models: [],
|
||||||
showModal: false,
|
showModal: false,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
formData: { id: '', manufacturer: '', model_name: '', specs: '' },
|
selectedIds: [],
|
||||||
init() { this.fetchModels(); },
|
bulkManufacturer: '',
|
||||||
|
bulkProductName: '',
|
||||||
|
formData: {
|
||||||
|
id: '', manufacturer: '', model_name: '', product_name: '',
|
||||||
|
cpu: '', npu: '', ram: '',
|
||||||
|
hdd0_model: '', hdd0_capacity: '',
|
||||||
|
hdd1_model: '', hdd1_capacity: '',
|
||||||
|
asset_status: '', fixed_ip: '', assigned_user: '', purchase_date: '',
|
||||||
|
power_rating: '', vendor: '', options: '', remarks: '', specs: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dept Data
|
||||||
|
depts: [],
|
||||||
|
showDeptModal: false,
|
||||||
|
showMembersModal: false,
|
||||||
|
isDeptEdit: false,
|
||||||
|
selectedDept: null,
|
||||||
|
members: [],
|
||||||
|
deptFormData: { id: '', name: '', parent_id: '', level: 1 },
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchModels();
|
||||||
|
this.fetchDepts();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Model Methods
|
||||||
fetchModels() {
|
fetchModels() {
|
||||||
const scrollPos = window.scrollY;
|
const scrollPos = window.scrollY;
|
||||||
fetch('api.php?action=get_models').then(res => res.json()).then(data => {
|
fetch('api.php?action=get_models').then(res => res.json()).then(data => {
|
||||||
|
|
@ -126,9 +475,37 @@
|
||||||
this.$nextTick(() => window.scrollTo(0, scrollPos));
|
this.$nextTick(() => window.scrollTo(0, scrollPos));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
toggleSelectAll(checked) {
|
||||||
|
this.selectedIds = checked ? this.models.map(m => m.id) : [];
|
||||||
|
},
|
||||||
|
applyBulkUpdate() {
|
||||||
|
if (this.selectedIds.length === 0) return;
|
||||||
|
if (!this.bulkManufacturer && !this.bulkProductName) { alert('변경할 항목을 선택해주세요.'); return; }
|
||||||
|
fetch('api.php?action=bulk_update_models', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ids: this.selectedIds,
|
||||||
|
manufacturer: this.bulkManufacturer,
|
||||||
|
product_name: this.bulkProductName
|
||||||
|
})
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.selectedIds = [];
|
||||||
|
this.bulkManufacturer = '';
|
||||||
|
this.bulkProductName = '';
|
||||||
|
this.fetchModels();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
openAddModal() {
|
openAddModal() {
|
||||||
this.isEdit = false;
|
this.isEdit = false;
|
||||||
this.formData = { id: '', manufacturer: '', model_name: '', specs: '' };
|
this.formData = {
|
||||||
|
id: '', manufacturer: '', model_name: '', product_name: '',
|
||||||
|
cpu: '', npu: '', ram: '', hdd0_model: '', hdd0_capacity: '', hdd1_model: '', hdd1_capacity: '',
|
||||||
|
asset_status: '', fixed_ip: '', assigned_user: '', purchase_date: '',
|
||||||
|
power_rating: '', vendor: '', options: '', remarks: '', specs: ''
|
||||||
|
};
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
},
|
},
|
||||||
editModel(model) {
|
editModel(model) {
|
||||||
|
|
@ -148,6 +525,66 @@
|
||||||
this.fetchModels();
|
this.fetchModels();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dept Methods
|
||||||
|
fetchDepts() {
|
||||||
|
fetch('api.php?action=get_departments').then(res => res.json()).then(data => {
|
||||||
|
this.depts = data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getDeptLevel(path) {
|
||||||
|
if (!path) return 0;
|
||||||
|
return (path.match(/>/g) || []).length;
|
||||||
|
},
|
||||||
|
openAddDeptModal() {
|
||||||
|
this.isDeptEdit = false;
|
||||||
|
this.deptFormData = { id: '', name: '', parent_id: '', level: 1 };
|
||||||
|
this.showDeptModal = true;
|
||||||
|
},
|
||||||
|
editDept(dept) {
|
||||||
|
this.isDeptEdit = true;
|
||||||
|
this.deptFormData = { id: dept.id, name: dept.name, parent_id: dept.parent_id || '', level: dept.level };
|
||||||
|
this.showDeptModal = true;
|
||||||
|
},
|
||||||
|
submitDept() {
|
||||||
|
const action = this.isDeptEdit ? 'update_dept' : 'add_dept';
|
||||||
|
if (this.deptFormData.parent_id) {
|
||||||
|
const parent = this.depts.find(d => d.id == this.deptFormData.parent_id);
|
||||||
|
this.deptFormData.level = this.getDeptLevel(parent.path) + 1;
|
||||||
|
} else {
|
||||||
|
this.deptFormData.level = 0;
|
||||||
|
}
|
||||||
|
fetch(`api.php?action=${action}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(this.deptFormData)
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.showDeptModal = false;
|
||||||
|
this.fetchDepts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
viewDeptMembers(dept) {
|
||||||
|
this.selectedDept = dept;
|
||||||
|
fetch(`api.php?action=get_dept_users&dept_id=${dept.id}`).then(res => res.json()).then(data => {
|
||||||
|
this.members = data;
|
||||||
|
this.showMembersModal = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
reassignMember(userId, newDeptId) {
|
||||||
|
if (!newDeptId) return;
|
||||||
|
fetch('api.php?action=reassign_user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ user_ids: [userId], new_dept_id: newDeptId })
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.viewDeptMembers(this.selectedDept);
|
||||||
|
this.fetchDepts();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
nav.php
2
nav.php
|
|
@ -27,8 +27,8 @@ $current_page = basename($_SERVER['PHP_SELF']);
|
||||||
<?php
|
<?php
|
||||||
$menu_items = [
|
$menu_items = [
|
||||||
['url' => 'index.php', 'name' => '대시보드', 'icon' => 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'],
|
['url' => 'index.php', 'name' => '대시보드', 'icon' => 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'],
|
||||||
|
['url' => 'rental.php', 'name' => '노트북 임대', 'icon' => 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4'],
|
||||||
['url' => 'users.php', 'name' => '직원 관리', 'icon' => 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z'],
|
['url' => 'users.php', 'name' => '직원 관리', 'icon' => 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z'],
|
||||||
['url' => 'departments.php', 'name' => '부서 관리', 'icon' => 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4'],
|
|
||||||
['url' => 'laptops.php', 'name' => '노트북 자산', 'icon' => 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z'],
|
['url' => 'laptops.php', 'name' => '노트북 자산', 'icon' => 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z'],
|
||||||
['url' => 'cards.php', 'name' => '출입증 현황', 'icon' => 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z'],
|
['url' => 'cards.php', 'name' => '출입증 현황', 'icon' => 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z'],
|
||||||
['url' => 'mfp.php', 'name' => '복합기 계정', 'icon' => 'M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z'],
|
['url' => 'mfp.php', 'name' => '복합기 계정', 'icon' => 'M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z'],
|
||||||
|
|
|
||||||
308
rental.php
Normal file
308
rental.php
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>노트북 임대 관리 | FKI ASSET</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="assets/style.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Pretendard', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-bg {
|
||||||
|
background-color: rgba(15, 23, 42, 0.7);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-[#f8fafc] text-slate-800" x-data="rentalManagement()">
|
||||||
|
|
||||||
|
<?php include 'nav.php'; ?>
|
||||||
|
|
||||||
|
<main class="max-w-[1600px] mx-auto p-8 pt-10">
|
||||||
|
<header class="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">노트북 단기 임대</h2>
|
||||||
|
<p class="text-slate-500 mt-1">업무용 공용 자산의 대여 및 반납 기록 관리 (사원별 임대 현황)</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="relative">
|
||||||
|
<input type="text" x-model="searchQuery" placeholder="자산번호, 모델명 검색..."
|
||||||
|
class="pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-blue-500 outline-none w-64 shadow-sm">
|
||||||
|
<svg class="w-4 h-4 absolute left-3.5 top-3 text-slate-400" fill="none" stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Rental List Table -->
|
||||||
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
|
<table class="min-w-full divide-y divide-slate-100">
|
||||||
|
<thead class="bg-slate-50/50">
|
||||||
|
<tr>
|
||||||
|
<th @click="sortBy('asset_tag')"
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
|
||||||
|
자산번호 <span x-show="sortKey === 'asset_tag'" x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</th>
|
||||||
|
<th @click="sortBy('model_name')"
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors">
|
||||||
|
모델 정보 <span x-show="sortKey === 'model_name'"
|
||||||
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</th>
|
||||||
|
<th @click="sortBy('rental_user_name')"
|
||||||
|
class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider cursor-pointer hover:bg-slate-100 transition-colors text-blue-600">
|
||||||
|
현재 임대사원 <span x-show="sortKey === 'rental_user_name'"
|
||||||
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
비고(특기사항)</th>
|
||||||
|
<th class="px-6 py-4 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">액션
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50">
|
||||||
|
<template x-for="item in sortedAssets" :key="item.id">
|
||||||
|
<tr class="hover:bg-slate-50/80 transition-colors group">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span class="text-sm font-black text-slate-900 px-2.5 py-1 bg-slate-100 rounded-lg"
|
||||||
|
x-text="item.asset_tag"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="text-sm font-bold text-slate-800" x-text="item.model_name"></div>
|
||||||
|
<div class="text-[10px] text-slate-400 font-black uppercase tracking-widest"
|
||||||
|
x-text="item.manufacturer"></div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<template x-if="item.rental_user_name">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-2 h-2 bg-emerald-500 rounded-full mr-2"></div>
|
||||||
|
<span class="text-sm font-black text-slate-900"
|
||||||
|
x-text="item.rental_user_name"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="!item.rental_user_name">
|
||||||
|
<span class="text-xs text-slate-300 italic font-medium">임대 가능</span>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="text-xs text-slate-500 truncate max-w-xs" x-text="item.remarks || '-'">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button @click="openHistoryModal(item)"
|
||||||
|
class="px-3 py-1.5 text-[11px] font-black text-slate-500 hover:text-slate-900 bg-slate-100 hover:bg-slate-200 rounded-lg transition-all uppercase">History</button>
|
||||||
|
<button @click="openRentalModal(item)"
|
||||||
|
:class="item.rental_user_name ? 'bg-amber-100 text-amber-600 hover:bg-amber-200' : 'bg-blue-600 text-white hover:bg-blue-700'"
|
||||||
|
class="px-4 py-1.5 text-[11px] font-black rounded-lg transition-all uppercase shadow-sm"
|
||||||
|
x-text="item.rental_user_name ? 'Change/Return' : 'Start Rental'"></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div x-show="filteredAssets.length === 0" class="py-20 text-center">
|
||||||
|
<div class="text-slate-300 text-5xl mb-4 italic">No Assets Found</div>
|
||||||
|
<p class="text-slate-400 text-sm font-medium">배정 사용자가 '업무용'인 노트북이 없거나 검색 결과가 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Rental Modal -->
|
||||||
|
<div x-show="showRentalModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
|
<div class="fixed inset-0 modal-bg" @click="showRentalModal = false"></div>
|
||||||
|
<div class="bg-white rounded-3xl p-8 max-w-md w-full relative z-[111] shadow-2xl">
|
||||||
|
<h3 class="text-2xl font-black mb-2" x-text="selectedAsset?.asset_tag + ' 임대 설정'"></h3>
|
||||||
|
<p class="text-slate-400 text-xs font-bold mb-6" x-text="selectedAsset?.model_name"></p>
|
||||||
|
|
||||||
|
<form @submit.prevent="submitRental" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">임대사원 성함</label>
|
||||||
|
<input type="text" x-model="rentalName" required 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">
|
||||||
|
<p class="mt-2 text-[10px] text-slate-400 italic">* 공란으로 입력 후 적용 시 반납 처리됩니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-4 flex gap-3">
|
||||||
|
<button type="button" @click="showRentalModal = false"
|
||||||
|
class="flex-1 py-3 bg-slate-100 rounded-xl font-bold">취소</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold shadow-lg shadow-blue-200">임대
|
||||||
|
적용</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- History Modal -->
|
||||||
|
<div x-show="showHistoryModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
|
<div class="fixed inset-0 modal-bg" @click="showHistoryModal = false"></div>
|
||||||
|
<div class="bg-white rounded-3xl p-8 max-w-2xl w-full relative z-[111] shadow-2xl flex flex-col max-h-[85vh]">
|
||||||
|
<div class="flex justify-between items-start mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-black text-slate-900" x-text="selectedAsset?.asset_tag + ' 이력 로그'"></h3>
|
||||||
|
<p class="text-slate-400 text-xs font-bold" x-text="selectedAsset?.model_name"></p>
|
||||||
|
</div>
|
||||||
|
<button @click="showHistoryModal = false" class="text-slate-400 hover:text-slate-900">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto pr-2 no-scrollbar space-y-8">
|
||||||
|
<!-- Part 1: Rental Logs -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<div class="h-px bg-slate-100 flex-1"></div>
|
||||||
|
<h4 class="text-[10px] font-black text-emerald-500 uppercase tracking-[0.2em]">01. PC 임대 기록 (Max
|
||||||
|
20)</h4>
|
||||||
|
<div class="h-px bg-slate-100 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<template x-for="log in history.rental" :key="log.id">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-3 bg-emerald-50 rounded-xl border border-emerald-100">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-8 h-8 bg-white rounded-lg flex items-center justify-center text-[10px] font-black text-emerald-500 shadow-sm"
|
||||||
|
x-text="log.user_name.charAt(0)"></div>
|
||||||
|
<span class="text-sm font-black text-slate-900" x-text="log.user_name"></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-bold text-emerald-600 bg-white px-2 py-1 rounded-md"
|
||||||
|
x-text="log.action_date"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div x-show="history.rental.length === 0"
|
||||||
|
class="text-center py-6 text-slate-300 italic text-xs">임대 기록이 없습니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Part 2: Assignment Logs -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<div class="h-px bg-slate-100 flex-1"></div>
|
||||||
|
<h4 class="text-[10px] font-black text-blue-500 uppercase tracking-[0.2em]">02. PC 배정 기록 (Max
|
||||||
|
20)</h4>
|
||||||
|
<div class="h-px bg-slate-100 flex-1"></div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<template x-for="log in history.assignment" :key="log.id">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-xl border border-blue-100">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-8 h-8 bg-white rounded-lg flex items-center justify-center text-[10px] font-black text-blue-500 shadow-sm"
|
||||||
|
x-text="log.user_name.charAt(0)"></div>
|
||||||
|
<span class="text-sm font-black text-slate-900" x-text="log.user_name"></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-bold text-blue-600 bg-white px-2 py-1 rounded-md"
|
||||||
|
x-text="log.action_date"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div x-show="history.assignment.length === 0"
|
||||||
|
class="text-center py-6 text-slate-300 italic text-xs">배정 기록이 없습니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function rentalManagement() {
|
||||||
|
return {
|
||||||
|
assets: [],
|
||||||
|
searchQuery: '',
|
||||||
|
sortKey: 'asset_tag',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
showRentalModal: false,
|
||||||
|
showHistoryModal: false,
|
||||||
|
selectedAsset: null,
|
||||||
|
rentalName: '',
|
||||||
|
history: { rental: [], assignment: [] },
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchAssets();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchAssets() {
|
||||||
|
fetch('api.php?action=get_rental_laptops').then(res => res.json()).then(data => {
|
||||||
|
this.assets = data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get filteredAssets() {
|
||||||
|
if (!this.searchQuery) return this.assets;
|
||||||
|
const q = this.searchQuery.toLowerCase();
|
||||||
|
return this.assets.filter(a =>
|
||||||
|
a.asset_tag.toLowerCase().includes(q) ||
|
||||||
|
a.model_name.toLowerCase().includes(q) ||
|
||||||
|
(a.rental_user_name && a.rental_user_name.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
get sortedAssets() {
|
||||||
|
return [...this.filteredAssets].sort((a, b) => {
|
||||||
|
let v1 = a[this.sortKey] || '';
|
||||||
|
let v2 = b[this.sortKey] || '';
|
||||||
|
if (this.sortOrder === 'asc') return v1 > v2 ? 1 : -1;
|
||||||
|
return v1 < v2 ? 1 : -1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
sortBy(key) {
|
||||||
|
if (this.sortKey === key) {
|
||||||
|
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
this.sortKey = key;
|
||||||
|
this.sortOrder = 'asc';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openRentalModal(asset) {
|
||||||
|
this.selectedAsset = asset;
|
||||||
|
this.rentalName = asset.rental_user_name || '';
|
||||||
|
this.showRentalModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
submitRental() {
|
||||||
|
fetch('api.php?action=update_rental', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: this.selectedAsset.id,
|
||||||
|
rental_user_name: this.rentalName
|
||||||
|
})
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.showRentalModal = false;
|
||||||
|
this.fetchAssets();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
openHistoryModal(asset) {
|
||||||
|
this.selectedAsset = asset;
|
||||||
|
fetch(`api.php?action=get_asset_history&asset_id=${asset.id}`).then(res => res.json()).then(data => {
|
||||||
|
this.history = data;
|
||||||
|
this.showHistoryModal = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue