564 lines
No EOL
34 KiB
PHP
564 lines
No EOL
34 KiB
PHP
<?php require_once 'auth_check.php'; ?>
|
|
<!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="assetManagement()">
|
|
|
|
<?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">
|
|
<button @click="exportToExcel()"
|
|
class="bg-white border border-slate-200 text-slate-700 px-5 py-2.5 rounded-xl font-bold hover:bg-slate-50 transition-all flex items-center shadow-sm text-sm">
|
|
<svg class="w-4 h-4 mr-2 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
엑셀 내보내기
|
|
</button>
|
|
<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 text-sm">
|
|
자산 등록
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Bulk Actions (Fixed Floating Bar) -->
|
|
<div x-show="selectedIds.length > 0" x-transition x-cloak
|
|
class="fixed bottom-10 left-10 z-[200] bg-slate-900 text-white px-6 py-4 rounded-3xl flex items-center gap-6 shadow-2xl border border-white/10 animate-in fade-in slide-in-from-left-4 duration-300">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center font-black text-sm shadow-lg shadow-blue-500/30"
|
|
x-text="selectedIds.length"></div>
|
|
<div class="flex flex-col">
|
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-tighter">Selected</span>
|
|
<span class="text-xs font-bold">자산 선택됨</span>
|
|
</div>
|
|
</div>
|
|
<div class="h-8 w-px bg-white/10 mx-2"></div>
|
|
<div class="flex items-center gap-3">
|
|
<select x-model="bulkStatus"
|
|
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[150px]">
|
|
<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-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[200px]">
|
|
<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-600 hover:bg-blue-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">일괄
|
|
적용</button>
|
|
<button @click="selectedIds = []"
|
|
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 hover:text-white transition-all">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Laptop List Table -->
|
|
<div class="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-hidden"
|
|
x-show="!loading && assets.length > 0">
|
|
<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">
|
|
<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>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-50">
|
|
<template x-for="asset in sortedAssets" :key="asset.id">
|
|
<tr class="hover:bg-slate-50/80 transition-colors group">
|
|
<td class="px-6 py-4">
|
|
<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
|
|
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>
|
|
</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">
|
|
<div class="text-[11px] font-black text-slate-800" x-text="asset.model_name"></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 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.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>
|
|
</td>
|
|
<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-[10px] font-mono text-slate-400" x-text="asset.serial_number || '-'">
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center" x-show="asset.user_name">
|
|
<div class="w-7 h-7 bg-indigo-50 rounded-lg flex items-center justify-center text-[10px] font-bold text-indigo-500 mr-2"
|
|
x-text="asset.user_name ? asset.user_name.charAt(0) : ''"></div>
|
|
<div class="text-sm font-bold text-slate-700" x-text="asset.user_name"></div>
|
|
</div>
|
|
<div x-show="!asset.user_name" class="text-xs text-slate-400 font-medium">배정 대기</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-xs text-slate-500"
|
|
x-text="asset.last_confirmed_date || '-'"></td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span x-show="asset.user_name || asset.assigned_user_name"
|
|
:class="( (asset.user_name && asset.user_name.includes('업무용')) || (asset.assigned_user_name && asset.assigned_user_name.includes('업무용')) ) ? 'bg-amber-50 text-amber-600 border-amber-100' : 'bg-blue-50 text-blue-600 border-blue-100'"
|
|
class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter"
|
|
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>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div x-show="loading" class="p-20 flex justify-center">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
<div x-show="!loading && assets.length === 0"
|
|
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300">
|
|
<p class="text-slate-400 font-medium italic">등록된 노트북 자산이 없습니다.</p>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Asset Registration Modal -->
|
|
<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"
|
|
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">
|
|
<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">
|
|
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<form @submit.prevent="submitAsset" class="space-y-6">
|
|
<!-- 기본 정보 섹션 -->
|
|
<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_tag" required
|
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all font-bold">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 모델 연동</label>
|
|
<select x-model="formData.model_id" required @change="applyModelDefaults"
|
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all appearance-none">
|
|
<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>
|
|
</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>
|
|
|
|
<!-- 개별 자산 정보 섹션 -->
|
|
<div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></div>
|
|
<h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">Individual Asset Info
|
|
</h4>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">배정
|
|
사용자</label>
|
|
<select x-model="formData.current_user_id"
|
|
class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
|
|
<option value="">미배정 (재고)</option>
|
|
<template x-for="user in allUsers" :key="user.id">
|
|
<option :value="user.id" x-text="user.name + ' (' + user.emp_id + ')'"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP
|
|
주소</label>
|
|
<input type="text" x-model="formData.ip_address" placeholder="192.168.x.x"
|
|
class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">노트북
|
|
상태</label>
|
|
<input type="text" x-model="formData.asset_status" placeholder="예: 정상"
|
|
class="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
|
|
</div>
|
|
<div>
|
|
<label
|
|
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1 text-blue-600">실사
|
|
확인일</label>
|
|
<input type="date" x-model="formData.last_confirmed_date"
|
|
class="w-full px-4 py-3 bg-white border border-blue-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-blue-600">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="border-slate-100">
|
|
|
|
<!-- 상세 사양 섹션 (모델에서 기본값 상속 가능) -->
|
|
<div class="flex items-center justify-between">
|
|
<h4 class="text-sm font-black text-slate-400 uppercase tracking-widest">Hardware Specifications</h4>
|
|
<button type="button" @click="applyModelDefaults" x-show="formData.model_id"
|
|
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 class="bg-slate-50 p-4 rounded-xl space-y-3">
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase">Storage 1</label>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<input type="text" x-model="formData.hdd1_model" placeholder="모델명/타입"
|
|
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
|
<input type="text" x-model="formData.hdd1_capacity" placeholder="용량"
|
|
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 모델 기반 공통 정보 섹션 (Read Only) -->
|
|
<div class="bg-slate-50 p-6 rounded-[2rem] border border-slate-100 space-y-4">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<div class="w-1.5 h-1.5 rounded-full bg-slate-400"></div>
|
|
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">Model Base Info (Read
|
|
Only)</h4>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">취득일</label>
|
|
<input type="text" :value="formData.purchase_date" readonly
|
|
class="w-full px-4 py-3 bg-slate-100 border border-slate-200 rounded-xl text-sm font-bold text-slate-500 cursor-not-allowed">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">정격입출력</label>
|
|
<input type="text" :value="formData.power_rating" readonly
|
|
class="w-full px-4 py-3 bg-slate-100 border border-slate-200 rounded-xl text-sm font-bold text-slate-500 cursor-not-allowed">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">옵션</label>
|
|
<input type="text" :value="formData.options" readonly
|
|
class="w-full px-4 py-3 bg-slate-100 border border-slate-200 rounded-xl text-sm font-bold text-slate-500 cursor-not-allowed">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">비고 (특기사항)</label>
|
|
<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 transition-all"></textarea>
|
|
</div>
|
|
|
|
<div class="pt-4 flex gap-4">
|
|
<button type="button" @click="showModal = false"
|
|
class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold hover:bg-slate-200 transition-all">취소</button>
|
|
<button type="submit"
|
|
class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all"
|
|
x-text="isEdit ? '수정 완료' : '자산 등록하기'"></button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function assetManagement() {
|
|
return {
|
|
assets: [],
|
|
models: [],
|
|
allUsers: [],
|
|
loading: true,
|
|
showModal: false,
|
|
isEdit: false,
|
|
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() {
|
|
this.fetchAssets();
|
|
this.fetchModels();
|
|
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) {
|
|
window.showAlert('변경할 항목(상태 또는 모델)을 선택해주세요.', '선택 오류', 'error');
|
|
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) {
|
|
window.showAlert(`${this.selectedIds.length}개의 자산 정보가 일괄 변경되었습니다.`, '변경 성공', 'success');
|
|
this.selectedIds = [];
|
|
this.bulkStatus = '';
|
|
this.bulkModelId = '';
|
|
this.fetchAssets();
|
|
} else {
|
|
window.showAlert('자산 정보 일괄 변경에 실패했습니다.', '변경 실패', 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
fetchAssets() {
|
|
const scrollPos = window.scrollY;
|
|
this.loading = true;
|
|
fetch('api.php?action=get_laptops').then(res => res.json()).then(data => {
|
|
this.assets = data;
|
|
this.loading = false;
|
|
this.$nextTick(() => window.scrollTo(0, scrollPos));
|
|
});
|
|
},
|
|
|
|
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() {
|
|
fetch('api.php?action=get_models').then(res => res.json()).then(data => this.models = data);
|
|
},
|
|
|
|
fetchUsers() {
|
|
fetch('api.php?action=get_users').then(res => res.json()).then(data => this.allUsers = data);
|
|
},
|
|
|
|
applyModelDefaults() {
|
|
if (!this.formData.model_id) return;
|
|
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() {
|
|
this.isEdit = false;
|
|
this.formData = {
|
|
id: '', asset_tag: '', model_id: '', current_user_id: '', status: 'stock',
|
|
serial_number: '', purchase_date: new Date().toISOString().split('T')[0], last_confirmed_date: '', ip_address: '',
|
|
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;
|
|
},
|
|
|
|
editAsset(asset) {
|
|
this.isEdit = true;
|
|
this.formData = { ...asset };
|
|
this.showModal = true;
|
|
},
|
|
|
|
submitAsset() {
|
|
const action = this.isEdit ? 'update_laptop_asset' : 'add_laptop_asset';
|
|
fetch(`api.php?action=${action}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(this.formData)
|
|
}).then(res => res.json()).then(data => {
|
|
if (data.success) {
|
|
this.showModal = false;
|
|
this.fetchAssets();
|
|
}
|
|
});
|
|
},
|
|
|
|
exportToExcel() {
|
|
if (this.assets.length === 0) return;
|
|
const headers = ['자산관리번호', '모델명', '제조사', '프로세서', 'RAM', 'SSD', 'IP주소', '시리얼', '사용자', '상태', '취득일'];
|
|
const rows = this.assets.map(a => [
|
|
a.asset_tag, a.model_name, a.manufacturer, a.processor || '', a.ram || '', a.storage || '',
|
|
a.ip_address || '', a.serial_number || '', a.user_name || '미배정', a.status === 'assigned' ? '지급됨' : '재고', a.purchase_date || ''
|
|
]);
|
|
let csvContent = "\uFEFF" + headers.join(",") + "\n" + rows.map(r => r.map(c => `"${c}"`).join(",")).join("\n");
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.setAttribute("href", url);
|
|
link.setAttribute("download", `FKI_Laptop_Assets_${new Date().toISOString().split('T')[0]}.csv`);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|