diff --git a/.gitignore b/.gitignore index 48e152a..71f9190 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ # [업로드 방지] 로컬 테스트 파일들이 GitLab으로 올라가지 않게 함 dda/, ex/ -*.sqlite +# *.sqlite # *.db \ No newline at end of file diff --git a/admins.php b/admins.php new file mode 100644 index 0000000..2b0abd2 --- /dev/null +++ b/admins.php @@ -0,0 +1,231 @@ + + + + + + + + 관리자 관리 | ASSET + + + + + + + + + + + +
+
+
+

System Administrators

+

시스템 관리자 계정 생성, 삭제 및 비밀번호 관리

+
+ +
+ +
+ +
+
+ + +
+ +
+

관리자 계정 생성

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+ +
+

비밀번호 변경

+

+
+
+ + +
+
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/api.php b/api.php index 0030d8d..6a89418 100644 --- a/api.php +++ b/api.php @@ -1,17 +1,81 @@ false, 'error' => 'Unauthorized']); + exit; +} + try { 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': // Laptops $laptop_total = $db->query("SELECT COUNT(*) FROM laptop_assets")->fetchColumn(); @@ -39,14 +103,19 @@ try { case 'get_users': $query = "WITH RECURSIVE dept_path(id, path) AS ( - SELECT id, name FROM departments WHERE parent_id IS NULL - UNION ALL - SELECT d.id, dp.path || ' > ' || d.name - FROM departments d JOIN dept_path dp ON d.parent_id = dp.id - ) - SELECT u.*, dp.path as dept_name - FROM users u - LEFT JOIN dept_path dp ON u.department_id = dp.id"; +SELECT id, name FROM departments WHERE parent_id IS NULL +UNION ALL +SELECT d.id, dp.path || ' > ' || d.name +FROM departments d JOIN dept_path dp ON d.parent_id = dp.id +) +SELECT u.*, dp.path as dept_name, +(SELECT asset_tag FROM laptop_assets WHERE current_user_id = u.id ORDER BY id DESC LIMIT 1) as laptop_tag, +(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'] ?? ''; if ($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': $data = json_decode(file_get_contents('php://input'), true); $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 if ($status === 'on_leave' || $status === 'retired') { @@ -71,25 +149,40 @@ try { $department_id = $target_dept; } - $stmt = $db->prepare("INSERT INTO users (emp_id, name, department_id, position, email, phone, mobile, accounting_type, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); - $stmt->execute([ - $data['emp_id'], - $data['name'], - $department_id, - $data['position'], - $data['email'], - $data['phone'] ?? '', - $data['mobile'], - $data['accounting_type'], - $status - ]); - echo json_encode(['success' => true]); + try { + $stmt = $db->prepare("INSERT INTO users (emp_id, name, department_id, position, email, phone, mobile, accounting_type, +status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([ + $emp_id, + $data['name'], + $department_id, + $data['position'], + $data['email'], + $data['phone'] ?? '', + $data['mobile'], + $data['accounting_type'], + $status + ]); + echo json_encode(['success' => true]); + } catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } break; case 'update_user': $data = json_decode(file_get_contents('php://input'), true); $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 if ($status === 'on_leave' || $status === 'retired') { @@ -99,55 +192,125 @@ try { $department_id = $target_dept; } - $stmt = $db->prepare("UPDATE users SET emp_id = ?, name = ?, department_id = ?, position = ?, email = ?, phone = ?, mobile = ?, accounting_type = ?, status = ? WHERE id = ?"); - $stmt->execute([ - $data['emp_id'], - $data['name'], - $department_id, - $data['position'], - $data['email'], - $data['phone'] ?? '', - $data['mobile'], - $data['accounting_type'], - $status, - $data['id'] - ]); - echo json_encode(['success' => true]); + try { + $stmt = $db->prepare("UPDATE users SET emp_id = ?, name = ?, department_id = ?, position = ?, email = ?, phone = ?, +mobile = ?, accounting_type = ?, status = ? WHERE id = ?"); + $stmt->execute([ + $emp_id, + $data['name'], + $department_id, + $data['position'], + $data['email'], + $data['phone'] ?? '', + $data['mobile'], + $data['accounting_type'], + $status, + $user_id + ]); + echo json_encode(['success' => true]); + } catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } break; case 'get_laptops': - $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 - LEFT JOIN laptop_models m ON a.model_id = m.id - LEFT JOIN users u ON a.current_user_id = u.id"; + $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 +LEFT JOIN laptop_models m ON a.model_id = m.id +LEFT JOIN users u ON a.current_user_id = u.id"; echo json_encode($db->query($query)->fetchAll()); break; case 'get_rental_laptops': - $query = "SELECT a.*, m.model_name, m.manufacturer, m.product_name - FROM laptop_assets a - LEFT JOIN laptop_models m ON a.model_id = m.id - WHERE a.assigned_user_name LIKE '%업무용%'"; + $query = "SELECT a.*, m.model_name, m.manufacturer, m.product_name +FROM laptop_assets a +LEFT JOIN laptop_models m ON a.model_id = m.id +WHERE (a.assigned_user_name LIKE '%업무용%' OR a.current_user_id IS NULL)"; echo json_encode($db->query($query)->fetchAll()); break; case 'update_rental': $data = json_decode(file_get_contents('php://input'), true); - $stmt = $db->prepare("UPDATE laptop_assets SET rental_user_name = ? WHERE id = ?"); - $stmt->execute([$data['rental_user_name'], $data['id']]); + $startDate = $data['rental_start_date'] ?: date('Y-m-d'); + + $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 - if ($data['rental_user_name']) { - $log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'rental', ?, ?)"); - $log_stmt->execute([$data['id'], $data['rental_user_name'], date('Y-m-d')]); + $peripherals = $data['rental_peripherals'] ? "\n부속품: " . $data['rental_peripherals'] : ""; + $note = "사유: " . ($data['rental_reason'] ?? '없음') . $peripherals; + $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]); 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': $asset_id = $_GET['asset_id']; - $rental_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'rental' ORDER BY id DESC LIMIT 20")->fetchAll(); - $assign_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'assignment' ORDER BY id DESC LIMIT 20")->fetchAll(); + $rental_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'rental' ORDER BY id +DESC LIMIT 20")->fetchAll(); + $assign_logs = $db->query("SELECT * FROM asset_history WHERE asset_id = $asset_id AND log_type = 'assignment' ORDER BY +id DESC LIMIT 20")->fetchAll(); echo json_encode([ 'rental' => $rental_logs, 'assignment' => $assign_logs @@ -197,10 +360,10 @@ try { case 'add_model': $data = json_decode(file_get_contents('php://input'), true); $stmt = $db->prepare("INSERT INTO laptop_models ( - model_name, manufacturer, specs, cpu, npu, hdd0_model, hdd0_capacity, - hdd1_model, hdd1_capacity, ram, asset_status, assigned_user, fixed_ip, - remarks, options, power_rating, purchase_date, vendor, product_name - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); +model_name, manufacturer, specs, cpu, npu, hdd0_model, hdd0_capacity, +hdd1_model, hdd1_capacity, ram, asset_status, assigned_user, fixed_ip, +remarks, options, power_rating, purchase_date, vendor, product_name +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([ $data['model_name'], $data['manufacturer'], @@ -227,13 +390,13 @@ try { case 'update_model': $data = json_decode(file_get_contents('php://input'), true); - $stmt = $db->prepare("UPDATE laptop_models SET - model_name = ?, manufacturer = ?, specs = ?, cpu = ?, npu = ?, - hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?, - ram = ?, asset_status = ?, assigned_user = ?, fixed_ip = ?, - remarks = ?, options = ?, power_rating = ?, purchase_date = ?, - vendor = ?, product_name = ? - WHERE id = ?"); + $stmt = $db->prepare("UPDATE laptop_models SET +model_name = ?, manufacturer = ?, specs = ?, cpu = ?, npu = ?, +hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?, +ram = ?, asset_status = ?, assigned_user = ?, fixed_ip = ?, +remarks = ?, options = ?, power_rating = ?, purchase_date = ?, +vendor = ?, product_name = ? +WHERE id = ?"); $stmt->execute([ $data['model_name'], $data['manufacturer'], @@ -261,26 +424,27 @@ try { case 'get_departments': $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 - UNION ALL - SELECT d.id, d.name, dp.path || ' > ' || d.name, d.level, d.parent_id - FROM departments d - JOIN dept_path dp ON d.parent_id = dp.id - ) - SELECT dp.*, - (SELECT COUNT(*) FROM users WHERE department_id = dp.id) as member_count - FROM dept_path dp - ORDER BY path"; +SELECT id, name, name, level, parent_id FROM departments WHERE parent_id IS NULL +UNION ALL +SELECT d.id, d.name, dp.path || ' > ' || d.name, d.level, d.parent_id +FROM departments d +JOIN dept_path dp ON d.parent_id = dp.id +) +SELECT dp.*, +(SELECT COUNT(*) FROM users WHERE department_id = dp.id) as member_count +FROM dept_path dp +ORDER BY path"; echo json_encode($db->query($sql)->fetchAll()); break; case 'add_laptop_asset': $data = json_decode(file_get_contents('php://input'), true); $stmt = $db->prepare("INSERT INTO laptop_assets ( - asset_tag, model_id, current_user_id, status, serial_number, purchase_date, ip_address, - 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 - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); +asset_tag, model_id, current_user_id, status, serial_number, purchase_date, ip_address, +cpu, npu, hdd0_model, hdd0_capacity, hdd1_model, hdd1_capacity, ram, +asset_status, assigned_user_name, fixed_ip, options, power_rating, manufacturer, vendor, product_name, remarks, +last_confirmed_date +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); $stmt->execute([ $data['asset_tag'], $data['model_id'], @@ -309,7 +473,8 @@ try { ]); $new_asset_id = $db->lastInsertId(); if ($data['assigned_user_name']) { - $log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'assignment', ?, ?)"); + $log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, +'assignment', ?, ?)"); $log_stmt->execute([$new_asset_id, $data['assigned_user_name'], date('Y-m-d')]); } echo json_encode(['success' => true]); @@ -323,11 +488,12 @@ try { $old_stmt->execute([$data['id']]); $old_asset = $old_stmt->fetch(); - $stmt = $db->prepare("UPDATE laptop_assets SET - asset_tag = ?, model_id = ?, current_user_id = ?, status = ?, serial_number = ?, purchase_date = ?, ip_address = ?, - cpu = ?, npu = ?, hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?, ram = ?, - asset_status = ?, assigned_user_name = ?, fixed_ip = ?, options = ?, power_rating = ?, manufacturer = ?, vendor = ?, product_name = ?, remarks = ?, last_confirmed_date = ? - WHERE id = ?"); + $stmt = $db->prepare("UPDATE laptop_assets SET +asset_tag = ?, model_id = ?, current_user_id = ?, status = ?, serial_number = ?, purchase_date = ?, ip_address = ?, +cpu = ?, npu = ?, hdd0_model = ?, hdd0_capacity = ?, hdd1_model = ?, hdd1_capacity = ?, ram = ?, +asset_status = ?, assigned_user_name = ?, fixed_ip = ?, options = ?, power_rating = ?, manufacturer = ?, vendor = ?, +product_name = ?, remarks = ?, last_confirmed_date = ? +WHERE id = ?"); $stmt->execute([ $data['asset_tag'], $data['model_id'], @@ -358,7 +524,8 @@ try { // Auto-log assignment change if ($data['assigned_user_name'] && $data['assigned_user_name'] !== ($old_asset['assigned_user_name'] ?? '')) { - $log_stmt = $db->prepare("INSERT INTO asset_history (asset_id, log_type, user_name, action_date) VALUES (?, 'assignment', ?, ?)"); + $log_stmt = $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')]); } @@ -409,7 +576,8 @@ try { $fki_root_id = $fki_root ? $fki_root['id'] : null; // 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) { $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']); 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->execute(array_merge($params, $ids)); echo json_encode(['success' => true]); @@ -502,7 +674,11 @@ try { echo json_encode(['success' => false, 'error' => 'No updates specified']); break; } - $sql = "UPDATE access_cards SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")"; + $sql = "UPDATE access_cards SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill( + 0, + count($ids), + "?" + )) . ")"; $stmt = $db->prepare($sql); $stmt->execute(array_merge($params, $ids)); echo json_encode(['success' => true]); @@ -531,7 +707,11 @@ try { echo json_encode(['success' => false, 'error' => 'No updates specified']); break; } - $sql = "UPDATE mfp_accounts SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")"; + $sql = "UPDATE mfp_accounts SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill( + 0, + count($ids), + "?" + )) . ")"; $stmt = $db->prepare($sql); $stmt->execute(array_merge($params, $ids)); echo json_encode(['success' => true]); @@ -560,7 +740,11 @@ try { echo json_encode(['success' => false, 'error' => 'No updates specified']); break; } - $sql = "UPDATE laptop_models SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill(0, count($ids), "?")) . ")"; + $sql = "UPDATE laptop_models SET " . implode(", ", $updates) . " WHERE id IN (" . implode(",", array_fill( + 0, + count($ids), + "?" + )) . ")"; $stmt = $db->prepare($sql); $stmt->execute(array_merge($params, $ids)); echo json_encode(['success' => true]); @@ -615,6 +799,55 @@ try { echo json_encode(['success' => true]); 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: echo json_encode(['error' => 'Invalid action']); break; diff --git a/assets.db b/assets.db index c897b2d..5a61581 100644 Binary files a/assets.db and b/assets.db differ diff --git a/auth_check.php b/auth_check.php new file mode 100644 index 0000000..ce89014 --- /dev/null +++ b/auth_check.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/cards.php b/cards.php index 25f2348..0176c03 100644 --- a/cards.php +++ b/cards.php @@ -1,3 +1,4 @@ + diff --git a/general_rental.php b/general_rental.php new file mode 100644 index 0000000..c2e8c27 --- /dev/null +++ b/general_rental.php @@ -0,0 +1,340 @@ + + + + + + + 일반 물품 임대 관리 | FKI ASSET + + + + + + + + + + + +
+
+
+

일반 물품 임대

+

사원별 각종 물품(어댑터, 주변기기 등) 단기 임대 및 반납 기록

+
+ +
+ + +
+ +
+
+ + +
+ +
+

임대 등록

+

Register New General Rental

+ +
+
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/index.php b/index.php index ed5c828..6f4b35f 100644 --- a/index.php +++ b/index.php @@ -1,3 +1,4 @@ + diff --git a/laptops.php b/laptops.php index 591be8f..28704f8 100644 --- a/laptops.php +++ b/laptops.php @@ -1,3 +1,4 @@ + @@ -251,32 +252,44 @@ -
-
- - + +
+
+
+

Individual Asset Info +

-
- - -
-
- - -
-
- - +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
@@ -330,21 +343,29 @@
-
-
- - + +
+
+
+

Model Base Info (Read + Only)

-
- - -
-
- - +
+
+ + +
+
+ + +
+
+ + +
@@ -401,7 +422,10 @@ applyBulkUpdate() { 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', { method: 'POST', @@ -413,10 +437,13 @@ }) }).then(res => res.json()).then(data => { if (data.success) { + window.showAlert(`${this.selectedIds.length}개의 자산 정보가 일괄 변경되었습니다.`, '변경 성공', 'success'); this.selectedIds = []; this.bulkStatus = ''; this.bulkModelId = ''; this.fetchAssets(); + } else { + window.showAlert('자산 정보 일괄 변경에 실패했습니다.', '변경 실패', 'error'); } }); }, diff --git a/login.php b/login.php new file mode 100644 index 0000000..ce999b4 --- /dev/null +++ b/login.php @@ -0,0 +1,123 @@ + + + + + + + + ASSET | Login + + + + + + + + +
+
+
+
+
+ +
+
+

ASSET

+

Management System v2.0

+
+ +
+

Sign In

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +

+ Forbidden access is strictly monitored +

+
+ + + \ No newline at end of file diff --git a/mfp.php b/mfp.php index 14f25ea..5980878 100644 --- a/mfp.php +++ b/mfp.php @@ -1,3 +1,4 @@ + diff --git a/models.php b/models.php index 14c05e5..5558d53 100644 --- a/models.php +++ b/models.php @@ -1,3 +1,4 @@ + @@ -289,23 +290,8 @@
- -
-
- - -
-
- - -
-
- - -
+ +
+
+ +

※ 삭제 시 소속 인원은 '미소속'으로 이동됩니다.

+
@@ -401,20 +394,30 @@ + + +
+
No Assets Found
+

배정 사용자가 '업무용'인 노트북이 없거나 검색 결과가 없습니다.

+
-
-
+

-
-
- - -

* 공란으로 입력 후 적용 시 반납 처리됩니다.

+ +
+
+ + +
+
+ + +
+ +
+ +
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ class="flex-1 py-4 bg-slate-100 text-slate-600 rounded-2xl font-bold">취소 + 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"> + 임대 적용 +
+ +
+ +
+
+
+
+ + + +
+
+

+

자산 상세 비고

+
+
+ +
+ + +
+
+
+
Remarks Content +
+ +
+ + + + +
+ + +
+ + +
+
+
+
+
@@ -175,15 +368,21 @@
res.json()).then(data => { + this.allUsers = data; + }); + }, + get filteredAssets() { if (!this.searchQuery) return this.assets; const q = this.searchQuery.toLowerCase(); @@ -273,32 +489,134 @@ openRentalModal(asset) { this.selectedAsset = asset; + this.rentalUserId = asset.rental_user_id || ''; this.rentalName = asset.rental_user_name || ''; + this.rentalStartDate = asset.rental_start_date || new Date().toISOString().split('T')[0]; + this.rentalEndDate = asset.rental_end_scheduled || ''; + this.rentalReason = asset.rental_reason || ''; + this.rentalRemarks = asset.remarks || ''; + this.rentalPeripherals = asset.rental_peripherals ? asset.rental_peripherals.split(',') : []; this.showRentalModal = true; }, + updateRentalName() { + const user = this.allUsers.find(u => u.id == this.rentalUserId); + if (user) { + this.rentalName = user.name; + } else { + this.rentalName = ''; + } + }, + submitRental() { + if (!this.rentalUserId) { + window.showAlert('임대할 직원을 선택해주세요.', '알림', 'warning'); + return; + } fetch('api.php?action=update_rental', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: this.selectedAsset.id, - rental_user_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 => { if (data.success) { + window.showAlert('임대 설정이 완료되었습니다.', '성공', 'success'); this.showRentalModal = false; this.fetchAssets(); + } else { + window.showAlert(data.error || '임대 적용 중 오류가 발생했습니다.', '오류', 'error'); } + }).catch(err => { + window.showAlert('서버와의 통신에 실패했습니다.', '통신 오류', 'error'); }); }, + returnRental(asset) { + window.showConfirm(`${asset.rental_user_name}님의 노트북 반납을 처리하시겠습니까?`, () => { + fetch('api.php?action=return_rental', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: asset.id }) + }).then(res => res.json()).then(data => { + if (data.success) { + window.showAlert('반납 처리가 완료되었습니다.', '성공', 'success'); + this.fetchAssets(); + } + }); + }, '반납 확인'); + }, + + calculateElapsed(startDate) { + if (!startDate) return 0; + const start = new Date(startDate); + const now = new Date(); + const diffTime = Math.abs(now - start); + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + return diffDays + 1; + }, + openHistoryModal(asset) { this.selectedAsset = asset; fetch(`api.php?action=get_asset_history&asset_id=${asset.id}`).then(res => res.json()).then(data => { this.history = data; this.showHistoryModal = true; }); + }, + + openRemarksModal(asset) { + this.selectedAsset = asset; + this.isEditingRemarks = false; + this.tempRemarks = asset.remarks || ''; + this.showRemarksModal = true; + }, + + startEditRemarks() { + this.tempRemarks = this.selectedAsset.remarks || ''; + this.isEditingRemarks = true; + }, + + saveRemarks() { + fetch('api.php?action=update_asset_remarks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: this.selectedAsset.id, + remarks: this.tempRemarks + }) + }).then(res => res.json()).then(data => { + if (data.success) { + this.selectedAsset.remarks = this.tempRemarks; + this.isEditingRemarks = false; + this.fetchAssets(); + } + }); + }, + + deleteRemarks() { + window.showConfirm('비고 내용을 완전히 삭제하시겠습니까?', () => { + fetch('api.php?action=update_asset_remarks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: this.selectedAsset.id, + remarks: '' + }) + }).then(res => res.json()).then(data => { + if (data.success) { + this.selectedAsset.remarks = ''; + this.tempRemarks = ''; + this.fetchAssets(); + } + }); + }, '비고 삭제 확인'); } } } diff --git a/users.php b/users.php index a6a83cf..cb4ad25 100644 --- a/users.php +++ b/users.php @@ -1,3 +1,4 @@ + @@ -96,6 +97,13 @@
휴직자 표시 +
@@ -167,6 +175,26 @@ x-text="sortOrder === 'asc' ? '↑' : '↓'">
+ +
+ 노트북 + ( + + / + + ) +
+ 연락처 / 사내전화
+ 단기임대 + + + + + @@ -264,6 +343,80 @@
+ +
+ +
+
+
+
+ + + +
+
+

+

+
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+
@@ -338,6 +491,36 @@
+ + +
@@ -360,12 +543,15 @@ searchQuery: '', filterRetired: false, filterOnLeave: true, + filterClassification: true, sortKey: 'name', sortOrder: 'asc', selectedIds: [], bulkStatus: '', bulkDeptId: '', bulkPosition: '', + showRentalListModal: false, + userRentals: [], formData: { id: '', name: '', emp_id: '', department_id: '', position: '', email: '', mobile: '', accounting_type: '일반회계', status: 'active', phone: '' }, 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))); } 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 valB = b[this.sortKey] || ''; if (typeof valA === 'string') valA = valA.toLowerCase(); if (typeof valB === 'string') valB = valB.toLowerCase(); - if (valA < valB) return this.sortOrder === 'asc' ? -1 : 1; - if (valA > valB) return this.sortOrder === 'asc' ? 1 : -1; - return 0; - }); - } + if (valA !== valB) { + if (valA < valB) return this.sortOrder === 'asc' ? -1 : 1; + if (valA > valB) return this.sortOrder === 'asc' ? 1 : -1; + } + } + + // 2순위: 직위 랭킹 (정렬 기준이 같을 때만 적용) + return getRank(a.position) - getRank(b.position); + }); 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() { const scrollPos = window.scrollY; this.loading = true; @@ -429,6 +692,16 @@ 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() { this.resetForm(); this.showModal = true; @@ -441,6 +714,10 @@ }, submitUser() { + if (!this.formData.department_id) { + window.showAlert('부서를 선택해주세요.', '입력 확인', 'error'); + return; + } const action = this.isEdit ? 'update_user' : 'add_user'; fetch(`api.php?action=${action}`, { method: 'POST', @@ -448,9 +725,15 @@ body: JSON.stringify(this.formData) }).then(res => res.json()).then(data => { if (data.success) { + window.showAlert(this.isEdit ? '정보가 수정되었습니다.' : '새 직원이 등록되었습니다.', '성공', 'success'); this.showModal = false; 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 => { if (data.success) { + window.showAlert(`${this.selectedIds.length}명의 직원 정보가 일괄 변경되었습니다.`, '변경 성공', 'success'); this.selectedIds = []; this.bulkStatus = ''; this.bulkDeptId = ''; this.bulkPosition = ''; this.fetchUsers(); + } else { + window.showAlert('직원 정보 일괄 변경에 실패했습니다.', '변경 실패', 'error'); } }); }, resetForm() { 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: '' }; } } }