feat: 관리자 인증 시스템을 도입하고 사용자 관리 페이지에 분류, 노트북, 단기 임대 정보를 추가하며 일반 대여 기능을 구현했습니다.
This commit is contained in:
parent
3f789c81ac
commit
5e7a53e731
15 changed files with 1971 additions and 229 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
# [업로드 방지] 로컬 테스트 파일들이 GitLab으로 올라가지 않게 함
|
# [업로드 방지] 로컬 테스트 파일들이 GitLab으로 올라가지 않게 함
|
||||||
dda/, ex/
|
dda/, ex/
|
||||||
*.sqlite
|
# *.sqlite
|
||||||
# *.db
|
# *.db
|
||||||
231
admins.php
Normal file
231
admins.php
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
<?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>관리자 관리 | 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;800&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="adminManagement()">
|
||||||
|
|
||||||
|
<?php include 'nav.php'; ?>
|
||||||
|
|
||||||
|
<main class="max-w-[1200px] 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">System Administrators</h2>
|
||||||
|
<p class="text-slate-500 mt-1">시스템 관리자 계정 생성, 삭제 및 비밀번호 관리</p>
|
||||||
|
</div>
|
||||||
|
<button @click="openAddModal()"
|
||||||
|
class="px-6 py-3 bg-slate-900 text-white rounded-2xl font-bold shadow-lg shadow-slate-200 hover:bg-slate-800 transition-all flex items-center gap-2">
|
||||||
|
<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="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
||||||
|
</svg>
|
||||||
|
관리자 추가
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<template x-for="admin in admins" :key="admin.id">
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-[2rem] p-8 shadow-sm border border-slate-100 hover:shadow-xl transition-all relative overflow-hidden group">
|
||||||
|
<div
|
||||||
|
class="absolute top-0 right-0 w-32 h-32 bg-slate-50 rounded-full -translate-y-16 translate-x-16 group-hover:scale-150 transition-transform duration-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative z-10">
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 bg-slate-100 rounded-2xl flex items-center justify-center mb-6 shadow-inner text-slate-400 group-hover:bg-blue-600 group-hover:text-white transition-colors">
|
||||||
|
<svg class="w-8 h-8" 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 class="mb-6">
|
||||||
|
<h3 class="text-xl font-black text-slate-900" x-text="admin.admin_name"></h3>
|
||||||
|
<p class="text-[10px] font-bold text-blue-500 uppercase tracking-[0.2em] mt-1"
|
||||||
|
x-text="admin.login_id"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between py-3 border-t border-slate-50">
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Created</span>
|
||||||
|
<span class="text-xs font-bold text-slate-600"
|
||||||
|
x-text="admin.created_at.split(' ')[0]"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 pt-2">
|
||||||
|
<button @click="openPasswordModal(admin)"
|
||||||
|
class="flex-1 py-3 bg-slate-100 text-slate-600 rounded-xl font-bold text-xs hover:bg-slate-200 transition-all">PW
|
||||||
|
변경</button>
|
||||||
|
<button @click="deleteAdmin(admin)"
|
||||||
|
class="flex-1 py-3 bg-rose-50 text-rose-500 rounded-xl font-bold text-xs hover:bg-rose-500 hover:text-white transition-all border border-rose-100">계정
|
||||||
|
삭제</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Add Admin 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-sm w-full relative z-[111] shadow-2xl">
|
||||||
|
<h3 class="text-2xl font-black text-slate-900 mb-8">관리자 계정 생성</h3>
|
||||||
|
<form @submit.prevent="addAdmin" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">이름</label>
|
||||||
|
<input type="text" x-model="formData.admin_name" required placeholder="User Name"
|
||||||
|
class="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">아이디</label>
|
||||||
|
<input type="text" x-model="formData.login_id" required placeholder="Login ID"
|
||||||
|
class="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">초기 비밀번호</label>
|
||||||
|
<input type="password" x-model="formData.password" required placeholder="Password"
|
||||||
|
class="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm">
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 pt-4">
|
||||||
|
<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-1 py-4 bg-blue-600 text-white rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-lg shadow-blue-200">생성하기</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password Management Modal -->
|
||||||
|
<div x-show="showPwModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
|
<div class="fixed inset-0 modal-bg" @click="showPwModal = false"></div>
|
||||||
|
<div class="bg-white rounded-[2.5rem] p-10 max-w-sm w-full relative z-[111] shadow-2xl">
|
||||||
|
<h3 class="text-2xl font-black text-slate-900 mb-2">비밀번호 변경</h3>
|
||||||
|
<p class="text-blue-500 font-bold text-xs mb-8"
|
||||||
|
x-text="selectedAdmin?.admin_name + ' (' + selectedAdmin?.login_id + ')'"></p>
|
||||||
|
<form @submit.prevent="updatePassword" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">새 비밀번호</label>
|
||||||
|
<input type="password" x-model="newPassword" required placeholder="New Password"
|
||||||
|
class="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm">
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 pt-4">
|
||||||
|
<button type="button" @click="showPwModal = 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-slate-900 text-white rounded-2xl font-bold hover:bg-slate-800 transition-all shadow-lg">변경하기</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function adminManagement() {
|
||||||
|
return {
|
||||||
|
admins: [],
|
||||||
|
showAddModal: false,
|
||||||
|
showPwModal: false,
|
||||||
|
selectedAdmin: null,
|
||||||
|
newPassword: '',
|
||||||
|
formData: { admin_name: '', login_id: '', password: '' },
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchAdmins();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchAdmins() {
|
||||||
|
fetch('api.php?action=get_admins').then(res => res.json()).then(data => {
|
||||||
|
this.admins = data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
openAddModal() {
|
||||||
|
this.formData = { admin_name: '', login_id: '', password: '' };
|
||||||
|
this.showAddModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
addAdmin() {
|
||||||
|
fetch('api.php?action=add_admin', {
|
||||||
|
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.fetchAdmins();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
openPasswordModal(admin) {
|
||||||
|
this.selectedAdmin = admin;
|
||||||
|
this.newPassword = '';
|
||||||
|
this.showPwModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePassword() {
|
||||||
|
fetch('api.php?action=update_admin_password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: this.selectedAdmin.id, password: this.newPassword })
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.showAlert('비밀번호가 성공적으로 변경되었습니다.', '성공', 'success');
|
||||||
|
this.showPwModal = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteAdmin(admin) {
|
||||||
|
window.showConfirm(`${admin.admin_name} 계정을 정말로 삭제하시겠습니까?`, () => {
|
||||||
|
fetch('api.php?action=delete_admin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: admin.id })
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.showAlert('계정이 삭제되었습니다.', '완료', 'success');
|
||||||
|
this.fetchAdmins();
|
||||||
|
} else {
|
||||||
|
window.showAlert(data.error, '실패', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, '계정 삭제 확인');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
423
api.php
423
api.php
|
|
@ -1,17 +1,81 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* api.php
|
|
||||||
* FKI Asset System RESTful API
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
$action = $_GET['action'] ?? '';
|
$action = $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
// 로그인 세션 체크 (login 액션 제외)
|
||||||
|
$public_actions = ['login'];
|
||||||
|
if (!isset($_SESSION['admin_id']) && !in_array($action, $public_actions)) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
|
case 'login':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$login_id = $data['login_id'] ?? '';
|
||||||
|
$password = $data['password'] ?? '';
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT * FROM administrators WHERE login_id = ?");
|
||||||
|
$stmt->execute([$login_id]);
|
||||||
|
$admin = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($admin && password_verify($password, $admin['password_hash'])) {
|
||||||
|
$_SESSION['admin_id'] = $admin['id'];
|
||||||
|
$_SESSION['admin_login_id'] = $admin['login_id'];
|
||||||
|
$_SESSION['admin_name'] = $admin['admin_name'];
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => '아이디 또는 비밀번호가 일치하지 않습니다.']);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'logout':
|
||||||
|
session_destroy();
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_admins':
|
||||||
|
$stmt = $db->query("SELECT id, login_id, admin_name, created_at FROM administrators ORDER BY id ASC");
|
||||||
|
echo json_encode($stmt->fetchAll());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'add_admin':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$stmt = $db->prepare("INSERT INTO administrators (login_id, password_hash, admin_name) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([
|
||||||
|
$data['login_id'],
|
||||||
|
password_hash($data['password'], PASSWORD_DEFAULT),
|
||||||
|
$data['admin_name']
|
||||||
|
]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_admin_password':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$stmt = $db->prepare("UPDATE administrators SET password_hash = ? WHERE id = ?");
|
||||||
|
$stmt->execute([
|
||||||
|
password_hash($data['password'], PASSWORD_DEFAULT),
|
||||||
|
$data['id']
|
||||||
|
]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'delete_admin':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if ($data['id'] == $_SESSION['admin_id']) {
|
||||||
|
echo json_encode(['success' => false, 'error' => '자기 자신은 삭제할 수 없습니다.']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$stmt = $db->prepare("DELETE FROM administrators WHERE id = ?");
|
||||||
|
$stmt->execute([$data['id']]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
case 'get_dashboard_stats':
|
case 'get_dashboard_stats':
|
||||||
// Laptops
|
// Laptops
|
||||||
$laptop_total = $db->query("SELECT COUNT(*) FROM laptop_assets")->fetchColumn();
|
$laptop_total = $db->query("SELECT COUNT(*) FROM laptop_assets")->fetchColumn();
|
||||||
|
|
@ -39,14 +103,19 @@ try {
|
||||||
|
|
||||||
case 'get_users':
|
case 'get_users':
|
||||||
$query = "WITH RECURSIVE dept_path(id, path) AS (
|
$query = "WITH RECURSIVE dept_path(id, path) AS (
|
||||||
SELECT id, name FROM departments WHERE parent_id IS NULL
|
SELECT id, name FROM departments WHERE parent_id IS NULL
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT d.id, dp.path || ' > ' || d.name
|
SELECT d.id, dp.path || ' > ' || d.name
|
||||||
FROM departments d JOIN dept_path dp ON d.parent_id = dp.id
|
FROM departments d JOIN dept_path dp ON d.parent_id = dp.id
|
||||||
)
|
)
|
||||||
SELECT u.*, dp.path as dept_name
|
SELECT u.*, dp.path as dept_name,
|
||||||
FROM users u
|
(SELECT asset_tag FROM laptop_assets WHERE current_user_id = u.id ORDER BY id DESC LIMIT 1) as laptop_tag,
|
||||||
LEFT JOIN dept_path dp ON u.department_id = dp.id";
|
(SELECT purchase_date FROM laptop_assets WHERE current_user_id = u.id ORDER BY id DESC LIMIT 1) as laptop_purchase_date,
|
||||||
|
(SELECT m.model_name FROM laptop_assets a JOIN laptop_models m ON a.model_id = m.id WHERE a.current_user_id = u.id ORDER
|
||||||
|
BY a.id DESC LIMIT 1) as laptop_model_name,
|
||||||
|
(SELECT COUNT(*) FROM laptop_assets WHERE rental_user_id = u.id) as rental_count
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN dept_path dp ON u.department_id = dp.id";
|
||||||
$search = $_GET['search'] ?? '';
|
$search = $_GET['search'] ?? '';
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$query .= " WHERE u.name LIKE :search OR u.emp_id LIKE :search OR u.email LIKE :search";
|
$query .= " WHERE u.name LIKE :search OR u.emp_id LIKE :search OR u.email LIKE :search";
|
||||||
|
|
@ -61,7 +130,16 @@ try {
|
||||||
case 'add_user':
|
case 'add_user':
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
$status = $data['status'] ?? 'active';
|
$status = $data['status'] ?? 'active';
|
||||||
$department_id = $data['department_id'];
|
$department_id = $data['department_id'] ?: null;
|
||||||
|
$emp_id = $data['emp_id'];
|
||||||
|
|
||||||
|
// Duplicate emp_id check
|
||||||
|
$check = $db->prepare("SELECT id FROM users WHERE emp_id = ?");
|
||||||
|
$check->execute([$emp_id]);
|
||||||
|
if ($check->fetch()) {
|
||||||
|
echo json_encode(['success' => false, 'error' => "이미 존재하는 사번(${emp_id})입니다."]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-relocate based on status
|
// Auto-relocate based on status
|
||||||
if ($status === 'on_leave' || $status === 'retired') {
|
if ($status === 'on_leave' || $status === 'retired') {
|
||||||
|
|
@ -71,25 +149,40 @@ try {
|
||||||
$department_id = $target_dept;
|
$department_id = $target_dept;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $db->prepare("INSERT INTO users (emp_id, name, department_id, position, email, phone, mobile, accounting_type, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
try {
|
||||||
$stmt->execute([
|
$stmt = $db->prepare("INSERT INTO users (emp_id, name, department_id, position, email, phone, mobile, accounting_type,
|
||||||
$data['emp_id'],
|
status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$data['name'],
|
$stmt->execute([
|
||||||
$department_id,
|
$emp_id,
|
||||||
$data['position'],
|
$data['name'],
|
||||||
$data['email'],
|
$department_id,
|
||||||
$data['phone'] ?? '',
|
$data['position'],
|
||||||
$data['mobile'],
|
$data['email'],
|
||||||
$data['accounting_type'],
|
$data['phone'] ?? '',
|
||||||
$status
|
$data['mobile'],
|
||||||
]);
|
$data['accounting_type'],
|
||||||
echo json_encode(['success' => true]);
|
$status
|
||||||
|
]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'update_user':
|
case 'update_user':
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
$status = $data['status'];
|
$status = $data['status'];
|
||||||
$department_id = $data['department_id'];
|
$department_id = $data['department_id'] ?: null;
|
||||||
|
$emp_id = $data['emp_id'];
|
||||||
|
$user_id = $data['id'];
|
||||||
|
|
||||||
|
// Duplicate emp_id check (excluding current user)
|
||||||
|
$check = $db->prepare("SELECT id FROM users WHERE emp_id = ? AND id != ?");
|
||||||
|
$check->execute([$emp_id, $user_id]);
|
||||||
|
if ($check->fetch()) {
|
||||||
|
echo json_encode(['success' => false, 'error' => "이미 존재하는 사번(${emp_id})입니다."]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-relocate based on status
|
// Auto-relocate based on status
|
||||||
if ($status === 'on_leave' || $status === 'retired') {
|
if ($status === 'on_leave' || $status === 'retired') {
|
||||||
|
|
@ -99,55 +192,125 @@ try {
|
||||||
$department_id = $target_dept;
|
$department_id = $target_dept;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $db->prepare("UPDATE users SET emp_id = ?, name = ?, department_id = ?, position = ?, email = ?, phone = ?, mobile = ?, accounting_type = ?, status = ? WHERE id = ?");
|
try {
|
||||||
$stmt->execute([
|
$stmt = $db->prepare("UPDATE users SET emp_id = ?, name = ?, department_id = ?, position = ?, email = ?, phone = ?,
|
||||||
$data['emp_id'],
|
mobile = ?, accounting_type = ?, status = ? WHERE id = ?");
|
||||||
$data['name'],
|
$stmt->execute([
|
||||||
$department_id,
|
$emp_id,
|
||||||
$data['position'],
|
$data['name'],
|
||||||
$data['email'],
|
$department_id,
|
||||||
$data['phone'] ?? '',
|
$data['position'],
|
||||||
$data['mobile'],
|
$data['email'],
|
||||||
$data['accounting_type'],
|
$data['phone'] ?? '',
|
||||||
$status,
|
$data['mobile'],
|
||||||
$data['id']
|
$data['accounting_type'],
|
||||||
]);
|
$status,
|
||||||
echo json_encode(['success' => true]);
|
$user_id
|
||||||
|
]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'get_laptops':
|
case 'get_laptops':
|
||||||
$query = "SELECT a.*, m.model_name, m.manufacturer, m.specs, m.processor, m.ram, m.storage, u.name as user_name
|
$query = "SELECT a.*, m.model_name, m.manufacturer, m.specs, m.processor, m.ram, m.storage, u.name as user_name
|
||||||
FROM laptop_assets a
|
FROM laptop_assets a
|
||||||
LEFT JOIN laptop_models m ON a.model_id = m.id
|
LEFT JOIN laptop_models m ON a.model_id = m.id
|
||||||
LEFT JOIN users u ON a.current_user_id = u.id";
|
LEFT JOIN users u ON a.current_user_id = u.id";
|
||||||
echo json_encode($db->query($query)->fetchAll());
|
echo json_encode($db->query($query)->fetchAll());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'get_rental_laptops':
|
case 'get_rental_laptops':
|
||||||
$query = "SELECT a.*, m.model_name, m.manufacturer, m.product_name
|
$query = "SELECT a.*, m.model_name, m.manufacturer, m.product_name
|
||||||
FROM laptop_assets a
|
FROM laptop_assets a
|
||||||
LEFT JOIN laptop_models m ON a.model_id = m.id
|
LEFT JOIN laptop_models m ON a.model_id = m.id
|
||||||
WHERE a.assigned_user_name LIKE '%업무용%'";
|
WHERE (a.assigned_user_name LIKE '%업무용%' OR a.current_user_id IS NULL)";
|
||||||
echo json_encode($db->query($query)->fetchAll());
|
echo json_encode($db->query($query)->fetchAll());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'update_rental':
|
case 'update_rental':
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
$stmt = $db->prepare("UPDATE laptop_assets SET rental_user_name = ? WHERE id = ?");
|
$startDate = $data['rental_start_date'] ?: date('Y-m-d');
|
||||||
$stmt->execute([$data['rental_user_name'], $data['id']]);
|
|
||||||
|
$stmt = $db->prepare("UPDATE laptop_assets SET
|
||||||
|
rental_user_id = ?,
|
||||||
|
rental_user_name = ?,
|
||||||
|
rental_start_date = ?,
|
||||||
|
rental_end_scheduled = ?,
|
||||||
|
rental_reason = ?,
|
||||||
|
rental_peripherals = ?,
|
||||||
|
remarks = ?
|
||||||
|
WHERE id = ?");
|
||||||
|
$stmt->execute([
|
||||||
|
$data['rental_user_id'] ?? null,
|
||||||
|
$data['rental_user_name'],
|
||||||
|
$startDate,
|
||||||
|
$data['rental_end_scheduled'] ?? null,
|
||||||
|
$data['rental_reason'] ?? '',
|
||||||
|
$data['rental_peripherals'] ?? '',
|
||||||
|
$data['remarks'] ?? '',
|
||||||
|
$data['id']
|
||||||
|
]);
|
||||||
|
|
||||||
// Log the rental action
|
// Log the rental action
|
||||||
if ($data['rental_user_name']) {
|
$peripherals = $data['rental_peripherals'] ? "\n부속품: " . $data['rental_peripherals'] : "";
|
||||||
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'rental', ?, ?)");
|
$note = "사유: " . ($data['rental_reason'] ?? '없음') . $peripherals;
|
||||||
$log_stmt->execute([$data['id'], $data['rental_user_name'], date('Y-m-d')]);
|
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date, note) VALUES (?,
|
||||||
|
'rental', ?, ?, ?)");
|
||||||
|
$log_stmt->execute([$data['id'], $data['rental_user_name'], $startDate, $note]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'return_rental':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$asset = $db->query("SELECT * FROM laptop_assets WHERE id = " . (int) $data['id'])->fetch();
|
||||||
|
|
||||||
|
if ($asset) {
|
||||||
|
// 반납 처리: 임대 정보는 초기화하되 비고(remarks)는 유지
|
||||||
|
$stmt = $db->prepare("UPDATE laptop_assets SET
|
||||||
|
rental_user_id = NULL,
|
||||||
|
rental_user_name = NULL,
|
||||||
|
rental_start_date = NULL,
|
||||||
|
rental_end_scheduled = NULL,
|
||||||
|
rental_reason = NULL,
|
||||||
|
rental_peripherals = NULL
|
||||||
|
WHERE id = ?");
|
||||||
|
$stmt->execute([$data['id']]);
|
||||||
|
|
||||||
|
// Log return
|
||||||
|
$log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date, note) VALUES (?,
|
||||||
|
'return', ?, ?, ?)");
|
||||||
|
$log_stmt->execute([$data['id'], $asset['rental_user_name'], date('Y-m-d'), '반납 완료']);
|
||||||
}
|
}
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'get_user_rentals':
|
||||||
|
$user_id = (int) $_GET['user_id'];
|
||||||
|
$query = "SELECT a.*, m.model_name, m.manufacturer
|
||||||
|
FROM laptop_assets a
|
||||||
|
JOIN laptop_models m ON a.model_id = m.id
|
||||||
|
WHERE a.rental_user_id = ?";
|
||||||
|
$stmt = $db->prepare($query);
|
||||||
|
$stmt->execute([$user_id]);
|
||||||
|
echo json_encode($stmt->fetchAll());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_asset_remarks':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$stmt = $db->prepare("UPDATE laptop_assets SET remarks = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$data['remarks'] ?? '', $data['id']]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'get_asset_history':
|
case 'get_asset_history':
|
||||||
$asset_id = $_GET['asset_id'];
|
$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();
|
$rental_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'rental' ORDER BY id
|
||||||
$assign_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'assignment' ORDER BY id DESC LIMIT 20")->fetchAll();
|
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([
|
echo json_encode([
|
||||||
'rental' => $rental_logs,
|
'rental' => $rental_logs,
|
||||||
'assignment' => $assign_logs
|
'assignment' => $assign_logs
|
||||||
|
|
@ -197,10 +360,10 @@ 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 (
|
$stmt = $db->prepare("INSERT INTO laptop_models (
|
||||||
model_name, manufacturer, specs, cpu, npu, hdd0_model, hdd0_capacity,
|
model_name, manufacturer, specs, cpu, npu, hdd0_model, hdd0_capacity,
|
||||||
hdd1_model, hdd1_capacity, ram, asset_status, assigned_user, fixed_ip,
|
hdd1_model, hdd1_capacity, ram, asset_status, assigned_user, fixed_ip,
|
||||||
remarks, options, power_rating, purchase_date, vendor, product_name
|
remarks, options, power_rating, purchase_date, vendor, product_name
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$data['model_name'],
|
$data['model_name'],
|
||||||
$data['manufacturer'],
|
$data['manufacturer'],
|
||||||
|
|
@ -227,13 +390,13 @@ try {
|
||||||
|
|
||||||
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
|
$stmt = $db->prepare("UPDATE laptop_models SET
|
||||||
model_name = ?, manufacturer = ?, specs = ?, cpu = ?, npu = ?,
|
model_name = ?, manufacturer = ?, specs = ?, cpu = ?, npu = ?,
|
||||||
hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?,
|
hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?,
|
||||||
ram = ?, asset_status = ?, assigned_user = ?, fixed_ip = ?,
|
ram = ?, asset_status = ?, assigned_user = ?, fixed_ip = ?,
|
||||||
remarks = ?, options = ?, power_rating = ?, purchase_date = ?,
|
remarks = ?, options = ?, power_rating = ?, purchase_date = ?,
|
||||||
vendor = ?, product_name = ?
|
vendor = ?, product_name = ?
|
||||||
WHERE id = ?");
|
WHERE id = ?");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$data['model_name'],
|
$data['model_name'],
|
||||||
$data['manufacturer'],
|
$data['manufacturer'],
|
||||||
|
|
@ -261,26 +424,27 @@ try {
|
||||||
|
|
||||||
case 'get_departments':
|
case 'get_departments':
|
||||||
$sql = "WITH RECURSIVE dept_path(id, name, path, level, parent_id) AS (
|
$sql = "WITH RECURSIVE dept_path(id, name, path, level, parent_id) AS (
|
||||||
SELECT id, name, name, level, parent_id FROM departments WHERE parent_id IS NULL
|
SELECT id, name, name, level, parent_id FROM departments WHERE parent_id IS NULL
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT d.id, d.name, dp.path || ' > ' || d.name, d.level, d.parent_id
|
SELECT d.id, d.name, dp.path || ' > ' || d.name, d.level, d.parent_id
|
||||||
FROM departments d
|
FROM departments d
|
||||||
JOIN dept_path dp ON d.parent_id = dp.id
|
JOIN dept_path dp ON d.parent_id = dp.id
|
||||||
)
|
)
|
||||||
SELECT dp.*,
|
SELECT dp.*,
|
||||||
(SELECT COUNT(*) FROM users WHERE department_id = dp.id) as member_count
|
(SELECT COUNT(*) FROM users WHERE department_id = dp.id) as member_count
|
||||||
FROM dept_path dp
|
FROM dept_path dp
|
||||||
ORDER BY path";
|
ORDER BY path";
|
||||||
echo json_encode($db->query($sql)->fetchAll());
|
echo json_encode($db->query($sql)->fetchAll());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
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 (
|
$stmt = $db->prepare("INSERT INTO laptop_assets (
|
||||||
asset_tag, model_id, current_user_id, status, serial_number, purchase_date, ip_address,
|
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,
|
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
|
asset_status, assigned_user_name, fixed_ip, options, power_rating, manufacturer, vendor, product_name, remarks,
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
last_confirmed_date
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$data['asset_tag'],
|
$data['asset_tag'],
|
||||||
$data['model_id'],
|
$data['model_id'],
|
||||||
|
|
@ -309,7 +473,8 @@ try {
|
||||||
]);
|
]);
|
||||||
$new_asset_id = $db->lastInsertId();
|
$new_asset_id = $db->lastInsertId();
|
||||||
if ($data['assigned_user_name']) {
|
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 = $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')]);
|
$log_stmt->execute([$new_asset_id, $data['assigned_user_name'], date('Y-m-d')]);
|
||||||
}
|
}
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|
@ -323,11 +488,12 @@ try {
|
||||||
$old_stmt->execute([$data['id']]);
|
$old_stmt->execute([$data['id']]);
|
||||||
$old_asset = $old_stmt->fetch();
|
$old_asset = $old_stmt->fetch();
|
||||||
|
|
||||||
$stmt = $db->prepare("UPDATE laptop_assets SET
|
$stmt = $db->prepare("UPDATE laptop_assets SET
|
||||||
asset_tag = ?, model_id = ?, current_user_id = ?, status = ?, serial_number = ?, purchase_date = ?, ip_address = ?,
|
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 = ?,
|
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 = ?
|
asset_status = ?, assigned_user_name = ?, fixed_ip = ?, options = ?, power_rating = ?, manufacturer = ?, vendor = ?,
|
||||||
WHERE id = ?");
|
product_name = ?, remarks = ?, last_confirmed_date = ?
|
||||||
|
WHERE id = ?");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$data['asset_tag'],
|
$data['asset_tag'],
|
||||||
$data['model_id'],
|
$data['model_id'],
|
||||||
|
|
@ -358,7 +524,8 @@ try {
|
||||||
|
|
||||||
// Auto-log assignment change
|
// Auto-log assignment change
|
||||||
if ($data['assigned_user_name'] && $data['assigned_user_name'] !== ($old_asset['assigned_user_name'] ?? '')) {
|
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 = $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')]);
|
$log_stmt->execute([$data['id'], $data['assigned_user_name'], date('Y-m-d')]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,7 +576,8 @@ try {
|
||||||
$fki_root_id = $fki_root ? $fki_root['id'] : null;
|
$fki_root_id = $fki_root ? $fki_root['id'] : null;
|
||||||
|
|
||||||
// 1. Find the '미소속' department under FKI Root
|
// 1. Find the '미소속' department under FKI Root
|
||||||
$miso_id = $db->query("SELECT id FROM departments WHERE name = '미소속' AND parent_id IS " . ($fki_root_id ?: 'NULL') . " LIMIT 1")->fetchColumn();
|
$miso_id = $db->query("SELECT id FROM departments WHERE name = '미소속' AND parent_id IS " . ($fki_root_id ?: 'NULL') . "
|
||||||
|
LIMIT 1")->fetchColumn();
|
||||||
|
|
||||||
if (!$miso_id) {
|
if (!$miso_id) {
|
||||||
$db->prepare("INSERT INTO departments (name, parent_id, level) VALUES ('미소속', ?, 1)")->execute([$fki_root_id]);
|
$db->prepare("INSERT INTO departments (name, parent_id, level) VALUES ('미소속', ?, 1)")->execute([$fki_root_id]);
|
||||||
|
|
@ -473,7 +641,11 @@ try {
|
||||||
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$sql = "UPDATE laptop_assets SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
|
$sql = "UPDATE laptop_assets SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(
|
||||||
|
0,
|
||||||
|
count($ids),
|
||||||
|
"?"
|
||||||
|
)) . ")";
|
||||||
$stmt = $db->prepare($sql);
|
$stmt = $db->prepare($sql);
|
||||||
$stmt->execute(array_merge($params, $ids));
|
$stmt->execute(array_merge($params, $ids));
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|
@ -502,7 +674,11 @@ try {
|
||||||
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$sql = "UPDATE access_cards SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
|
$sql = "UPDATE access_cards SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(
|
||||||
|
0,
|
||||||
|
count($ids),
|
||||||
|
"?"
|
||||||
|
)) . ")";
|
||||||
$stmt = $db->prepare($sql);
|
$stmt = $db->prepare($sql);
|
||||||
$stmt->execute(array_merge($params, $ids));
|
$stmt->execute(array_merge($params, $ids));
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|
@ -531,7 +707,11 @@ try {
|
||||||
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$sql = "UPDATE mfp_accounts SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
|
$sql = "UPDATE mfp_accounts SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(
|
||||||
|
0,
|
||||||
|
count($ids),
|
||||||
|
"?"
|
||||||
|
)) . ")";
|
||||||
$stmt = $db->prepare($sql);
|
$stmt = $db->prepare($sql);
|
||||||
$stmt->execute(array_merge($params, $ids));
|
$stmt->execute(array_merge($params, $ids));
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|
@ -560,7 +740,11 @@ try {
|
||||||
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
echo json_encode(['success' => false, 'error' => 'No updates specified']);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$sql = "UPDATE laptop_models SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")";
|
$sql = "UPDATE laptop_models SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(
|
||||||
|
0,
|
||||||
|
count($ids),
|
||||||
|
"?"
|
||||||
|
)) . ")";
|
||||||
$stmt = $db->prepare($sql);
|
$stmt = $db->prepare($sql);
|
||||||
$stmt->execute(array_merge($params, $ids));
|
$stmt->execute(array_merge($params, $ids));
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
|
|
@ -615,6 +799,55 @@ try {
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'get_general_rentals':
|
||||||
|
$query = "SELECT r.*, GROUP_CONCAT(i.item_name, ', ') as items
|
||||||
|
FROM general_rentals r
|
||||||
|
LEFT JOIN general_rental_items i ON r.id = i.rental_id
|
||||||
|
GROUP BY r.id
|
||||||
|
ORDER BY r.id DESC";
|
||||||
|
echo json_encode($db->query($query)->fetchAll());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'add_general_rental':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$startDate = $data['rental_start_date'] ?: date('Y-m-d');
|
||||||
|
|
||||||
|
$db->beginTransaction();
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("INSERT INTO general_rentals (user_id, user_name, rental_start_date, rental_reason, status) VALUES
|
||||||
|
(?, ?, ?, ?, 'rented')");
|
||||||
|
$stmt->execute([
|
||||||
|
$data['user_id'] ?? null,
|
||||||
|
$data['user_name'],
|
||||||
|
$startDate,
|
||||||
|
$data['rental_reason'] ?? ''
|
||||||
|
]);
|
||||||
|
$rentalId = $db->lastInsertId();
|
||||||
|
|
||||||
|
if (!empty($data['items'])) {
|
||||||
|
$itemStmt = $db->prepare("INSERT INTO general_rental_items (rental_id, item_name) VALUES (?, ?)");
|
||||||
|
foreach ($data['items'] as $itemName) {
|
||||||
|
if (trim($itemName)) {
|
||||||
|
$itemStmt->execute([$rentalId, trim($itemName)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$db->commit();
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$db->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'return_general_rental':
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$returnDate = date('Y-m-d');
|
||||||
|
$stmt = $db->prepare("UPDATE general_rentals SET status = 'returned', rental_return_date = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$returnDate, $data['id']]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
echo json_encode(['error' => 'Invalid action']);
|
echo json_encode(['error' => 'Invalid action']);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
BIN
assets.db
BIN
assets.db
Binary file not shown.
10
auth_check.php
Normal file
10
auth_check.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_SESSION['admin_id'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?php require_once 'auth_check.php'; ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
||||||
|
|
|
||||||
340
general_rental.php
Normal file
340
general_rental.php
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
<!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>
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Rental Cards Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
<template x-for="rental in rentals" :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>
|
||||||
|
</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>
|
||||||
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대사원 선택
|
||||||
|
(필수)</label>
|
||||||
|
<select x-model="formData.user_id" required @change="updateUserName()"
|
||||||
|
class="w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm">
|
||||||
|
<option value="">직원 선택</option>
|
||||||
|
<template x-for="user in users" :key="user.id">
|
||||||
|
<option :value="user.id"
|
||||||
|
x-text="user.name + ' (' + (user.dept_name ? user.dept_name.split(' > ').pop() : '미소속') + ')'">
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</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: [],
|
||||||
|
showAddModal: false,
|
||||||
|
formData: {
|
||||||
|
user_id: '',
|
||||||
|
user_name: '',
|
||||||
|
rental_start_date: new Date().toISOString().split('T')[0],
|
||||||
|
rental_reason: '',
|
||||||
|
items: ['']
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchRentals();
|
||||||
|
this.fetchUsers();
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchRentals() {
|
||||||
|
fetch('api.php?action=get_general_rentals').then(res => res.json()).then(data => {
|
||||||
|
this.rentals = data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
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.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>
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?php require_once 'auth_check.php'; ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
||||||
|
|
|
||||||
107
laptops.php
107
laptops.php
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?php require_once 'auth_check.php'; ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
||||||
|
|
@ -251,32 +252,44 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<!-- 개별 자산 정보 섹션 -->
|
||||||
<div>
|
<div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4">
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">배정 사용자</label>
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<select x-model="formData.current_user_id"
|
<div class="w-1.5 h-1.5 rounded-full bg-blue-500"></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 transition-all">
|
<h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">Individual Asset Info
|
||||||
<option value="">미배정 (재고)</option>
|
</h4>
|
||||||
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">고정 IP 주소</label>
|
<div>
|
||||||
<input type="text" x-model="formData.ip_address" placeholder="192.168.x.x"
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">배정
|
||||||
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">
|
사용자</label>
|
||||||
</div>
|
<select x-model="formData.current_user_id"
|
||||||
<div>
|
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">
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">취득일</label>
|
<option value="">미배정 (재고)</option>
|
||||||
<input type="date" x-model="formData.purchase_date"
|
<template x-for="user in allUsers" :key="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="user.id" x-text="user.name + ' (' + user.emp_id + ')'"></option>
|
||||||
</div>
|
</template>
|
||||||
<div>
|
</select>
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2 text-blue-600">실사
|
</div>
|
||||||
확인일</label>
|
<div>
|
||||||
<input type="date" x-model="formData.last_confirmed_date"
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP
|
||||||
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">
|
주소</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -330,21 +343,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<!-- 모델 기반 공통 정보 섹션 (Read Only) -->
|
||||||
<div>
|
<div class="bg-slate-50 p-6 rounded-[2rem] border border-slate-100 space-y-4">
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">노트북 상태</label>
|
<div class="flex items-center gap-2 mb-2">
|
||||||
<input type="text" x-model="formData.asset_status"
|
<div class="w-1.5 h-1.5 rounded-full bg-slate-400"></div>
|
||||||
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
|
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">Model Base Info (Read
|
||||||
|
Only)</h4>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">정격입출력</label>
|
<div>
|
||||||
<input type="text" x-model="formData.power_rating"
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">취득일</label>
|
||||||
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
|
<input type="text" :value="formData.purchase_date" readonly
|
||||||
</div>
|
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-xs font-bold text-slate-500 uppercase mb-2">옵션</label>
|
<div>
|
||||||
<input type="text" x-model="formData.options"
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">정격입출력</label>
|
||||||
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm">
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|
@ -401,7 +422,10 @@
|
||||||
|
|
||||||
applyBulkUpdate() {
|
applyBulkUpdate() {
|
||||||
if (this.selectedIds.length === 0) return;
|
if (this.selectedIds.length === 0) return;
|
||||||
if (!this.bulkStatus && !this.bulkModelId) { alert('변경할 항목을 선택해주세요.'); return; }
|
if (!this.bulkStatus && !this.bulkModelId) {
|
||||||
|
window.showAlert('변경할 항목(상태 또는 모델)을 선택해주세요.', '선택 오류', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
fetch('api.php?action=bulk_update_laptops', {
|
fetch('api.php?action=bulk_update_laptops', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -413,10 +437,13 @@
|
||||||
})
|
})
|
||||||
}).then(res => res.json()).then(data => {
|
}).then(res => res.json()).then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
window.showAlert(`${this.selectedIds.length}개의 자산 정보가 일괄 변경되었습니다.`, '변경 성공', 'success');
|
||||||
this.selectedIds = [];
|
this.selectedIds = [];
|
||||||
this.bulkStatus = '';
|
this.bulkStatus = '';
|
||||||
this.bulkModelId = '';
|
this.bulkModelId = '';
|
||||||
this.fetchAssets();
|
this.fetchAssets();
|
||||||
|
} else {
|
||||||
|
window.showAlert('자산 정보 일괄 변경에 실패했습니다.', '변경 실패', 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
123
login.php
Normal file
123
login.php
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if (isset($_SESSION['admin_id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ASSET | Login</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;800&display=swap"
|
||||||
|
rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Pretendard', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-[#0f172a] min-h-screen flex items-center justify-center p-6 relative overflow-hidden text-slate-800">
|
||||||
|
<!-- Animated background elements -->
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full opacity-20 pointer-events-none">
|
||||||
|
<div
|
||||||
|
class="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-600 rounded-full blur-[120px] animate-pulse">
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-indigo-600 rounded-full blur-[120px] animate-pulse"
|
||||||
|
style="animation-delay: 2s;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full max-w-md relative z-10" x-data="{
|
||||||
|
login_id: '',
|
||||||
|
password: '',
|
||||||
|
loading: false,
|
||||||
|
error: '',
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
try {
|
||||||
|
const res = await fetch('api.php?action=login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ login_id: this.login_id, password: this.password })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
location.href = 'index.php';
|
||||||
|
} else {
|
||||||
|
this.error = data.error || '로그인에 실패했습니다.';
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.error = '서버 통신 오류가 발생했습니다.';
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<div class="text-center mb-10">
|
||||||
|
<h1 class="text-6xl font-black text-white tracking-tighter mb-2 italic">ASSET</h1>
|
||||||
|
<p class="text-blue-400 font-bold uppercase tracking-[0.3em] text-[10px]">Management System v2.0</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass rounded-[2.5rem] p-10 shadow-2xl">
|
||||||
|
<h2 class="text-2xl font-black text-slate-900 mb-8">Sign In</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="submit" class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1 tracking-widest">Admin
|
||||||
|
ID</label>
|
||||||
|
<input type="text" x-model="login_id" required placeholder="Enter ID"
|
||||||
|
class="w-full px-5 py-4 bg-slate-100/50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold transition-all">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1 tracking-widest">Password</label>
|
||||||
|
<input type="password" x-model="password" required placeholder="••••••••"
|
||||||
|
class="w-full px-5 py-4 bg-slate-100/50 border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none font-bold transition-all">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="error" x-transition x-cloak
|
||||||
|
class="p-4 bg-rose-50 border border-rose-100 rounded-2xl text-rose-600 text-xs font-bold"
|
||||||
|
x-text="error"></div>
|
||||||
|
|
||||||
|
<button type="submit" :disabled="loading"
|
||||||
|
class="w-full py-5 bg-blue-600 text-white rounded-2xl font-black text-sm shadow-xl shadow-blue-500/20 hover:bg-blue-700 transition-all active:scale-95 disabled:opacity-50 disabled:pointer-events-none flex items-center justify-center gap-2">
|
||||||
|
<template x-if="loading">
|
||||||
|
<svg class="animate-spin h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
|
||||||
|
</circle>
|
||||||
|
<path class="opacity-75" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<span x-text="loading ? 'Authenticating...' : 'Access Console'"></span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-8 text-center text-slate-500 text-[10px] font-bold uppercase tracking-widest">
|
||||||
|
Forbidden access is strictly monitored
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
1
mfp.php
1
mfp.php
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?php require_once 'auth_check.php'; ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
||||||
|
|
|
||||||
127
models.php
127
models.php
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?php require_once 'auth_check.php'; ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
||||||
|
|
@ -289,23 +290,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 자산 정보 -->
|
<!-- 자산 정보 (취득일) -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
<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" 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>
|
<div>
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2 text-blue-600">취득일</label>
|
<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"
|
<input type="date" x-model="formData.purchase_date"
|
||||||
|
|
@ -376,6 +362,13 @@
|
||||||
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold"
|
<button type="submit" class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold"
|
||||||
x-text="isDeptEdit ? '수정 완료' : '부서 생성'"></button>
|
x-text="isDeptEdit ? '수정 완료' : '부서 생성'"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div x-show="isDeptEdit" class="pt-4 mt-4 border-t border-slate-100">
|
||||||
|
<button type="button" @click="deleteDept()"
|
||||||
|
class="w-full py-3 text-sm font-bold text-rose-500 hover:text-white hover:bg-rose-500 border border-rose-200 rounded-xl transition-all">
|
||||||
|
이 부서 삭제하기
|
||||||
|
</button>
|
||||||
|
<p class="text-[10px] text-slate-400 mt-2 text-center">※ 삭제 시 소속 인원은 '미소속'으로 이동됩니다.</p>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -401,20 +394,30 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-slate-50">
|
<tbody class="divide-y divide-slate-50">
|
||||||
<template x-for="user in members" :key="user.id">
|
<template x-for="user in members" :key="user.id">
|
||||||
<tr>
|
<tr :class="getPositionInfo(user.position).bgClass"
|
||||||
<td class="px-4 py-3 text-sm font-bold text-slate-900" x-text="user.name"></td>
|
:style="getPositionInfo(user.position).gradientStyle">
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="text-xs font-black text-slate-400" x-text="user.emp_id"></div>
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-[10px] text-blue-500 font-bold uppercase"
|
<div class="text-sm font-black text-slate-900" x-text="user.name"></div>
|
||||||
x-text="user.position || '-'"></div>
|
<template x-if="getPositionInfo(user.position).isVIP">
|
||||||
|
<span
|
||||||
|
class="text-[9px] px-1.5 py-0.5 rounded-md font-black uppercase tracking-tighter"
|
||||||
|
:class="getPositionInfo(user.position).tagClass"
|
||||||
|
x-text="user.position"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] font-bold text-slate-400" x-text="user.emp_id"></div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="text-xs font-black text-slate-900" x-text="user.position || '-'"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-4 py-3 text-right">
|
||||||
<select @change="reassignMember(user.id, $event.target.value)"
|
<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">
|
class="text-[10px] font-bold bg-slate-50 border-none rounded-lg px-2 py-1 outline-none focus:ring-1 focus:ring-blue-500">
|
||||||
<option value="">부서 이동...</option>
|
<option value="">부서 이동...</option>
|
||||||
<template x-for="d in depts" :key="d.id">
|
<template x-for="d in depts" :key="d.id">
|
||||||
<option :value="d.id" x-text="d.name" :disabled="d.id == selectedDept.id">
|
<option :value="d.id" x-text="d.name"
|
||||||
</option>
|
:selected="d.id == user.department_id"></option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -480,7 +483,10 @@
|
||||||
},
|
},
|
||||||
applyBulkUpdate() {
|
applyBulkUpdate() {
|
||||||
if (this.selectedIds.length === 0) return;
|
if (this.selectedIds.length === 0) return;
|
||||||
if (!this.bulkManufacturer && !this.bulkProductName) { alert('변경할 항목을 선택해주세요.'); return; }
|
if (!this.bulkManufacturer && !this.bulkProductName) {
|
||||||
|
window.showAlert('변경할 항목(제조사 또는 품명)을 입력해주세요.', '입력 오류', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
fetch('api.php?action=bulk_update_models', {
|
fetch('api.php?action=bulk_update_models', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|
@ -503,7 +509,7 @@
|
||||||
this.formData = {
|
this.formData = {
|
||||||
id: '', manufacturer: '', model_name: '', product_name: '',
|
id: '', manufacturer: '', model_name: '', product_name: '',
|
||||||
cpu: '', npu: '', ram: '', hdd0_model: '', hdd0_capacity: '', hdd1_model: '', hdd1_capacity: '',
|
cpu: '', npu: '', ram: '', hdd0_model: '', hdd0_capacity: '', hdd1_model: '', hdd1_capacity: '',
|
||||||
asset_status: '', fixed_ip: '', assigned_user: '', purchase_date: '',
|
purchase_date: '',
|
||||||
power_rating: '', vendor: '', options: '', remarks: '', specs: ''
|
power_rating: '', vendor: '', options: '', remarks: '', specs: ''
|
||||||
};
|
};
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
|
|
@ -569,10 +575,49 @@
|
||||||
viewDeptMembers(dept) {
|
viewDeptMembers(dept) {
|
||||||
this.selectedDept = dept;
|
this.selectedDept = dept;
|
||||||
fetch(`api.php?action=get_dept_users&dept_id=${dept.id}`).then(res => res.json()).then(data => {
|
fetch(`api.php?action=get_dept_users&dept_id=${dept.id}`).then(res => res.json()).then(data => {
|
||||||
this.members = data;
|
// 직위 랭킹 정렬 적용
|
||||||
|
const getRank = (pos) => {
|
||||||
|
if (!pos) return 99;
|
||||||
|
if (pos.includes('부회장')) return 1;
|
||||||
|
if (pos.includes('총괄')) return 2;
|
||||||
|
if (pos.includes('원장')) return 3;
|
||||||
|
if (pos.includes('센터장')) return 4;
|
||||||
|
if (pos.includes('본부장')) return 5;
|
||||||
|
if (pos.includes('실장')) return 6;
|
||||||
|
if (pos.includes('부문장')) return 7;
|
||||||
|
if (pos.includes('팀장')) return 8;
|
||||||
|
return 90;
|
||||||
|
};
|
||||||
|
this.members = data.sort((a, b) => getRank(a.position) - getRank(b.position));
|
||||||
this.showMembersModal = true;
|
this.showMembersModal = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getPositionInfo(pos) {
|
||||||
|
if (!pos) return { bgClass: 'bg-white', iconClass: 'bg-slate-100 text-slate-500', isVIP: false };
|
||||||
|
|
||||||
|
const configs = [
|
||||||
|
{ key: '부회장', from: '#fbbf24', to: '#d97706', icon: 'bg-amber-100 text-amber-600', tag: 'bg-amber-500 text-white' },
|
||||||
|
{ key: '총괄', from: '#3b82f6', to: '#1d4ed8', icon: 'bg-blue-100 text-blue-600', tag: 'bg-blue-600 text-white' },
|
||||||
|
{ key: '원장', from: '#10b981', to: '#059669', icon: 'bg-emerald-100 text-emerald-600', tag: 'bg-emerald-600 text-white' },
|
||||||
|
{ key: '센터장', from: '#06b6d4', to: '#0891b2', icon: 'bg-cyan-100 text-cyan-600', tag: 'bg-cyan-600 text-white' },
|
||||||
|
{ key: '본부장', from: '#8b5cf6', to: '#7c3aed', icon: 'bg-purple-100 text-purple-600', tag: 'bg-purple-600 text-white' },
|
||||||
|
{ key: '실장', from: '#64748b', to: '#475569', icon: 'bg-slate-100 text-slate-600', tag: 'bg-slate-600 text-white' },
|
||||||
|
{ key: '부문장', from: '#f43f5e', to: '#e11d48', icon: 'bg-rose-100 text-rose-600', tag: 'bg-rose-600 text-white' },
|
||||||
|
{ key: '팀장', from: '#6366f1', to: '#4f46e5', icon: 'bg-indigo-100 text-indigo-600', tag: 'bg-indigo-600 text-white' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const config = configs.find(c => pos.includes(c.key));
|
||||||
|
if (config) {
|
||||||
|
return {
|
||||||
|
isVIP: true,
|
||||||
|
gradientStyle: `background: linear-gradient(135deg, ${config.from}08 0%, ${config.to}05 100%)`,
|
||||||
|
bgClass: 'hover:bg-opacity-50',
|
||||||
|
iconClass: config.icon,
|
||||||
|
tagClass: config.tag
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { bgClass: 'bg-white', iconClass: 'bg-slate-100 text-slate-500', isVIP: false };
|
||||||
|
},
|
||||||
reassignMember(userId, newDeptId) {
|
reassignMember(userId, newDeptId) {
|
||||||
if (!newDeptId) return;
|
if (!newDeptId) return;
|
||||||
fetch('api.php?action=reassign_user', {
|
fetch('api.php?action=reassign_user', {
|
||||||
|
|
@ -585,6 +630,32 @@
|
||||||
this.fetchDepts();
|
this.fetchDepts();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
deleteDept() {
|
||||||
|
const deptId = this.deptFormData.id;
|
||||||
|
const deptName = this.deptFormData.name;
|
||||||
|
|
||||||
|
window.showConfirm(
|
||||||
|
`'${deptName}' 부서를 정말 삭제하시겠습니까?\n삭제 후 소속 인원은 '미소속' 부서로 이동하며, 하위 부서들은 상위 계층으로 재배치됩니다.`,
|
||||||
|
() => {
|
||||||
|
fetch(`api.php?action=delete_dept&id=${deptId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.showAlert('부서가 성공적으로 삭제되었습니다.', '성공', 'success');
|
||||||
|
this.showDeptModal = false;
|
||||||
|
this.fetchDepts();
|
||||||
|
} else {
|
||||||
|
window.showAlert('부서 삭제 중 오류: ' + (data.error || '알 수 없는 오류'), '삭제 실패', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
window.showAlert('서버 통신 중 오류가 발생했습니다.', '시스템 오류', 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'부서 영구 삭제'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
114
nav.php
114
nav.php
|
|
@ -28,11 +28,13 @@ $current_page = basename($_SERVER['PHP_SELF']);
|
||||||
$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' => 'rental.php', 'name' => '노트북 임대', 'icon' => 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4'],
|
||||||
|
['url' => 'general_rental.php', 'name' => '임대', 'icon' => 'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 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' => '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'],
|
||||||
['url' => 'models.php', 'name' => 'DB 관리', 'icon' => 'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4']
|
['url' => 'models.php', 'name' => 'DB 관리', 'icon' => 'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4'],
|
||||||
|
['url' => 'admins.php', 'name' => 'ADMIN', 'icon' => 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z']
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($menu_items as $item):
|
foreach ($menu_items as $item):
|
||||||
|
|
@ -51,15 +53,29 @@ $current_page = basename($_SERVER['PHP_SELF']);
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Info -->
|
<!-- User Info & Logout -->
|
||||||
<div class="hidden lg:flex items-center ml-10 space-x-4 border-l border-slate-700 pl-10">
|
<div class="hidden lg:flex items-center ml-10 space-x-4 border-l border-slate-700 pl-10" x-data="{
|
||||||
|
logout() {
|
||||||
|
window.showConfirm('로그아웃 하시겠습니까?', () => {
|
||||||
|
fetch('api.php?action=logout').then(() => location.href = 'login.php');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}">
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-xs font-black text-white">ADMIN MASTER</p>
|
<p class="text-xs font-black text-white"><?php echo $_SESSION['admin_name'] ?? 'ADMIN'; ?></p>
|
||||||
<p class="text-[10px] text-blue-400 font-bold uppercase tracking-tight">System Online</p>
|
<button @click="logout()"
|
||||||
|
class="text-[10px] text-blue-400 font-bold uppercase tracking-tight hover:text-white transition-colors flex items-center gap-1">
|
||||||
|
Sign Out
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="w-10 h-10 bg-slate-800 rounded-full flex items-center justify-center border border-slate-700 shadow-inner">
|
class="w-10 h-10 bg-slate-800 rounded-full flex items-center justify-center border border-slate-700 shadow-inner">
|
||||||
<span class="text-xs font-black text-slate-300">AD</span>
|
<span
|
||||||
|
class="text-xs font-black text-slate-300"><?php echo strtoupper(substr($_SESSION['admin_login_id'] ?? 'AD', 0, 2)); ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -86,4 +102,88 @@ $current_page = basename($_SERVER['PHP_SELF']);
|
||||||
body {
|
body {
|
||||||
padding-top: 80px;
|
padding-top: 80px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Global Notification System (Antigravity Standard) -->
|
||||||
|
<div x-data="{
|
||||||
|
show: false,
|
||||||
|
type: 'alert',
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
onConfirm: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
window.showAlert = (msg, title = '알림', type = 'alert') => {
|
||||||
|
this.type = type;
|
||||||
|
this.title = title;
|
||||||
|
this.message = msg;
|
||||||
|
this.onConfirm = null;
|
||||||
|
this.show = true;
|
||||||
|
};
|
||||||
|
window.showConfirm = (msg, callback, title = '확인 요청') => {
|
||||||
|
this.type = 'confirm';
|
||||||
|
this.title = title;
|
||||||
|
this.message = msg;
|
||||||
|
this.onConfirm = callback;
|
||||||
|
this.show = true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
confirmAction() {
|
||||||
|
if (this.onConfirm) this.onConfirm();
|
||||||
|
this.show = false;
|
||||||
|
}
|
||||||
|
}" x-show="show" x-cloak class="fixed inset-0 z-[1000] flex items-center justify-center p-4">
|
||||||
|
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" @click="show = false"></div>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-[2.5rem] p-8 max-w-sm w-full relative z-[1001] shadow-[0_32px_64px_-12px_rgba(0,0,0,0.2)] border border-white/20 transform transition-all animate-in zoom-in-95 duration-200">
|
||||||
|
<div class="mb-6 text-center">
|
||||||
|
<div class="w-20 h-20 rounded-3xl mb-6 flex items-center justify-center mx-auto transition-transform hover:scale-110 duration-300"
|
||||||
|
:class="{
|
||||||
|
'bg-amber-50 text-amber-500': type === 'confirm',
|
||||||
|
'bg-rose-50 text-rose-500': type === 'error',
|
||||||
|
'bg-emerald-50 text-emerald-500': type === 'success',
|
||||||
|
'bg-blue-50 text-blue-500': type === 'alert'
|
||||||
|
}">
|
||||||
|
<template x-if="type === 'confirm'">
|
||||||
|
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<template x-if="type === 'error'">
|
||||||
|
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<template x-if="type === 'success'">
|
||||||
|
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<template x-if="type === 'alert'">
|
||||||
|
<svg class="w-10 h-10" 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>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-2xl font-black text-slate-900 mb-3 tracking-tight" x-text="title"></h4>
|
||||||
|
<p class="text-sm font-bold text-slate-500 leading-relaxed whitespace-pre-line px-2" x-text="message"></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 mt-8">
|
||||||
|
<template x-if="type === 'confirm'">
|
||||||
|
<button @click="show = false"
|
||||||
|
class="flex-1 py-4 bg-slate-100 rounded-2xl font-black text-sm text-slate-600 hover:bg-slate-200 transition-all active:scale-95">취소</button>
|
||||||
|
</template>
|
||||||
|
<button @click="confirmAction"
|
||||||
|
class="flex-1 py-4 rounded-2xl font-black text-sm text-white shadow-2xl transition-all active:scale-95"
|
||||||
|
:class="{
|
||||||
|
'bg-amber-500 shadow-amber-200 hover:bg-amber-600': type === 'confirm',
|
||||||
|
'bg-rose-500 shadow-rose-200 hover:bg-rose-600': type === 'error',
|
||||||
|
'bg-emerald-500 shadow-emerald-200 hover:bg-emerald-600': type === 'success',
|
||||||
|
'bg-blue-600 shadow-blue-200 hover:bg-blue-700': type === 'alert'
|
||||||
|
}" x-text="type === 'confirm' ? '확인 및 실행' : '확인'"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
392
rental.php
392
rental.php
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?php require_once 'auth_check.php'; ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
||||||
|
|
@ -67,6 +68,9 @@
|
||||||
현재 임대사원 <span x-show="sortKey === 'rental_user_name'"
|
현재 임대사원 <span x-show="sortKey === 'rental_user_name'"
|
||||||
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
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">
|
<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-right text-xs font-bold text-slate-500 uppercase tracking-wider">액션
|
<th class="px-6 py-4 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">액션
|
||||||
|
|
@ -77,7 +81,8 @@
|
||||||
<template x-for="item in sortedAssets" :key="item.id">
|
<template x-for="item in sortedAssets" :key="item.id">
|
||||||
<tr class="hover:bg-slate-50/80 transition-colors 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 whitespace-nowrap">
|
||||||
<span class="text-sm font-black text-slate-900 px-2.5 py-1 bg-slate-100 rounded-lg"
|
<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>
|
x-text="item.asset_tag"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
|
@ -88,65 +93,253 @@
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<template x-if="item.rental_user_name">
|
<template x-if="item.rental_user_name">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-2 h-2 bg-emerald-500 rounded-full mr-2"></div>
|
<div
|
||||||
<span class="text-sm font-black text-slate-900"
|
class="w-2.5 h-2.5 bg-emerald-500 rounded-full mr-2.5 shadow-[0_0_8px_rgba(16,185,129,0.5)]">
|
||||||
x-text="item.rental_user_name"></span>
|
</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"
|
||||||
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!item.rental_user_name">
|
<template x-if="!item.rental_user_name">
|
||||||
<span class="text-xs text-slate-300 italic font-medium">임대 가능</span>
|
<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>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="text-xs text-slate-500 truncate max-w-xs" x-text="item.remarks || '-'">
|
<div class="text-xs text-slate-500 truncate max-w-[200px]" x-text="item.remarks || '-'">
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button @click="openHistoryModal(item)"
|
<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>
|
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)"
|
<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="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"
|
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>
|
x-text="item.rental_user_name ? 'EDIT' : 'RENTAL'"></button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
</template>
|
</tr>
|
||||||
</tbody>
|
</template>
|
||||||
</table>
|
</tbody>
|
||||||
<div x-show="filteredAssets.length === 0" class="py-20 text-center">
|
</table>
|
||||||
<div class="text-slate-300 text-5xl mb-4 italic">No Assets Found</div>
|
<div x-show="filteredAssets.length === 0" class="py-20 text-center">
|
||||||
<p class="text-slate-400 text-sm font-medium">배정 사용자가 '업무용'인 노트북이 없거나 검색 결과가 없습니다.</p>
|
<div class="text-slate-300 text-5xl mb-4 italic">No Assets Found</div>
|
||||||
</div>
|
<p class="text-slate-400 text-sm font-medium">배정 사용자가 '업무용'인 노트북이 없거나 검색 결과가 없습니다.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Rental Modal -->
|
|
||||||
<div x-show="showRentalModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
<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="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">
|
<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>
|
<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>
|
<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">
|
<form @submit.prevent="submitRental" class="space-y-5">
|
||||||
<div>
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">임대사원 성함</label>
|
<div>
|
||||||
<input type="text" x-model="rentalName" required placeholder="임대받는 사원의 이름을 입력하세요"
|
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">임대사원 선택
|
||||||
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>
|
||||||
<p class="mt-2 text-[10px] text-slate-400 italic">* 공란으로 입력 후 적용 시 반납 처리됩니다.</p>
|
<select x-model="rentalUserId" required @change="updateRentalName()"
|
||||||
|
class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none font-bold text-sm">
|
||||||
|
<option value="">직원 선택</option>
|
||||||
|
<template x-for="user in allUsers" :key="user.id">
|
||||||
|
<option :value="user.id"
|
||||||
|
x-text="user.name + ' (' + (user.dept_name ? user.dept_name.split(' > ').pop() : '미소속') + ')'">
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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">
|
<div class="pt-4 flex gap-3">
|
||||||
<button type="button" @click="showRentalModal = false"
|
<button type="button" @click="showRentalModal = false"
|
||||||
class="flex-1 py-3 bg-slate-100 rounded-xl font-bold">취소</button>
|
class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold">취소</button>
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="flex-1 py-3 bg-blue-600 text-white rounded-xl font-bold shadow-lg shadow-blue-200">임대
|
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>
|
임대 적용
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- History Modal -->
|
||||||
<div x-show="showHistoryModal" x-cloak class="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
<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="fixed inset-0 modal-bg" @click="showHistoryModal = false"></div>
|
||||||
|
|
@ -175,15 +368,21 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<template x-for="log in history.rental" :key="log.id">
|
<template x-for="log in history.rental" :key="log.id">
|
||||||
<div
|
<div class="p-4 bg-emerald-50 rounded-2xl border border-emerald-100 flex flex-col gap-3">
|
||||||
class="flex items-center justify-between p-3 bg-emerald-50 rounded-xl border border-emerald-100">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-3">
|
<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"
|
<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>
|
x-text="log.user_name.charAt(0)"></div>
|
||||||
<span class="text-sm font-black text-slate-900" x-text="log.user_name"></span>
|
<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>
|
</div>
|
||||||
<span class="text-[10px] font-bold text-emerald-600 bg-white px-2 py-1 rounded-md"
|
<template x-if="log.note">
|
||||||
x-text="log.action_date"></span>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div x-show="history.rental.length === 0"
|
<div x-show="history.rental.length === 0"
|
||||||
|
|
@ -229,12 +428,23 @@
|
||||||
sortOrder: 'asc',
|
sortOrder: 'asc',
|
||||||
showRentalModal: false,
|
showRentalModal: false,
|
||||||
showHistoryModal: false,
|
showHistoryModal: false,
|
||||||
|
showRemarksModal: false,
|
||||||
|
isEditingRemarks: false,
|
||||||
|
tempRemarks: '',
|
||||||
selectedAsset: null,
|
selectedAsset: null,
|
||||||
|
rentalUserId: '',
|
||||||
rentalName: '',
|
rentalName: '',
|
||||||
|
rentalStartDate: '',
|
||||||
|
rentalEndDate: '',
|
||||||
|
rentalReason: '',
|
||||||
|
rentalRemarks: '',
|
||||||
|
rentalPeripherals: [],
|
||||||
|
allUsers: [],
|
||||||
history: { rental: [], assignment: [] },
|
history: { rental: [], assignment: [] },
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.fetchAssets();
|
this.fetchAssets();
|
||||||
|
this.fetchUsers();
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchAssets() {
|
fetchAssets() {
|
||||||
|
|
@ -243,6 +453,12 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchUsers() {
|
||||||
|
fetch('api.php?action=get_users').then(res => res.json()).then(data => {
|
||||||
|
this.allUsers = data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
get filteredAssets() {
|
get filteredAssets() {
|
||||||
if (!this.searchQuery) return this.assets;
|
if (!this.searchQuery) return this.assets;
|
||||||
const q = this.searchQuery.toLowerCase();
|
const q = this.searchQuery.toLowerCase();
|
||||||
|
|
@ -273,32 +489,134 @@
|
||||||
|
|
||||||
openRentalModal(asset) {
|
openRentalModal(asset) {
|
||||||
this.selectedAsset = asset;
|
this.selectedAsset = asset;
|
||||||
|
this.rentalUserId = asset.rental_user_id || '';
|
||||||
this.rentalName = asset.rental_user_name || '';
|
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;
|
this.showRentalModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateRentalName() {
|
||||||
|
const user = this.allUsers.find(u => u.id == this.rentalUserId);
|
||||||
|
if (user) {
|
||||||
|
this.rentalName = user.name;
|
||||||
|
} else {
|
||||||
|
this.rentalName = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
submitRental() {
|
submitRental() {
|
||||||
|
if (!this.rentalUserId) {
|
||||||
|
window.showAlert('임대할 직원을 선택해주세요.', '알림', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
fetch('api.php?action=update_rental', {
|
fetch('api.php?action=update_rental', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: this.selectedAsset.id,
|
id: this.selectedAsset.id,
|
||||||
rental_user_name: this.rentalName
|
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 => {
|
}).then(res => res.json()).then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
window.showAlert('임대 설정이 완료되었습니다.', '성공', 'success');
|
||||||
this.showRentalModal = false;
|
this.showRentalModal = false;
|
||||||
this.fetchAssets();
|
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) {
|
openHistoryModal(asset) {
|
||||||
this.selectedAsset = asset;
|
this.selectedAsset = asset;
|
||||||
fetch(`api.php?action=get_asset_history&asset_id=${asset.id}`).then(res => res.json()).then(data => {
|
fetch(`api.php?action=get_asset_history&asset_id=${asset.id}`).then(res => res.json()).then(data => {
|
||||||
this.history = data;
|
this.history = data;
|
||||||
this.showHistoryModal = true;
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, '비고 삭제 확인');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
328
users.php
328
users.php
|
|
@ -1,3 +1,4 @@
|
||||||
|
<?php require_once 'auth_check.php'; ?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
|
|
||||||
|
|
@ -96,6 +97,13 @@
|
||||||
<div class="w-2 h-2 rounded-full mr-2" :class="filterOnLeave ? 'bg-white' : 'bg-amber-500'"></div>
|
<div class="w-2 h-2 rounded-full mr-2" :class="filterOnLeave ? 'bg-white' : 'bg-amber-500'"></div>
|
||||||
휴직자 표시
|
휴직자 표시
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="filterClassification = !filterClassification"
|
||||||
|
:class="filterClassification ? 'bg-indigo-600 text-white border-indigo-600 shadow-md shadow-indigo-100' : 'bg-white text-slate-500 border-slate-200 hover:border-indigo-300 hover:text-indigo-500'"
|
||||||
|
class="px-4 py-2 rounded-xl text-xs font-bold border transition-all flex items-center">
|
||||||
|
<div class="w-2 h-2 rounded-full mr-2" :class="filterClassification ? 'bg-white' : 'bg-indigo-500'">
|
||||||
|
</div>
|
||||||
|
분류 대상 표시
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bulk Command Center (Fixed Floating Bar) -->
|
<!-- Bulk Command Center (Fixed Floating Bar) -->
|
||||||
|
|
@ -167,6 +175,26 @@
|
||||||
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="mr-1">노트북</span>
|
||||||
|
<span class="text-slate-300 font-normal">(</span>
|
||||||
|
<button @click="toggleSort('laptop_purchase_date')"
|
||||||
|
class="hover:text-blue-600 transition-colors flex items-center">
|
||||||
|
취득년도
|
||||||
|
<span class="ml-0.5 text-[10px]" x-show="sortKey === 'laptop_purchase_date'"
|
||||||
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</button>
|
||||||
|
<span class="text-slate-300 font-normal">/</span>
|
||||||
|
<button @click="toggleSort('laptop_model_name')"
|
||||||
|
class="hover:text-blue-600 transition-colors flex items-center">
|
||||||
|
모델명
|
||||||
|
<span class="ml-0.5 text-[10px]" x-show="sortKey === 'laptop_model_name'"
|
||||||
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
|
</button>
|
||||||
|
<span class="text-slate-300 font-normal">)</span>
|
||||||
|
</div>
|
||||||
|
</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 @click="toggleSort('accounting_type')"
|
<th @click="toggleSort('accounting_type')"
|
||||||
|
|
@ -185,31 +213,71 @@
|
||||||
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
x-text="sortOrder === 'asc' ? '↑' : '↓'"></span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-emerald-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="user in filteredUsers" :key="user.id">
|
<template x-for="user in filteredUsers" :key="user.id">
|
||||||
<tr class="hover:bg-slate-50/80 transition-colors group">
|
<tr class="hover:shadow-md transition-all group relative overflow-hidden"
|
||||||
<td class="px-6 py-4">
|
:class="getPositionInfo(user.position).bgClass" :style="getRowStyle(user)">
|
||||||
|
<td class="px-6 py-4 relative z-10">
|
||||||
<input type="checkbox" :value="user.id" x-model="selectedIds"
|
<input type="checkbox" :value="user.id" x-model="selectedIds"
|
||||||
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
|
class="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500">
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editUser(user)">
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer relative z-10"
|
||||||
|
@click="editUser(user)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="h-10 w-10 flex-shrink-0 bg-indigo-100 rounded-xl flex items-center justify-center text-indigo-600 font-bold"
|
<div class="h-11 w-11 flex-shrink-0 rounded-2xl flex items-center justify-center font-black text-lg shadow-sm transform group-hover:scale-110 transition-transform duration-300"
|
||||||
x-text="user.name.charAt(0)"></div>
|
:class="getPositionInfo(user.position).iconClass" x-text="user.name.charAt(0)">
|
||||||
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<div class="text-sm font-bold text-slate-900 group-hover:text-blue-600 transition-colors"
|
<div class="flex items-center gap-2">
|
||||||
x-text="user.name"></div>
|
<div class="text-sm font-black text-slate-900 group-hover:text-blue-700 transition-colors"
|
||||||
<div class="text-xs text-slate-400" x-text="user.emp_id"></div>
|
x-text="user.name"></div>
|
||||||
|
<template x-if="getPositionInfo(user.position).isVIP">
|
||||||
|
<span
|
||||||
|
class="text-[9px] px-1.5 py-0.5 rounded-md font-black uppercase tracking-tighter"
|
||||||
|
:class="getPositionInfo(user.position).tagClass"
|
||||||
|
x-text="'VIP Member'"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] font-bold text-slate-400" x-text="user.emp_id"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap cursor-pointer" @click="editUser(user)">
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer relative z-10"
|
||||||
<div class="text-sm font-semibold text-slate-700"
|
@click="editUser(user)">
|
||||||
|
<div class="text-sm font-bold text-slate-700"
|
||||||
x-text="user.dept_name ? user.dept_name.split(' > ').pop() : '미소속'">
|
x-text="user.dept_name ? user.dept_name.split(' > ').pop() : '미소속'">
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-slate-400" x-text="user.position || '-'"></div>
|
<div class="text-xs font-black text-slate-900 mt-0.5 flex items-center gap-1.5"
|
||||||
|
:class="getPositionInfo(user.position).isVIP ? getPositionInfo(user.position).textClass : ''">
|
||||||
|
<template x-if="getPositionInfo(user.position).isVIP">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full"
|
||||||
|
:class="getPositionInfo(user.position).tagClass"></div>
|
||||||
|
</template>
|
||||||
|
<span x-text="user.position || '-'"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap cursor-pointer relative z-10"
|
||||||
|
@click="editUser(user)">
|
||||||
|
<template x-if="user.laptop_tag">
|
||||||
|
<div>
|
||||||
|
<div class="text-[11px] font-black text-blue-600 bg-blue-50 px-2 py-0.5 rounded inline-block mb-1"
|
||||||
|
x-text="user.laptop_tag"></div>
|
||||||
|
<div class="text-[10px] text-slate-500 font-bold">
|
||||||
|
<span
|
||||||
|
x-text="user.laptop_purchase_date ? user.laptop_purchase_date.substring(0,4) : ''"></span>
|
||||||
|
<span x-show="user.laptop_purchase_date && user.laptop_model_name"> /
|
||||||
|
</span>
|
||||||
|
<span x-text="user.laptop_model_name || ''"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="!user.laptop_tag">
|
||||||
|
<span class="text-[10px] text-slate-300 italic">미배정</span>
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-sm text-slate-600 font-medium" x-text="user.email || '-'"></div>
|
<div class="text-sm text-slate-600 font-medium" x-text="user.email || '-'"></div>
|
||||||
|
|
@ -226,7 +294,8 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
class="px-3 py-1 text-[10px] font-black rounded-lg border border-slate-200 bg-slate-50 text-slate-500 uppercase"
|
class="px-3 py-1.5 text-[10px] font-black rounded-lg border shadow-sm transition-all"
|
||||||
|
:class="getAccountingInfo(user.accounting_type).badgeClass"
|
||||||
x-text="user.accounting_type"></span>
|
x-text="user.accounting_type"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
|
@ -248,6 +317,16 @@
|
||||||
class="px-3 py-1 text-xs font-bold rounded-full bg-indigo-50 text-indigo-600 border border-indigo-100">분류</span>
|
class="px-3 py-1 text-xs font-bold rounded-full bg-indigo-50 text-indigo-600 border border-indigo-100">분류</span>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||||
|
<template x-if="user.rental_count > 0">
|
||||||
|
<button @click="showUserRentals(user)"
|
||||||
|
class="w-8 h-8 rounded-full bg-emerald-100 text-emerald-600 font-black text-sm hover:bg-emerald-600 hover:text-white transition-all shadow-sm border border-emerald-200"
|
||||||
|
x-text="user.rental_count"></button>
|
||||||
|
</template>
|
||||||
|
<template x-if="!user.rental_count || user.rental_count == 0">
|
||||||
|
<span class="text-slate-200 text-xs">-</span>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -264,6 +343,80 @@
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Rental List Modal -->
|
||||||
|
<div x-show="showRentalListModal" x-cloak class="fixed inset-0 z-[120] flex items-center justify-center p-4">
|
||||||
|
<div class="fixed inset-0 modal-bg" @click="showRentalListModal = false"></div>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-[2rem] p-8 max-w-2xl w-full relative z-[121] shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
|
||||||
|
<div class="flex justify-between items-start mb-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 bg-emerald-100 rounded-2xl flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-black text-slate-900" x-text="selectedUser?.name + '님의 단기 임대 목록'"></h3>
|
||||||
|
<p class="text-xs font-bold text-slate-400 uppercase tracking-widest"
|
||||||
|
x-text="(selectedUser?.dept_name || '미소속') + ' / ' + (selectedUser?.position || '-')"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button @click="showRentalListModal = false"
|
||||||
|
class="text-slate-300 hover:text-slate-900 transition-colors">
|
||||||
|
<svg class="w-8 h-8" 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">
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
<template x-for="rental in userRentals" :key="rental.id">
|
||||||
|
<div
|
||||||
|
class="bg-slate-50 rounded-2xl p-5 border border-slate-100 items-center justify-between flex">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
<span class="text-xs font-black text-blue-600 bg-blue-100 px-2 py-0.5 rounded-lg"
|
||||||
|
x-text="rental.asset_tag"></span>
|
||||||
|
<span class="text-sm font-bold text-slate-800" x-text="rental.model_name"></span>
|
||||||
|
</div>
|
||||||
|
<div class="text-[10px] text-slate-400 font-bold uppercase tracking-wider"
|
||||||
|
x-text="rental.manufacturer"></div>
|
||||||
|
<template x-if="rental.rental_peripherals">
|
||||||
|
<div class="flex flex-wrap gap-1 mt-2">
|
||||||
|
<template x-for="p in rental.rental_peripherals.split(',')">
|
||||||
|
<span
|
||||||
|
class="px-1.5 py-0.5 bg-emerald-50 text-emerald-600 text-[9px] font-black rounded border border-emerald-100"
|
||||||
|
x-text="p"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-[11px] font-black text-slate-400 uppercase mb-1">임대 현황</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-xs font-bold text-slate-700">시작: <span
|
||||||
|
x-text="rental.rental_start_date"></span></div>
|
||||||
|
<div class="text-xs font-bold text-rose-500" x-show="rental.rental_end_scheduled">
|
||||||
|
예정: <span x-text="rental.rental_end_scheduled"></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<button @click="showRentalListModal = 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- User Registration/Edit Modal -->
|
<!-- User Registration/Edit 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>
|
||||||
|
|
@ -338,6 +491,36 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Laptop Info Section (Read-only) -->
|
||||||
|
<template x-if="isEdit && formData.laptop_tag">
|
||||||
|
<div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4 mt-6">
|
||||||
|
<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">사용 중인 노트북 정보</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>
|
||||||
|
<div class="px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold text-blue-600"
|
||||||
|
x-text="formData.laptop_tag"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">모델명</label>
|
||||||
|
<div class="px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold text-slate-700"
|
||||||
|
x-text="formData.laptop_model_name || '-'"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">취득일</label>
|
||||||
|
<div class="px-4 py-3 bg-white border border-slate-200 rounded-xl text-sm font-bold text-slate-700"
|
||||||
|
x-text="formData.laptop_purchase_date || '-'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div class="pt-6 flex gap-4">
|
<div class="pt-6 flex gap-4">
|
||||||
<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>
|
||||||
|
|
@ -360,12 +543,15 @@
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
filterRetired: false,
|
filterRetired: false,
|
||||||
filterOnLeave: true,
|
filterOnLeave: true,
|
||||||
|
filterClassification: true,
|
||||||
sortKey: 'name',
|
sortKey: 'name',
|
||||||
sortOrder: 'asc',
|
sortOrder: 'asc',
|
||||||
selectedIds: [],
|
selectedIds: [],
|
||||||
bulkStatus: '',
|
bulkStatus: '',
|
||||||
bulkDeptId: '',
|
bulkDeptId: '',
|
||||||
bulkPosition: '',
|
bulkPosition: '',
|
||||||
|
showRentalListModal: false,
|
||||||
|
userRentals: [],
|
||||||
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' },
|
formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' },
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
@ -384,22 +570,99 @@
|
||||||
result = result.filter(u => u.name.toLowerCase().includes(q) || u.emp_id.toLowerCase().includes(q) || (u.email && u.email.toLowerCase().includes(q)));
|
result = result.filter(u => u.name.toLowerCase().includes(q) || u.emp_id.toLowerCase().includes(q) || (u.email && u.email.toLowerCase().includes(q)));
|
||||||
}
|
}
|
||||||
if (!this.filterRetired) result = result.filter(u => u.status !== 'retired');
|
if (!this.filterRetired) result = result.filter(u => u.status !== 'retired');
|
||||||
if (!this.filterOnLeave) result = result.filter(u => u.status !== 'on_leave' && u.status !== 'classification');
|
if (!this.filterOnLeave) result = result.filter(u => u.status !== 'on_leave');
|
||||||
|
if (!this.filterClassification) result = result.filter(u => u.status !== 'classification');
|
||||||
|
|
||||||
if (this.sortKey) {
|
// 직위 랭킹 함수
|
||||||
result.sort((a, b) => {
|
const getRank = (pos) => {
|
||||||
|
if (!pos) return 99;
|
||||||
|
if (pos.includes('부회장')) return 1;
|
||||||
|
if (pos.includes('총괄')) return 2;
|
||||||
|
if (pos.includes('원장')) return 3;
|
||||||
|
if (pos.includes('센터장')) return 4;
|
||||||
|
if (pos.includes('본부장')) return 5;
|
||||||
|
if (pos.includes('실장')) return 6;
|
||||||
|
if (pos.includes('부문장')) return 7;
|
||||||
|
if (pos.includes('팀장')) return 8;
|
||||||
|
return 90;
|
||||||
|
};
|
||||||
|
|
||||||
|
result.sort((a, b) => {
|
||||||
|
if (this.sortKey) {
|
||||||
let valA = a[this.sortKey] || '';
|
let valA = a[this.sortKey] || '';
|
||||||
let valB = b[this.sortKey] || '';
|
let valB = b[this.sortKey] || '';
|
||||||
if (typeof valA === 'string') valA = valA.toLowerCase();
|
if (typeof valA === 'string') valA = valA.toLowerCase();
|
||||||
if (typeof valB === 'string') valB = valB.toLowerCase();
|
if (typeof valB === 'string') valB = valB.toLowerCase();
|
||||||
if (valA < valB) return this.sortOrder === 'asc' ? -1 : 1;
|
if (valA !== valB) {
|
||||||
if (valA > valB) return this.sortOrder === 'asc' ? 1 : -1;
|
if (valA < valB) return this.sortOrder === 'asc' ? -1 : 1;
|
||||||
return 0;
|
if (valA > valB) return this.sortOrder === 'asc' ? 1 : -1;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2순위: 직위 랭킹 (정렬 기준이 같을 때만 적용)
|
||||||
|
return getRank(a.position) - getRank(b.position);
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getPositionInfo(pos) {
|
||||||
|
if (!pos) return { bgClass: 'bg-white', iconClass: 'bg-slate-100 text-slate-500', isVIP: false };
|
||||||
|
|
||||||
|
const configs = [
|
||||||
|
{ key: '부회장', from: '#fbbf24', to: '#d97706', text: 'text-amber-700', icon: 'bg-amber-100 text-amber-600', tag: 'bg-amber-500 text-white' },
|
||||||
|
{ key: '총괄', from: '#3b82f6', to: '#1d4ed8', text: 'text-blue-700', icon: 'bg-blue-100 text-blue-600', tag: 'bg-blue-600 text-white' },
|
||||||
|
{ key: '원장', from: '#10b981', to: '#059669', text: 'text-emerald-700', icon: 'bg-emerald-100 text-emerald-600', tag: 'bg-emerald-600 text-white' },
|
||||||
|
{ key: '센터장', from: '#06b6d4', to: '#0891b2', text: 'text-cyan-700', icon: 'bg-cyan-100 text-cyan-600', tag: 'bg-cyan-600 text-white' },
|
||||||
|
{ key: '본부장', from: '#8b5cf6', to: '#7c3aed', text: 'text-purple-700', icon: 'bg-purple-100 text-purple-600', tag: 'bg-purple-600 text-white' },
|
||||||
|
{ key: '실장', from: '#64748b', to: '#475569', text: 'text-slate-700', icon: 'bg-slate-100 text-slate-600', tag: 'bg-slate-600 text-white' },
|
||||||
|
{ key: '부문장', from: '#f43f5e', to: '#e11d48', text: 'text-rose-700', icon: 'bg-rose-100 text-rose-600', tag: 'bg-rose-600 text-white' },
|
||||||
|
{ key: '팀장', from: '#6366f1', to: '#4f46e5', text: 'text-indigo-700', icon: 'bg-indigo-100 text-indigo-600', tag: 'bg-indigo-600 text-white' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const config = configs.find(c => pos.includes(c.key));
|
||||||
|
if (config) {
|
||||||
|
return {
|
||||||
|
isVIP: true,
|
||||||
|
from: config.from,
|
||||||
|
bgClass: 'hover:bg-opacity-50 transition-all cursor-pointer',
|
||||||
|
iconClass: config.icon,
|
||||||
|
tagClass: config.tag,
|
||||||
|
textClass: config.text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { bgClass: 'bg-white', iconClass: 'bg-slate-100 text-slate-500', isVIP: false };
|
||||||
|
},
|
||||||
|
|
||||||
|
getAccountingInfo(type) {
|
||||||
|
if (type === '특별회계') {
|
||||||
|
return {
|
||||||
|
isSpecial: true,
|
||||||
|
badgeClass: 'bg-amber-100 text-amber-700 border-amber-200',
|
||||||
|
rowGradient: 'linear-gradient(to left, rgba(251, 191, 36, 0.1) 0%, transparent 40%)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isSpecial: false,
|
||||||
|
badgeClass: 'bg-blue-50 text-blue-600 border-blue-100',
|
||||||
|
rowGradient: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getRowStyle(user) {
|
||||||
|
const vip = this.getPositionInfo(user.position);
|
||||||
|
const acc = this.getAccountingInfo(user.accounting_type);
|
||||||
|
let gradients = [];
|
||||||
|
|
||||||
|
if (vip.isVIP) {
|
||||||
|
gradients.push(`linear-gradient(to right, ${vip.from}08 0%, transparent 60%)`);
|
||||||
|
}
|
||||||
|
if (acc.isSpecial) {
|
||||||
|
gradients.push(acc.rowGradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gradients.length > 0 ? `background: ${gradients.join(', ')}` : 'background: white';
|
||||||
|
},
|
||||||
|
|
||||||
fetchUsers() {
|
fetchUsers() {
|
||||||
const scrollPos = window.scrollY;
|
const scrollPos = window.scrollY;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
@ -429,6 +692,16 @@
|
||||||
this.selectedIds = checked ? this.filteredUsers.map(u => u.id) : [];
|
this.selectedIds = checked ? this.filteredUsers.map(u => u.id) : [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showUserRentals(user) {
|
||||||
|
this.selectedUser = user;
|
||||||
|
fetch(`api.php?action=get_user_rentals&user_id=${user.id}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
this.userRentals = data;
|
||||||
|
this.showRentalListModal = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
openAddModal() {
|
openAddModal() {
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
|
|
@ -441,6 +714,10 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
submitUser() {
|
submitUser() {
|
||||||
|
if (!this.formData.department_id) {
|
||||||
|
window.showAlert('부서를 선택해주세요.', '입력 확인', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const action = this.isEdit ? 'update_user' : 'add_user';
|
const action = this.isEdit ? 'update_user' : 'add_user';
|
||||||
fetch(`api.php?action=${action}`, {
|
fetch(`api.php?action=${action}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -448,9 +725,15 @@
|
||||||
body: JSON.stringify(this.formData)
|
body: JSON.stringify(this.formData)
|
||||||
}).then(res => res.json()).then(data => {
|
}).then(res => res.json()).then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
window.showAlert(this.isEdit ? '정보가 수정되었습니다.' : '새 직원이 등록되었습니다.', '성공', 'success');
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
|
} else {
|
||||||
|
window.showAlert(data.error || '처리 중 오류가 발생했습니다.', '오류', 'error');
|
||||||
}
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
window.showAlert('서버와 통신 중 문제가 발생했습니다.', '시스템 오류', 'error');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -467,18 +750,21 @@
|
||||||
})
|
})
|
||||||
}).then(res => res.json()).then(data => {
|
}).then(res => res.json()).then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
window.showAlert(`${this.selectedIds.length}명의 직원 정보가 일괄 변경되었습니다.`, '변경 성공', 'success');
|
||||||
this.selectedIds = [];
|
this.selectedIds = [];
|
||||||
this.bulkStatus = '';
|
this.bulkStatus = '';
|
||||||
this.bulkDeptId = '';
|
this.bulkDeptId = '';
|
||||||
this.bulkPosition = '';
|
this.bulkPosition = '';
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
|
} else {
|
||||||
|
window.showAlert('직원 정보 일괄 변경에 실패했습니다.', '변경 실패', 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.isEdit = false;
|
this.isEdit = false;
|
||||||
this.formData = { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active' };
|
this.formData = { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue