696 lines
No EOL
40 KiB
PHP
696 lines
No EOL
40 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 JASAN</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-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 @click="openRemarksModal(item)"
|
|
class="text-sm font-black text-blue-600 px-2.5 py-1 bg-blue-50 rounded-lg cursor-pointer hover:bg-blue-600 hover:text-white transition-all shadow-sm border border-blue-100"
|
|
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">
|
|
<template x-if="item.rental_user_name">
|
|
<div class="flex items-start">
|
|
<div
|
|
class="w-2.5 h-2.5 bg-emerald-500 rounded-full mr-2.5 mt-1.5 shadow-[0_0_8px_rgba(16,185,129,0.5)]">
|
|
</div>
|
|
<div>
|
|
<span class="text-sm font-black text-slate-900"
|
|
x-text="item.rental_user_name"></span>
|
|
<div class="text-[10px] text-slate-400 font-bold max-w-[280px] whitespace-normal break-words"
|
|
x-text="item.rental_reason ? '사유: ' + item.rental_reason : ''"></div>
|
|
<template x-if="item.rental_peripherals">
|
|
<div class="flex flex-wrap gap-1 mt-1">
|
|
<template x-for="p in item.rental_peripherals.split(',')">
|
|
<span
|
|
class="px-1 py-0.5 bg-slate-100 text-[9px] text-slate-500 rounded border border-slate-200 font-bold"
|
|
x-text="p"></span>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template x-if="!item.rental_user_name">
|
|
<span class="text-xs text-slate-300 italic font-medium">임대 가능(STOCK)</span>
|
|
</template>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<template x-if="item.rental_user_name">
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-2">
|
|
<span
|
|
class="text-[10px] font-black text-slate-400 uppercase w-12">Start</span>
|
|
<span class="text-xs font-bold text-slate-700"
|
|
x-text="item.rental_start_date"></span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span
|
|
class="text-[10px] font-black text-slate-400 uppercase w-12">Elapsed</span>
|
|
<span
|
|
class="text-xs font-black text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded"
|
|
x-text="calculateElapsed(item.rental_start_date) + '일째'"></span>
|
|
</div>
|
|
<div class="flex items-center gap-2" x-show="item.rental_end_scheduled">
|
|
<span
|
|
class="text-[10px] font-black text-rose-400 uppercase w-12">Return</span>
|
|
<span class="text-xs font-bold text-rose-500"
|
|
x-text="item.rental_end_scheduled"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template x-if="!item.rental_user_name">
|
|
<span class="text-xs text-slate-200">-</span>
|
|
</template>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<div class="text-xs text-slate-500 truncate max-w-[200px]" 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">LOG</button>
|
|
<template x-if="item.rental_user_name">
|
|
<button @click="returnRental(item)"
|
|
class="px-3 py-1.5 text-[11px] font-black text-rose-600 bg-rose-50 hover:bg-rose-100 rounded-lg transition-all uppercase border border-rose-100">RETURN</button>
|
|
</template>
|
|
<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 ? 'EDIT' : 'RENTAL'"></button>
|
|
</div>
|
|
</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>
|
|
|
|
<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-[2rem] p-8 max-w-lg w-full relative z-[111] shadow-2xl max-h-[90vh] overflow-y-auto">
|
|
<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-5">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="relative" x-on:click.outside="showRentalUserDropdown = false">
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대사원 선택
|
|
(필수)</label>
|
|
<div class="relative">
|
|
<input type="text" x-model="rentalUserSearchQuery"
|
|
@focus="showRentalUserDropdown = true; rentalUserSearchQuery = ''"
|
|
@input="showRentalUserDropdown = true" placeholder="직원 검색 (이름, 사번)"
|
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm text-slate-700">
|
|
|
|
<div class="absolute right-3 top-3.5 flex items-center gap-2">
|
|
<button type="button" x-show="rentalUserSearchQuery" @click="clearRentalUser()"
|
|
class="text-slate-400 hover:text-slate-600">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor"
|
|
viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rental User Search Dropdown -->
|
|
<div x-show="showRentalUserDropdown" x-transition
|
|
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto no-scrollbar">
|
|
<div class="p-2 border-b border-slate-50 sticky top-0 bg-white/90 backdrop-blur-sm">
|
|
<div class="text-[9px] font-black text-slate-400 uppercase px-2 py-1">Search Results
|
|
</div>
|
|
</div>
|
|
<div class="py-1">
|
|
<template x-for="user in filteredRentalUsers" :key="user.id">
|
|
<div @click="selectRentalUser(user)"
|
|
class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
|
|
<div>
|
|
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600"
|
|
x-text="user.name"></div>
|
|
<div class="text-[10px] text-slate-400 font-medium"
|
|
x-text="user.dept_name || '부서 정보 없음'"></div>
|
|
</div>
|
|
<div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400"
|
|
x-text="user.emp_id"></div>
|
|
</div>
|
|
</template>
|
|
<div x-show="filteredRentalUsers.length === 0" class="px-4 py-8 text-center">
|
|
<p class="text-xs text-slate-400 italic">검색 결과가 없습니다.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 시작일</label>
|
|
<input type="date" x-model="rentalStartDate"
|
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none font-bold">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-3 ml-1">부속품 선택</label>
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<template x-for="item in ['어댑터', '마우스', '가방', '프리젠터', '모니터', '키보드']">
|
|
<label
|
|
class="flex items-center gap-2 p-3 bg-slate-50 border border-slate-200 rounded-xl cursor-pointer hover:bg-blue-50 hover:border-blue-200 transition-all group">
|
|
<input type="checkbox" :value="item" x-model="rentalPeripherals"
|
|
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
|
|
<span class="text-xs font-bold text-slate-600 group-hover:text-blue-600"
|
|
x-text="item"></span>
|
|
</label>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">반납 예정일</label>
|
|
<input type="date" x-model="rentalEndDate"
|
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none font-bold">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 사유</label>
|
|
<textarea x-model="rentalReason" rows="4" placeholder="임대 목적 및 사유를 입력하세요 (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 resize-none font-medium text-sm"></textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">비고 (노트북 자체
|
|
기록)</label>
|
|
<textarea x-model="rentalRemarks" 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 font-medium text-sm"></textarea>
|
|
</div>
|
|
|
|
<div class="pt-4 flex gap-3">
|
|
<button type="button" @click="showRentalModal = false"
|
|
class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold">취소</button>
|
|
<button type="submit"
|
|
class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 transition-all hover:bg-blue-700">
|
|
임대 적용
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Remarks Modal -->
|
|
<div x-show="showRemarksModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
|
<div class="fixed inset-0 modal-bg" @click="showRemarksModal = false"></div>
|
|
<div
|
|
class="bg-white rounded-[2rem] p-8 max-w-md w-full relative z-[111] shadow-2xl transform transition-all animate-in zoom-in-95 duration-200">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
|
|
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-black text-slate-900" x-text="selectedAsset?.asset_tag"></h3>
|
|
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">자산 상세 비고</p>
|
|
</div>
|
|
</div>
|
|
<button @click="showRemarksModal = false" class="text-slate-300 hover:text-slate-900 transition-colors">
|
|
<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>
|
|
|
|
<!-- Remarks Display/Editor Area -->
|
|
<div class="space-y-4">
|
|
<div class="bg-slate-50 rounded-2xl p-6 border border-slate-100 min-h-[160px] relative transition-all"
|
|
:class="isEditingRemarks ? 'ring-2 ring-blue-500 bg-white border-transparent' : ''">
|
|
<div class="flex justify-between items-center mb-3">
|
|
<div class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Remarks Content
|
|
</div>
|
|
<template x-if="!isEditingRemarks">
|
|
<div class="flex gap-2">
|
|
<button @click="startEditRemarks()"
|
|
class="p-1.5 text-blue-500 hover:bg-blue-50 rounded-lg transition-colors">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
</svg>
|
|
</button>
|
|
<button @click="deleteRemarks()"
|
|
class="p-1.5 text-rose-500 hover:bg-rose-50 rounded-lg transition-colors">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<template x-if="!isEditingRemarks">
|
|
<div class="text-slate-700 leading-relaxed font-semibold whitespace-pre-wrap"
|
|
x-text="selectedAsset?.remarks || '등록된 비고 사항이 없습니다.'"></div>
|
|
</template>
|
|
|
|
<template x-if="isEditingRemarks">
|
|
<textarea x-model="tempRemarks"
|
|
class="w-full h-32 bg-transparent border-none focus:ring-0 p-0 text-slate-700 font-semibold leading-relaxed resize-none"
|
|
placeholder="비고 내용을 입력하세요..."></textarea>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex gap-3">
|
|
<template x-if="isEditingRemarks">
|
|
<div class="flex gap-3 w-full">
|
|
<button @click="isEditingRemarks = false"
|
|
class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold hover:bg-slate-200 transition-all">
|
|
취소
|
|
</button>
|
|
<button @click="saveRemarks()"
|
|
class="flex-[2] py-4 bg-blue-600 text-white rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-lg shadow-blue-200">
|
|
저장하기
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<template x-if="!isEditingRemarks">
|
|
<button @click="showRemarksModal = false"
|
|
class="w-full py-4 bg-slate-900 text-white rounded-2xl font-bold hover:bg-slate-800 transition-all shadow-lg shadow-slate-200">
|
|
닫기
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</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="p-4 bg-emerald-50 rounded-2xl border border-emerald-100 flex flex-col gap-3">
|
|
<div class="flex items-center justify-between">
|
|
<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 shadow-sm border border-emerald-100"
|
|
x-text="log.action_date"></span>
|
|
</div>
|
|
<template x-if="log.note">
|
|
<div class="text-[11px] text-slate-500 font-bold bg-white/50 p-2 rounded-lg border border-emerald-100/50 whitespace-pre-wrap leading-relaxed"
|
|
x-text="log.note"></div>
|
|
</template>
|
|
</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,
|
|
showRemarksModal: false,
|
|
isEditingRemarks: false,
|
|
tempRemarks: '',
|
|
selectedAsset: null,
|
|
rentalUserId: '',
|
|
rentalName: '',
|
|
rentalStartDate: '',
|
|
rentalEndDate: '',
|
|
rentalReason: '',
|
|
rentalRemarks: '',
|
|
rentalPeripherals: [],
|
|
allUsers: [],
|
|
history: { rental: [], assignment: [] },
|
|
rentalUserSearchQuery: '',
|
|
showRentalUserDropdown: false,
|
|
|
|
get filteredRentalUsers() {
|
|
if (!this.rentalUserSearchQuery) return this.allUsers.slice(0, 50);
|
|
const q = this.rentalUserSearchQuery.toLowerCase();
|
|
return this.allUsers.filter(u =>
|
|
(u.name && u.name.toLowerCase().includes(q)) ||
|
|
(u.emp_id && u.emp_id.toLowerCase().includes(q)) ||
|
|
(u.dept_name && u.dept_name.toLowerCase().includes(q))
|
|
);
|
|
},
|
|
|
|
selectRentalUser(user) {
|
|
this.rentalUserId = user.id;
|
|
this.rentalUserSearchQuery = `${user.name} (${user.emp_id})`;
|
|
this.showRentalUserDropdown = false;
|
|
this.updateRentalName();
|
|
},
|
|
|
|
clearRentalUser() {
|
|
this.rentalUserId = '';
|
|
this.rentalUserSearchQuery = '';
|
|
this.updateRentalName();
|
|
},
|
|
|
|
init() {
|
|
this.fetchAssets();
|
|
this.fetchUsers();
|
|
},
|
|
|
|
fetchAssets() {
|
|
fetch('api.php?action=get_rental_laptops').then(res => res.json()).then(data => {
|
|
this.assets = data;
|
|
});
|
|
},
|
|
|
|
fetchUsers() {
|
|
fetch('api.php?action=get_users').then(res => res.json()).then(data => {
|
|
this.allUsers = 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.rentalUserId = asset.rental_user_id || '';
|
|
if (this.rentalUserId) {
|
|
const user = this.allUsers.find(u => u.id == this.rentalUserId);
|
|
this.rentalUserSearchQuery = user ? `${user.name} (${user.emp_id})` : asset.rental_user_name;
|
|
} else {
|
|
this.rentalUserSearchQuery = '';
|
|
}
|
|
this.rentalName = asset.rental_user_name || '';
|
|
this.rentalStartDate = asset.rental_start_date || new Date().toISOString().split('T')[0];
|
|
this.rentalEndDate = asset.rental_end_scheduled || '';
|
|
this.rentalReason = asset.rental_reason || '';
|
|
this.rentalRemarks = asset.remarks || '';
|
|
this.rentalPeripherals = asset.rental_peripherals ? asset.rental_peripherals.split(',') : [];
|
|
this.showRentalModal = true;
|
|
},
|
|
|
|
updateRentalName() {
|
|
const user = this.allUsers.find(u => u.id == this.rentalUserId);
|
|
if (user) {
|
|
this.rentalName = user.name;
|
|
} else {
|
|
this.rentalName = '';
|
|
}
|
|
},
|
|
|
|
submitRental() {
|
|
if (!this.rentalUserId) {
|
|
window.showAlert('임대할 직원을 선택해주세요.', '알림', 'warning');
|
|
return;
|
|
}
|
|
fetch('api.php?action=update_rental', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
id: this.selectedAsset.id,
|
|
rental_user_id: this.rentalUserId,
|
|
rental_user_name: this.rentalName,
|
|
rental_start_date: this.rentalStartDate,
|
|
rental_end_scheduled: this.rentalEndDate,
|
|
rental_reason: this.rentalReason,
|
|
rental_peripherals: this.rentalPeripherals.join(','),
|
|
remarks: this.rentalRemarks
|
|
})
|
|
}).then(res => res.json()).then(data => {
|
|
if (data.success) {
|
|
window.showAlert('임대 설정이 완료되었습니다.', '성공', 'success');
|
|
this.showRentalModal = false;
|
|
this.fetchAssets();
|
|
} else {
|
|
window.showAlert(data.error || '임대 적용 중 오류가 발생했습니다.', '오류', 'error');
|
|
}
|
|
}).catch(err => {
|
|
window.showAlert('서버와의 통신에 실패했습니다.', '통신 오류', 'error');
|
|
});
|
|
},
|
|
|
|
returnRental(asset) {
|
|
window.showConfirm(`${asset.rental_user_name}님의 노트북 반납을 처리하시겠습니까?`, () => {
|
|
fetch('api.php?action=return_rental', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: asset.id })
|
|
}).then(res => res.json()).then(data => {
|
|
if (data.success) {
|
|
window.showAlert('반납 처리가 완료되었습니다.', '성공', 'success');
|
|
this.fetchAssets();
|
|
}
|
|
});
|
|
}, '반납 확인');
|
|
},
|
|
|
|
calculateElapsed(startDate) {
|
|
if (!startDate) return 0;
|
|
const start = new Date(startDate);
|
|
const now = new Date();
|
|
const diffTime = Math.abs(now - start);
|
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
return diffDays + 1;
|
|
},
|
|
|
|
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;
|
|
});
|
|
},
|
|
|
|
openRemarksModal(asset) {
|
|
this.selectedAsset = asset;
|
|
this.isEditingRemarks = false;
|
|
this.tempRemarks = asset.remarks || '';
|
|
this.showRemarksModal = true;
|
|
},
|
|
|
|
startEditRemarks() {
|
|
this.tempRemarks = this.selectedAsset.remarks || '';
|
|
this.isEditingRemarks = true;
|
|
},
|
|
|
|
saveRemarks() {
|
|
fetch('api.php?action=update_asset_remarks', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
id: this.selectedAsset.id,
|
|
remarks: this.tempRemarks
|
|
})
|
|
}).then(res => res.json()).then(data => {
|
|
if (data.success) {
|
|
this.selectedAsset.remarks = this.tempRemarks;
|
|
this.isEditingRemarks = false;
|
|
this.fetchAssets();
|
|
}
|
|
});
|
|
},
|
|
|
|
deleteRemarks() {
|
|
window.showConfirm('비고 내용을 완전히 삭제하시겠습니까?', () => {
|
|
fetch('api.php?action=update_asset_remarks', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
id: this.selectedAsset.id,
|
|
remarks: ''
|
|
})
|
|
}).then(res => res.json()).then(data => {
|
|
if (data.success) {
|
|
this.selectedAsset.remarks = '';
|
|
this.tempRemarks = '';
|
|
this.fetchAssets();
|
|
}
|
|
});
|
|
}, '비고 삭제 확인');
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|