jasan/general_rental.php
2026-03-04 21:15:35 +09:00

448 lines
No EOL
24 KiB
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);
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</head>
<body class="bg-[#f8fafc] text-slate-800" x-data="generalRental()">
<?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 group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-blue-500 transition-colors"
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>
<input type="text" x-model="searchQuery"
class="block w-64 pl-10 pr-3 py-2.5 border border-slate-200 rounded-xl leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-sm shadow-sm"
placeholder="사원명, 품목 검색...">
</div>
<button @click="openAddModal()"
class="px-6 py-3 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all flex items-center gap-2 shrink-0">
<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="M12 4v16m8-8H4" />
</svg>
임대 등록
</button>
</div>
</header>
<!-- Rental Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
x-show="!loading && filteredRentals.length > 0">
<template x-for="rental in filteredRentals" :key="rental.id">
<div class="bg-white rounded-[2rem] p-6 shadow-sm border border-slate-100 hover:shadow-xl transition-all relative overflow-hidden flex flex-col h-full"
:class="rental.status === 'returned' ? 'opacity-75' : ''">
<!-- Status Ribbon for Returned -->
<template x-if="rental.status === 'returned'">
<div class="absolute top-0 right-0">
<div
class="bg-slate-500 p-1 px-4 text-[10px] font-black text-white uppercase transform rotate-45 translate-x-3 translate-y-1 shadow-sm">
Returned
</div>
</div>
</template>
<div class="flex items-center gap-4 mb-5">
<div class="w-12 h-12 rounded-2xl flex items-center justify-center shadow-inner"
:class="rental.status === 'returned' ? 'bg-slate-100 text-slate-400' : 'bg-emerald-50 text-emerald-500'">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div>
<h4 class="text-lg font-black text-slate-900" x-text="rental.user_name"></h4>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Rental Employee
</p>
</div>
</div>
<div class="flex-1 space-y-4">
<!-- Item List -->
<div>
<div
class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 flex items-center gap-2">
<div class="w-1 h-3 bg-blue-500 rounded-full"></div>
Rental Items
</div>
<div class="flex flex-wrap gap-1.5">
<template x-for="item in rental.items.split(', ')" :key="item">
<span
class="px-2 py-1 bg-blue-50 text-blue-600 text-[11px] font-black rounded-lg border border-blue-100"
x-text="item"></span>
</template>
</div>
</div>
<!-- Date Info -->
<div class="grid grid-cols-2 gap-4 pt-2 border-t border-slate-50">
<div>
<div class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Start
Date</div>
<div class="text-sm font-bold text-slate-700" x-text="rental.rental_start_date"></div>
</div>
<div>
<div class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Status
</div>
<template x-if="rental.status === 'rented'">
<div class="text-xs font-black text-blue-600 flex items-center gap-1.5">
<span class="w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse"></span>
<span x-text="calculateElapsed(rental.rental_start_date) + '일째'"></span>
</div>
</template>
<template x-if="rental.status === 'returned'">
<div class="text-xs font-black text-slate-400"
x-text="'반납 (' + rental.rental_return_date + ')'"></div>
</template>
</div>
</div>
<!-- Reason -->
<template x-if="rental.rental_reason">
<div class="bg-slate-50 p-3 rounded-xl border border-slate-100 mt-2">
<div class="text-[9px] font-black text-slate-400 uppercase mb-1 tracking-wider">Reason
</div>
<div class="text-[11px] text-slate-600 font-bold whitespace-pre-wrap leading-relaxed"
x-text="rental.rental_reason"></div>
</div>
</template>
</div>
<!-- Return Button -->
<template x-if="rental.status === 'rented'">
<button @click="returnRental(rental)"
class="mt-6 w-full py-3.5 bg-slate-900 text-white rounded-2xl font-black text-sm shadow-xl hover:bg-slate-800 transition-all active:scale-95 flex items-center justify-center gap-2">
<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="M7 16l-4-4m0 0l4-4m-4 4h18" />
</svg>
반납 처리
</button>
</template>
</div>
</template>
</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 && filteredRentals.length === 0"
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300">
<template x-if="searchQuery">
<p class="text-slate-400 font-medium italic">검색 결과와 일치하는 임대 기록이 없습니다.</p>
</template>
<template x-if="!searchQuery">
<p class="text-slate-400 font-medium italic">등록된 임대 기록이 없습니다.</p>
</template>
</div>
</main>
<!-- Add Rental Modal -->
<div x-show="showAddModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
<div class="fixed inset-0 modal-bg" @click="showAddModal = false"></div>
<div
class="bg-white rounded-[2.5rem] p-10 max-w-lg w-full relative z-[111] shadow-2xl flex flex-col max-h-[90vh]">
<h3 class="text-3xl font-black text-slate-900 mb-2">임대 등록</h3>
<p class="text-slate-400 text-xs font-bold mb-8 uppercase tracking-widest">Register New General Rental</p>
<form @submit.prevent="submitRental" class="space-y-6 overflow-y-auto pr-2 no-scrollbar">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="relative" x-on:click.outside="showUserDropdown = 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="userSearchQuery"
@focus="showUserDropdown = true; userSearchQuery = ''" @input="showUserDropdown = true"
placeholder="직원 검색 (이름, 사번)"
class="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm text-slate-700">
<div class="absolute right-3 top-3.5 flex items-center gap-2">
<button type="button" x-show="userSearchQuery" @click="clearUser()"
class="text-slate-400 hover:text-slate-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<!-- User Search Dropdown -->
<div x-show="showUserDropdown" x-transition
class="absolute z-[120] mt-2 w-full bg-white rounded-[1.5rem] shadow-2xl border border-slate-100 max-h-60 overflow-y-auto no-scrollbar">
<div class="p-2 border-b border-slate-50 sticky top-0 bg-white/90 backdrop-blur-sm">
<div class="text-[9px] font-black text-slate-400 uppercase px-2 py-1">Search Results
</div>
</div>
<div class="py-1">
<template x-for="user in filteredUsers" :key="user.id">
<div @click="selectUser(user)"
class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
<div>
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600"
x-text="user.name"></div>
<div class="text-[10px] text-slate-400 font-medium"
x-text="user.dept_name || '부서 정보 없음'"></div>
</div>
<div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400"
x-text="user.emp_id"></div>
</div>
</template>
<div x-show="filteredUsers.length === 0" class="px-4 py-8 text-center">
<p class="text-xs text-slate-400 italic">검색 결과가 없습니다.</p>
</div>
</div>
</div>
</div>
<div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 시작일</label>
<input type="date" x-model="formData.rental_start_date"
class="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm">
</div>
</div>
<div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-3 ml-1">임대 품목 (항목별
입력)</label>
<div class="space-y-3">
<template x-for="(item, index) in formData.items" :key="index">
<div class="flex items-center gap-2 group">
<div class="flex-1 relative">
<input type="text" x-model="formData.items[index]" placeholder="품목명을 적으세요..."
class="w-full pl-10 pr-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 transition-all group-hover:bg-white">
<div class="absolute left-3.5 top-3.5">
<input type="checkbox" checked disabled
class="w-3.5 h-3.5 text-blue-500 rounded border-slate-300">
</div>
</div>
<button type="button" @click="removeItem(index)" x-show="formData.items.length > 1"
class="p-3 text-slate-300 hover:text-rose-500 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="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>
<button type="button" @click="addItem()"
class="w-full py-3 bg-white border-2 border-dashed border-slate-200 rounded-xl text-xs font-black text-slate-400 hover:border-blue-400 hover:text-blue-500 transition-all flex items-center justify-center gap-2">
<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="M12 4v16m8-8H4" />
</svg>
항목 추가
</button>
</div>
</div>
<div>
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대 사유</label>
<textarea x-model="formData.rental_reason" rows="3" placeholder="임대 목적 및 사유를 입력하세요"
class="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none resize-none font-medium text-sm"></textarea>
</div>
<div class="pt-4 flex gap-3">
<button type="button" @click="showAddModal = 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-[2] py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-2xl shadow-blue-200 hover:bg-blue-700 transition-all active:scale-95">
임대 적용
</button>
</div>
</form>
</div>
</div>
<script>
function generalRental() {
return {
rentals: [],
users: [],
loading: true,
searchQuery: '',
showAddModal: false,
formData: {
user_id: '',
user_name: '',
rental_start_date: new Date().toISOString().split('T')[0],
rental_reason: '',
items: ['']
},
userSearchQuery: '',
showUserDropdown: false,
get filteredUsers() {
if (!this.userSearchQuery) return this.users.slice(0, 50);
const q = this.userSearchQuery.toLowerCase();
return this.users.filter(u =>
(u.name && u.name.toLowerCase().includes(q)) ||
(u.emp_id && u.emp_id.toLowerCase().includes(q)) ||
(u.dept_name && u.dept_name.toLowerCase().includes(q))
);
},
selectUser(user) {
this.formData.user_id = user.id;
this.userSearchQuery = `${user.name} (${user.emp_id})`;
this.showUserDropdown = false;
this.updateUserName();
},
clearUser() {
this.formData.user_id = '';
this.userSearchQuery = '';
this.updateUserName();
},
init() {
this.fetchRentals();
this.fetchUsers();
},
fetchRentals() {
const scrollPos = window.scrollY;
this.loading = true;
fetch('api.php?action=get_general_rentals').then(res => res.json()).then(data => {
this.rentals = data;
this.loading = false;
this.$nextTick(() => window.scrollTo(0, scrollPos));
});
},
get filteredRentals() {
if (!this.searchQuery) return this.rentals;
const q = this.searchQuery.toLowerCase();
return this.rentals.filter(r =>
(r.user_name && r.user_name.toLowerCase().includes(q)) ||
(r.items && r.items.toLowerCase().includes(q))
);
},
fetchUsers() {
fetch('api.php?action=get_users').then(res => res.json()).then(data => {
this.users = data;
});
},
openAddModal() {
this.formData = {
user_id: '',
user_name: '',
rental_start_date: new Date().toISOString().split('T')[0],
rental_reason: '',
items: ['']
};
this.userSearchQuery = '';
this.showAddModal = true;
},
addItem() {
this.formData.items.push('');
},
removeItem(index) {
this.formData.items.splice(index, 1);
},
updateUserName() {
const user = this.users.find(u => u.id == this.formData.user_id);
this.formData.user_name = user ? user.name : '';
},
submitRental() {
if (this.formData.items.every(item => !item.trim())) {
window.showAlert('최소 하나 이상의 품목을 입력해주세요.', '알림', 'warning');
return;
}
fetch('api.php?action=add_general_rental', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.formData)
}).then(res => res.json()).then(data => {
if (data.success) {
window.showAlert('임대 등록이 완료되었습니다.', '성공', 'success');
this.showAddModal = false;
this.fetchRentals();
} else {
window.showAlert(data.error || '오류가 발생했습니다.', '실패', 'error');
}
});
},
returnRental(rental) {
window.showConfirm(`${rental.user_name}님의 [${rental.items}] 품목 반납을 처리하시겠습니까?`, () => {
fetch('api.php?action=return_general_rental', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: rental.id })
}).then(res => res.json()).then(data => {
if (data.success) {
window.showAlert('반납 처리가 완료되었습니다.', '성공', 'success');
this.fetchRentals();
}
});
}, '반납 확인');
},
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;
}
}
}
</script>
</body>
</html>