From 5e7a53e731253d0d8aff57605a54cb454b0c077d Mon Sep 17 00:00:00 2001 From: NAS-Admin Date: Mon, 9 Feb 2026 00:40:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=8B=9C=EC=8A=A4=ED=85=9C=EC=9D=84=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=ED=95=98=EA=B3=A0=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EB=B6=84=EB=A5=98,=20=EB=85=B8=ED=8A=B8=EB=B6=81,=20=EB=8B=A8?= =?UTF-8?q?=EA=B8=B0=20=EC=9E=84=EB=8C=80=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EB=A9=B0=20=EC=9D=BC=EB=B0=98=20?= =?UTF-8?q?=EB=8C=80=EC=97=AC=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- admins.php | 231 +++++++++++++++++++++++++ api.php | 423 +++++++++++++++++++++++++++++++++++---------- assets.db | Bin 143360 -> 172032 bytes auth_check.php | 10 ++ cards.php | 1 + general_rental.php | 340 ++++++++++++++++++++++++++++++++++++ index.php | 1 + laptops.php | 107 +++++++----- login.php | 123 +++++++++++++ mfp.php | 1 + models.php | 127 +++++++++++--- nav.php | 114 +++++++++++- rental.php | 392 +++++++++++++++++++++++++++++++++++++---- users.php | 328 ++++++++++++++++++++++++++++++++--- 15 files changed, 1971 insertions(+), 229 deletions(-) create mode 100644 admins.php create mode 100644 auth_check.php create mode 100644 general_rental.php create mode 100644 login.php 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 c897b2ddbd1b91cd87ad78ab8476e814367f66d1..5a61581d5d9911d1b39ed3943f06d9ae788a6424 100644 GIT binary patch delta 11781 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 delta 6897 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 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: '' }; } } }