최종
This commit is contained in:
parent
bd1ea046b1
commit
9ab5065026
16 changed files with 792 additions and 224 deletions
76
FIX_AND_MIGRATE.php
Normal file
76
FIX_AND_MIGRATE.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
/**
|
||||
* Synology Autonomous Architect - Deep Migration & Repair Tool
|
||||
* Target: dda/assets.db
|
||||
* Function: Fix Mojibake/URL-encoding in names & Apply latest schema
|
||||
*/
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
try {
|
||||
$dbPath = __DIR__ . DIRECTORY_SEPARATOR . 'dda' . DIRECTORY_SEPARATOR . 'assets.db';
|
||||
if (!file_exists($dbPath)) {
|
||||
throw new Exception("File not found at $dbPath");
|
||||
}
|
||||
|
||||
$db = new PDO("sqlite:$dbPath");
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
echo "--- 🛠 심층 마이그레이션 및 데이터 복구 시작 ---\n";
|
||||
|
||||
// 1. 스키마 강제 보정
|
||||
$newColumns = [
|
||||
'disposal_recipient' => 'TEXT',
|
||||
'disposal_date' => 'TEXT',
|
||||
'real_user' => 'TEXT',
|
||||
'disposal_user_id' => 'INTEGER'
|
||||
];
|
||||
foreach ($newColumns as $col => $type) {
|
||||
try {
|
||||
$db->exec("ALTER TABLE laptop_assets ADD COLUMN $col $type");
|
||||
echo "✅ 컬럼 보정: $col\n";
|
||||
} catch (PDOException $e) {
|
||||
// Already exists or other error
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 인코딩 복구 (URL-Encoded 데이터가 식별되어 수정 시도)
|
||||
$stmt = $db->query("SELECT id, assigned_user_name, disposal_recipient FROM laptop_assets");
|
||||
$assets = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$fixCount = 0;
|
||||
$updateStmt = $db->prepare("UPDATE laptop_assets SET assigned_user_name = ?, disposal_recipient = ? WHERE id = ?");
|
||||
|
||||
foreach ($assets as $asset) {
|
||||
$uName = $asset['assigned_user_name'];
|
||||
$dRec = $asset['disposal_recipient'];
|
||||
|
||||
$newUName = $uName;
|
||||
$newDRec = $dRec;
|
||||
|
||||
// URL 인코딩 탐지 및 변환
|
||||
if ($uName && strpos($uName, '%') !== false) {
|
||||
$newUName = urldecode($uName);
|
||||
}
|
||||
if ($dRec && strpos($dRec, '%') !== false) {
|
||||
$newDRec = urldecode($dRec);
|
||||
}
|
||||
|
||||
if ($newUName !== $uName || $newDRec !== $dRec) {
|
||||
$updateStmt->execute([$newUName, $newDRec, $asset['id']]);
|
||||
$fixCount++;
|
||||
}
|
||||
}
|
||||
echo "✅ Mojibake/URL-Encoding 데이터 복구: {$fixCount}건 수정 완료.\n";
|
||||
|
||||
// 3. STOCK 자산 명칙 동기화
|
||||
$stmtStock = $db->prepare("UPDATE laptop_assets SET assigned_user_name = '업무용(TEMP_44666b)', current_user_id = NULL WHERE status = 'stock'");
|
||||
$stmtStock->execute();
|
||||
echo "✅ 재고(STOCK) 명칭 동기화 완료.\n";
|
||||
|
||||
echo "--- 🎉 모든 복구 및 마이그레이션이 완료되었습니다. ---";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ 오류: " . $e->getMessage() . "\n";
|
||||
}
|
||||
?>
|
||||
89
RECOVER_MIGRATE.php
Normal file
89
RECOVER_MIGRATE.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
/**
|
||||
* Synology Autonomous Architect - Database Migration Script (v2.1)
|
||||
* Target: dda/assets.db (Legacy/Production Recovery Path)
|
||||
* Date: 2026-03-04
|
||||
*/
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
try {
|
||||
// 1. DB 연결 (dda 폴더 내부로 경로 수정)
|
||||
$dbPath = __DIR__ . DIRECTORY_SEPARATOR . 'dda' . DIRECTORY_SEPARATOR . 'assets.db';
|
||||
if (!file_exists($dbPath)) {
|
||||
throw new Exception("데이터베이스 파일({$dbPath})을 찾을 수 없습니다. dda/assets.db 확인이 필요합니다.");
|
||||
}
|
||||
|
||||
$db = new PDO("sqlite:$dbPath");
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
echo "--- [복구 마이그레이션 시작] 노트북 자산 관리 시스템 v2.1 ---\n\n";
|
||||
|
||||
// 2. 백업 생성
|
||||
$backupPath = $dbPath . '.bak_' . date('Ymd_His');
|
||||
if (!copy($dbPath, $backupPath)) {
|
||||
throw new Exception("데이터베이스 백업 생성 실패");
|
||||
}
|
||||
echo "🛡️ 데이터 안전: 작업 전 원본 백업이 생성되었습니다. ($backupPath)\n";
|
||||
|
||||
// 3. 스키마 확장 (신규 규격 컬럼 추가)
|
||||
$newColumns = [
|
||||
'disposal_recipient' => 'TEXT',
|
||||
'disposal_date' => 'TEXT',
|
||||
'real_user' => 'TEXT',
|
||||
'disposal_user_id' => 'INTEGER'
|
||||
];
|
||||
|
||||
foreach ($newColumns as $col => $type) {
|
||||
try {
|
||||
$db->exec("ALTER TABLE laptop_assets ADD COLUMN $col $type");
|
||||
echo "✅ 컬럼 보정 성공: $col\n";
|
||||
} catch (PDOException $e) {
|
||||
if (strpos($e->getMessage(), 'duplicate column name') !== false) {
|
||||
echo "ℹ️ 이미 존재함: $col (스키마 유지)\n";
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 데이터 정합성 - STOCK 자산 '업무용' 명칭 통합
|
||||
$stmtStock = $db->prepare("
|
||||
UPDATE laptop_assets
|
||||
SET assigned_user_name = '업무용(TEMP_44666b)',
|
||||
current_user_id = NULL
|
||||
WHERE status = 'stock'
|
||||
AND (assigned_user_name != '업무용(TEMP_44666b)' OR assigned_user_name IS NULL)
|
||||
");
|
||||
$stmtStock->execute();
|
||||
$affectedStock = $stmtStock->rowCount();
|
||||
echo "✅ 데이터 정합성: STOCK 자산 $affectedStock 건의 입력을 '업무용'으로 통일했습니다.\n";
|
||||
|
||||
// 5. 데이터 정합성 - 매각 대상자 ID 복구 (직원 DB 대조)
|
||||
$allDisposed = $db->query("
|
||||
SELECT id, disposal_recipient
|
||||
FROM laptop_assets
|
||||
WHERE status = 'disposed'
|
||||
AND disposal_recipient IS NOT NULL
|
||||
AND disposal_user_id IS NULL
|
||||
")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$idRestored = 0;
|
||||
$userFinder = $db->prepare("SELECT id FROM users WHERE name = ? LIMIT 1");
|
||||
$idUpdater = $db->prepare("UPDATE laptop_assets SET disposal_user_id = ? WHERE id = ?");
|
||||
|
||||
foreach ($allDisposed as $row) {
|
||||
$userFinder->execute([$row['disposal_recipient']]);
|
||||
$uid = $userFinder->fetchColumn();
|
||||
if ($uid) {
|
||||
$idUpdater->execute([$uid, $row['id']]);
|
||||
$idRestored++;
|
||||
}
|
||||
}
|
||||
echo "✅ 데이터 정합성: DISPOSED 자산 $idRestored 건의 사원 매핑을 완료했습니다.\n\n";
|
||||
|
||||
echo "--- [마이그레이션 성공] 복구된 DB가 최신 규격으로 업데이트되었습니다. ---\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "\n❌ [마이그레이션 실패] 오류 내용: " . $e->getMessage() . "\n";
|
||||
}
|
||||
81
api.php
81
api.php
|
|
@ -669,34 +669,62 @@ LIMIT 1")->fetchColumn();
|
|||
}
|
||||
$updates = [];
|
||||
$params = [];
|
||||
$log_msg = "";
|
||||
|
||||
if ($newStatus) {
|
||||
$updates[] = "status = ?";
|
||||
$params[] = $newStatus;
|
||||
// 만약 상태를 'stock'(재고)으로 변경하는 경우 '업무용'으로 설정, 'disposed'(매각)인 경우 완전 초기화
|
||||
if ($newStatus === 'stock') {
|
||||
$updates[] = "current_user_id = NULL";
|
||||
$updates[] = "assigned_user_name = '업무용(TEMP_44666b)'";
|
||||
$log_msg = "일괄 상태 변경: 재고 (업무용 지정)";
|
||||
} elseif ($newStatus === 'disposed') {
|
||||
$updates[] = "current_user_id = NULL";
|
||||
$updates[] = "assigned_user_name = NULL";
|
||||
$updates[] = "disposal_date = '" . date('Y-m-d') . "'";
|
||||
$log_msg = "일괄 상태 변경: 매각됨";
|
||||
} elseif ($newStatus === 'assigned') {
|
||||
$log_msg = "일괄 상태 변경: 직원배정";
|
||||
}
|
||||
}
|
||||
if ($newModelId) {
|
||||
$updates[] = "model_id = ?";
|
||||
$params[] = $newModelId;
|
||||
$log_msg .= ($log_msg ? ", " : "") . "일괄 모델 변경(ID: $newModelId)";
|
||||
}
|
||||
|
||||
if (empty($updates)) {
|
||||
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),
|
||||
"?"
|
||||
)) . ")";
|
||||
|
||||
$placeholders = implode(",", array_fill(0, count($ids), "?"));
|
||||
$sql = "UPDATE laptop_assets SET " . implode(", ", $updates) . " WHERE id IN ($placeholders)";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute(array_merge($params, $ids));
|
||||
|
||||
// 히스토리 일괄 기록
|
||||
if ($log_msg) {
|
||||
$history_sql = "INSERT INTO asset_history (asset_id, log_type, user_name, action_date, note) VALUES (?, 'bulk_update', '시스템(일괄)', ?, ?)";
|
||||
$h_stmt = $db->prepare($history_sql);
|
||||
foreach ($ids as $id) {
|
||||
$h_stmt->execute([$id, date('Y-m-d'), $log_msg]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'bulk_delete_laptops':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$ids = $data['ids'];
|
||||
if (empty($ids)) {
|
||||
echo json_encode(['success' => false, 'error' => 'No items selected']);
|
||||
break;
|
||||
}
|
||||
$placeholders = implode(",", array_fill(0, count($ids), "?"));
|
||||
$db->prepare("DELETE FROM laptop_assets WHERE id IN ($placeholders)")->execute($ids);
|
||||
// 관련 히스토리도 삭제? 보통 자산 삭제시에는 히스토리도 함께 날리거나 남김. 여기선 DB 정합성을 위해 연쇄 삭제는 안하더라도 자산은 사라짐.
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
|
|
@ -799,6 +827,33 @@ LIMIT 1")->fetchColumn();
|
|||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'bulk_delete_models':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$ids = $data['ids'];
|
||||
if (empty($ids)) {
|
||||
echo json_encode(['success' => false, 'error' => 'No items selected']);
|
||||
break;
|
||||
}
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
$placeholders = implode(",", array_fill(0, count($ids), "?"));
|
||||
|
||||
// 1. 해당 모델을 참조 중인 자산들의 연결 고리 해제 (ID 참조를 NULL로 변경)
|
||||
$stmt1 = $db->prepare("UPDATE laptop_assets SET model_id = NULL WHERE model_id IN ($placeholders)");
|
||||
$stmt1->execute($ids);
|
||||
|
||||
// 2. 노트북 모델 마스터 정보 삭제
|
||||
$stmt2 = $db->prepare("DELETE FROM laptop_models WHERE id IN ($placeholders)");
|
||||
$stmt2->execute($ids);
|
||||
|
||||
$db->commit();
|
||||
echo json_encode(['success' => true]);
|
||||
} catch (Exception $e) {
|
||||
$db->rollBack();
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bulk_update_users':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$userIds = $data['user_ids'];
|
||||
|
|
@ -1033,6 +1088,20 @@ ORDER BY r.id DESC";
|
|||
}
|
||||
break;
|
||||
|
||||
case 'get_settings':
|
||||
$settings = $db->query("SELECT key_name, value FROM system_settings")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
echo json_encode(['success' => true, 'settings' => $settings]);
|
||||
break;
|
||||
|
||||
case 'update_settings':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$stmt = $db->prepare("INSERT OR REPLACE INTO system_settings (key_name, value) VALUES (?, ?)");
|
||||
foreach ($data['settings'] as $key => $value) {
|
||||
$stmt->execute([$key, (string) $value]);
|
||||
}
|
||||
echo json_encode(['success' => true]);
|
||||
break;
|
||||
|
||||
case 'return_general_rental':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$returnDate = date('Y-m-d');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,19 @@
|
|||
<?php
|
||||
$db = new PDO('sqlite:d:\Docker\jasan\assets.db');
|
||||
$cols = $db->query("PRAGMA table_info(laptop_assets)")->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode($cols, JSON_PRETTY_PRINT);
|
||||
try {
|
||||
$db = new PDO('sqlite:d:/Docker/jasan/dda/assets.db');
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
echo "--- LAPTOP_ASSETS SCHEMA ---\n";
|
||||
$res = $db->query('PRAGMA table_info(laptop_assets)')->fetchAll();
|
||||
foreach ($res as $col) {
|
||||
echo "{$col['name']} ({$col['type']})\n";
|
||||
}
|
||||
|
||||
echo "\n--- LAPTOP_MODELS SCHEMA ---\n";
|
||||
$res = $db->query('PRAGMA table_info(laptop_models)')->fetchAll();
|
||||
foreach ($res as $col) {
|
||||
echo "{$col['name']} ({$col['type']})\n";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
9
check_tables.php
Normal file
9
check_tables.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
try {
|
||||
$db = new PDO('sqlite:d:/Docker/jasan/dda/assets.db');
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$tables = $db->query("SELECT name FROM sqlite_master WHERE type='table'")->fetchAll(PDO::FETCH_COLUMN);
|
||||
print_r($tables);
|
||||
} catch (Exception $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
BIN
dda/assets.db
BIN
dda/assets.db
Binary file not shown.
BIN
dda/assets.db.back
Normal file
BIN
dda/assets.db.back
Normal file
Binary file not shown.
BIN
dda/assets.db.bak_20260304_122706
Normal file
BIN
dda/assets.db.bak_20260304_122706
Normal file
Binary file not shown.
BIN
dda/assets.db.bak_20260304_122901
Normal file
BIN
dda/assets.db.bak_20260304_122901
Normal file
Binary file not shown.
5
debug_db.php
Normal file
5
debug_db.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
$db = new PDO('sqlite:d:\Docker\jasan\dda\assets.db');
|
||||
$rows = $db->query("SELECT id, asset_tag, status, assigned_user_name FROM laptop_assets LIMIT 50")->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
?>
|
||||
|
|
@ -67,102 +67,118 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Rental Cards Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
x-show="!loading && filteredRentals.length > 0">
|
||||
<template x-for="rental in filteredRentals" :key="rental.id">
|
||||
<div class="bg-white rounded-[2rem] p-6 shadow-sm border border-slate-100 hover:shadow-xl transition-all relative overflow-hidden flex flex-col h-full"
|
||||
:class="rental.status === 'returned' ? 'opacity-75' : ''">
|
||||
|
||||
<!-- Status Ribbon for Returned -->
|
||||
<template x-if="rental.status === 'returned'">
|
||||
<div class="absolute top-0 right-0">
|
||||
<div
|
||||
class="bg-slate-500 p-1 px-4 text-[10px] font-black text-white uppercase transform rotate-45 translate-x-3 translate-y-1 shadow-sm">
|
||||
Returned
|
||||
<!-- Table List -->
|
||||
<div class="bg-white rounded-[2.5rem] shadow-sm border border-slate-200 overflow-hidden"
|
||||
x-show="!loading && sortedRentals.length > 0">
|
||||
<table class="min-w-full divide-y divide-slate-100">
|
||||
<thead class="bg-slate-50/50">
|
||||
<tr>
|
||||
<th @click="sortBy('user_name')"
|
||||
class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest cursor-pointer hover:bg-slate-100 transition-colors">
|
||||
<div class="flex items-center gap-1">
|
||||
사원명
|
||||
<svg class="w-3 h-3 transition-transform"
|
||||
:class="sortKey === 'user_name' ? (sortOrder === 'asc' ? 'rotate-180' : '') : 'opacity-20'"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</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
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest">
|
||||
임대 품목</th>
|
||||
<th @click="sortBy('rental_start_date')"
|
||||
class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest cursor-pointer hover:bg-slate-100 transition-colors">
|
||||
<div class="flex items-center gap-1">
|
||||
임대일
|
||||
<svg class="w-3 h-3 transition-transform"
|
||||
:class="sortKey === 'rental_start_date' ? (sortOrder === 'asc' ? 'rotate-180' : '') : 'opacity-20'"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</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>
|
||||
</th>
|
||||
<th @click="sortBy('status')"
|
||||
class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest cursor-pointer hover:bg-slate-100 transition-colors">
|
||||
<div class="flex items-center gap-1">
|
||||
상태/경과
|
||||
<svg class="w-3 h-3 transition-transform"
|
||||
:class="sortKey === 'status' ? (sortOrder === 'asc' ? 'rotate-180' : '') : 'opacity-20'"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</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
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-[11px] font-black text-slate-400 uppercase tracking-widest">
|
||||
사유</th>
|
||||
<th
|
||||
class="px-6 py-4 text-right text-[11px] font-black text-slate-400 uppercase tracking-widest">
|
||||
관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
<template x-for="rental in sortedRentals" :key="rental.id">
|
||||
<tr class="hover:bg-slate-50/80 transition-colors group"
|
||||
:class="rental.status === 'returned' ? 'opacity-60 bg-slate-50/30' : ''">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg flex items-center justify-center text-xs font-black"
|
||||
:class="rental.status === 'returned' ? 'bg-slate-100 text-slate-400' : 'bg-emerald-50 text-emerald-600'">
|
||||
<span x-text="rental.user_name.charAt(0)"></span>
|
||||
</div>
|
||||
<span class="text-sm font-black text-slate-900" x-text="rental.user_name"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template x-for="item in (rental.items ? rental.items.split(', ') : [])"
|
||||
:key="item">
|
||||
<span
|
||||
class="px-2 py-0.5 bg-blue-50 text-blue-600 text-[10px] font-black rounded-md border border-blue-100"
|
||||
x-text="item"></span>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-xs font-bold text-slate-600" x-text="rental.rental_start_date"></span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<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
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 bg-blue-50 text-blue-600 rounded-full">
|
||||
<span class="w-1 h-1 bg-blue-500 rounded-full animate-pulse"></span>
|
||||
<span class="text-[10px] font-black"
|
||||
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>
|
||||
<div class="inline-flex items-center bg-slate-100 px-2.5 py-1 rounded-full">
|
||||
<span class="text-[10px] font-black text-slate-500"
|
||||
x-text="'반납 (' + rental.rental_return_date + ')'"></span>
|
||||
</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>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<p class="text-[11px] text-slate-500 font-medium truncate max-w-[200px]"
|
||||
:title="rental.rental_reason" x-text="rental.rental_reason || '-'"></p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<template x-if="rental.status === 'rented'">
|
||||
<button @click="returnRental(rental)"
|
||||
class="px-4 py-1.5 bg-slate-900 text-white rounded-xl font-black text-[10px] hover:bg-blue-600 transition-all active:scale-95">
|
||||
반납 처리
|
||||
</button>
|
||||
</template>
|
||||
<template x-if="rental.status === 'returned'">
|
||||
<span class="text-[10px] font-bold text-slate-300">처리완료</span>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div x-show="loading" class="p-20 flex justify-center">
|
||||
|
|
@ -315,6 +331,8 @@
|
|||
rental_reason: '',
|
||||
items: ['']
|
||||
},
|
||||
sortKey: 'rental_start_date',
|
||||
sortOrder: 'desc',
|
||||
userSearchQuery: '',
|
||||
showUserDropdown: false,
|
||||
|
||||
|
|
@ -361,10 +379,29 @@
|
|||
const q = this.searchQuery.toLowerCase();
|
||||
return this.rentals.filter(r =>
|
||||
(r.user_name && r.user_name.toLowerCase().includes(q)) ||
|
||||
(r.items && r.items.toLowerCase().includes(q))
|
||||
(r.items && r.items.toLowerCase().includes(q)) ||
|
||||
(r.rental_reason && r.rental_reason.toLowerCase().includes(q))
|
||||
);
|
||||
},
|
||||
|
||||
get sortedRentals() {
|
||||
return [...this.filteredRentals].sort((a, b) => {
|
||||
let modifier = this.sortOrder === 'asc' ? 1 : -1;
|
||||
if (a[this.sortKey] < b[this.sortKey]) return -1 * modifier;
|
||||
if (a[this.sortKey] > b[this.sortKey]) return 1 * modifier;
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
|
||||
sortBy(key) {
|
||||
if (this.sortKey === key) {
|
||||
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
this.sortKey = key;
|
||||
this.sortOrder = 'asc';
|
||||
}
|
||||
},
|
||||
|
||||
fetchUsers() {
|
||||
fetch('api.php?action=get_users').then(res => res.json()).then(data => {
|
||||
this.users = data;
|
||||
|
|
|
|||
121
index.php
121
index.php
|
|
@ -60,39 +60,43 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all">
|
||||
<div class="flex justify-between mb-6">
|
||||
<div class="p-4 bg-emerald-50 rounded-2xl text-emerald-600"><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="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" />
|
||||
</svg></div><span
|
||||
class="text-xs font-bold text-emerald-500 bg-emerald-50 px-2.5 py-1 rounded-lg">Security</span>
|
||||
<?php if (($settings['menu_cards_visible'] ?? '1') === '1'): ?>
|
||||
<div
|
||||
class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all">
|
||||
<div class="flex justify-between mb-6">
|
||||
<div class="p-4 bg-emerald-50 rounded-2xl text-emerald-600"><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="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" />
|
||||
</svg></div><span
|
||||
class="text-xs font-bold text-emerald-500 bg-emerald-50 px-2.5 py-1 rounded-lg">Security</span>
|
||||
</div>
|
||||
<h3 class="text-slate-500 font-bold uppercase tracking-wider text-xs mb-1">Access Cards</h3>
|
||||
<div class="flex items-baseline space-x-2 mb-4"><span class="text-4xl font-black text-slate-900"
|
||||
x-text="stats.cards.assigned || 0">0</span><span
|
||||
class="text-slate-400 font-medium tracking-tighter">/ <span
|
||||
x-text="stats.cards.total || 0">0</span></span></div>
|
||||
</div>
|
||||
<h3 class="text-slate-500 font-bold uppercase tracking-wider text-xs mb-1">Access Cards</h3>
|
||||
<div class="flex items-baseline space-x-2 mb-4"><span class="text-4xl font-black text-slate-900"
|
||||
x-text="stats.cards.assigned || 0">0</span><span
|
||||
class="text-slate-400 font-medium tracking-tighter">/ <span
|
||||
x-text="stats.cards.total || 0">0</span></span></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div
|
||||
class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all">
|
||||
<div class="flex justify-between mb-6">
|
||||
<div class="p-4 bg-amber-50 rounded-2xl text-amber-600"><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="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" />
|
||||
</svg></div><span
|
||||
class="text-xs font-bold text-amber-500 bg-amber-50 px-2.5 py-1 rounded-lg">Printing</span>
|
||||
<?php if (($settings['menu_mfp_visible'] ?? '1') === '1'): ?>
|
||||
<div
|
||||
class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all">
|
||||
<div class="flex justify-between mb-6">
|
||||
<div class="p-4 bg-amber-50 rounded-2xl text-amber-600"><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="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" />
|
||||
</svg></div><span
|
||||
class="text-xs font-bold text-amber-500 bg-amber-50 px-2.5 py-1 rounded-lg">Printing</span>
|
||||
</div>
|
||||
<h3 class="text-slate-500 font-bold uppercase tracking-wider text-xs mb-1">MFP Accounts</h3>
|
||||
<div class="flex items-baseline space-x-2 mb-4"><span class="text-4xl font-black text-slate-900"
|
||||
x-text="stats.mfp.assigned || 0">0</span><span
|
||||
class="text-slate-400 font-medium tracking-tighter">/ <span
|
||||
x-text="stats.mfp.total || 0">0</span></span></div>
|
||||
</div>
|
||||
<h3 class="text-slate-500 font-bold uppercase tracking-wider text-xs mb-1">MFP Accounts</h3>
|
||||
<div class="flex items-baseline space-x-2 mb-4"><span class="text-4xl font-black text-slate-900"
|
||||
x-text="stats.mfp.assigned || 0">0</span><span
|
||||
class="text-slate-400 font-medium tracking-tighter">/ <span
|
||||
x-text="stats.mfp.total || 0">0</span></span></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div
|
||||
class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all">
|
||||
|
|
@ -147,32 +151,37 @@
|
|||
<p class="text-xs text-slate-400">자산태그/배정현황</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="cards.php"
|
||||
class="group bg-white p-6 rounded-2xl border border-slate-200 hover:border-blue-500 transition-all flex items-center space-x-4">
|
||||
<div class="bg-slate-50 p-3 rounded-xl group-hover:bg-emerald-50 transition-colors"><svg
|
||||
class="w-6 h-6 text-slate-400 group-hover:text-emerald-500" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="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" />
|
||||
</svg></div>
|
||||
<div>
|
||||
<p class="font-black text-slate-900 group-hover:text-emerald-600 transition-colors">출입증 관리</p>
|
||||
<p class="text-xs text-slate-400">보안실 연동 데이터</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="mfp.php"
|
||||
class="group bg-white p-6 rounded-2xl border border-slate-200 hover:border-blue-500 transition-all flex items-center space-x-4">
|
||||
<div class="bg-slate-50 p-3 rounded-xl group-hover:bg-amber-50 transition-colors"><svg
|
||||
class="w-6 h-6 text-slate-400 group-hover:text-amber-500" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="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" />
|
||||
</svg></div>
|
||||
<div>
|
||||
<p class="font-black text-slate-900 group-hover:text-amber-600 transition-colors">복합기 계정</p>
|
||||
<p class="text-xs text-slate-400">임시 계정/pw 관리</p>
|
||||
</div>
|
||||
</a>
|
||||
<?php if (($settings['menu_cards_visible'] ?? '1') === '1'): ?>
|
||||
<a href="cards.php"
|
||||
class="group bg-white p-6 rounded-2xl border border-slate-200 hover:border-blue-500 transition-all flex items-center space-x-4">
|
||||
<div class="bg-slate-50 p-3 rounded-xl group-hover:bg-emerald-50 transition-colors"><svg
|
||||
class="w-6 h-6 text-slate-400 group-hover:text-emerald-500" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="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" />
|
||||
</svg></div>
|
||||
<div>
|
||||
<p class="font-black text-slate-900 group-hover:text-emerald-600 transition-colors">출입증 관리</p>
|
||||
<p class="text-xs text-slate-400">보안실 연동 데이터</p>
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (($settings['menu_mfp_visible'] ?? '1') === '1'): ?>
|
||||
<a href="mfp.php"
|
||||
class="group bg-white p-6 rounded-2xl border border-slate-200 hover:border-blue-500 transition-all flex items-center space-x-4">
|
||||
<div class="bg-slate-50 p-3 rounded-xl group-hover:bg-amber-50 transition-colors"><svg
|
||||
class="w-6 h-6 text-slate-400 group-hover:text-amber-500" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="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" />
|
||||
</svg></div>
|
||||
<div>
|
||||
<p class="font-black text-slate-900 group-hover:text-amber-600 transition-colors">복합기 계정</p>
|
||||
<p class="text-xs text-slate-400">임시 계정/pw 관리</p>
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
|||
17
init_settings.php
Normal file
17
init_settings.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
require_once 'config.php';
|
||||
try {
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS system_settings (
|
||||
key_name TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
)");
|
||||
|
||||
// 기본값 설정 (이미 있으면 무시)
|
||||
$stmt = $db->prepare("INSERT OR IGNORE INTO system_settings (key_name, value) VALUES (?, ?)");
|
||||
$stmt->execute(['menu_cards_visible', '1']);
|
||||
$stmt->execute(['menu_mfp_visible', '1']);
|
||||
|
||||
echo "✅ system_settings table created and initialized.";
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Error: " . $e->getMessage();
|
||||
}
|
||||
221
laptops.php
221
laptops.php
|
|
@ -68,32 +68,32 @@
|
|||
<!-- Tabs for filtering -->
|
||||
<div class="mb-6 flex items-center justify-between border-b border-slate-200">
|
||||
<div class="flex gap-8">
|
||||
<button @click="currentTab = 'all'"
|
||||
class="pb-4 text-sm font-black transition-all relative"
|
||||
<button @click="currentTab = 'all'" class="pb-4 text-sm font-black transition-all relative"
|
||||
:class="currentTab === 'all' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'">
|
||||
전체 자산
|
||||
<div x-show="currentTab === 'all'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
|
||||
<div x-show="currentTab === 'all'" x-transition
|
||||
class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
|
||||
</button>
|
||||
<button @click="currentTab = 'stock'"
|
||||
class="pb-4 text-sm font-black transition-all relative"
|
||||
<button @click="currentTab = 'stock'" class="pb-4 text-sm font-black transition-all relative"
|
||||
:class="currentTab === 'stock' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'">
|
||||
재고 현황
|
||||
<div x-show="currentTab === 'stock'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
|
||||
<div x-show="currentTab === 'stock'" x-transition
|
||||
class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
|
||||
</button>
|
||||
<button @click="currentTab = 'assigned'"
|
||||
class="pb-4 text-sm font-black transition-all relative"
|
||||
<button @click="currentTab = 'assigned'" class="pb-4 text-sm font-black transition-all relative"
|
||||
:class="currentTab === 'assigned' ? 'text-blue-600' : 'text-slate-400 hover:text-slate-600'">
|
||||
지급 완료
|
||||
<div x-show="currentTab === 'assigned'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
|
||||
<div x-show="currentTab === 'assigned'" x-transition
|
||||
class="absolute bottom-0 left-0 w-full h-1 bg-blue-600 rounded-t-full"></div>
|
||||
</button>
|
||||
<button @click="currentTab = 'disposed'"
|
||||
class="pb-4 text-sm font-black transition-all relative"
|
||||
<button @click="currentTab = 'disposed'" class="pb-4 text-sm font-black transition-all relative"
|
||||
:class="currentTab === 'disposed' ? 'text-rose-600' : 'text-slate-400 hover:text-slate-600'">
|
||||
매각 자산
|
||||
<div x-show="currentTab === 'disposed'" x-transition class="absolute bottom-0 left-0 w-full h-1 bg-rose-600 rounded-t-full"></div>
|
||||
<div x-show="currentTab === 'disposed'" x-transition
|
||||
class="absolute bottom-0 left-0 w-full h-1 bg-rose-600 rounded-t-full"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pb-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">
|
||||
Total: <span class="text-slate-900" x-text="filteredAssets.length"></span> Items
|
||||
</div>
|
||||
|
|
@ -113,22 +113,37 @@
|
|||
<div class="h-8 w-px bg-white/10 mx-2"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<select x-model="bulkStatus"
|
||||
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[150px]">
|
||||
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[120px]">
|
||||
<option value="">상태 변경...</option>
|
||||
<option value="assigned">지급됨(assigned)</option>
|
||||
<option value="stock">재고(stock)</option>
|
||||
<option value="disposed">매각됨(disposed)</option>
|
||||
<option value="assigned">지급됨</option>
|
||||
<option value="stock">재고</option>
|
||||
</select>
|
||||
<select x-model="bulkModelId"
|
||||
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[200px]">
|
||||
class="bg-slate-800 border-none rounded-xl text-xs font-bold px-4 py-2 focus:ring-2 focus:ring-blue-500 appearance-none shadow-inner min-w-[150px]">
|
||||
<option value="">모델 변경...</option>
|
||||
<template x-for="model in models" :key="model.id">
|
||||
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button @click="applyBulkUpdate"
|
||||
class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">일괄
|
||||
적용</button>
|
||||
class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-blue-500/20 active:scale-95">
|
||||
변경 적용
|
||||
</button>
|
||||
|
||||
<div class="h-6 w-px bg-white/10 mx-1"></div>
|
||||
|
||||
<button @click="applyBulkDisposal"
|
||||
class="bg-rose-600 hover:bg-rose-700 px-4 py-2 rounded-xl text-xs font-black transition-all shadow-lg shadow-rose-500/20 active:scale-95 flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5" 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>
|
||||
매각 완료
|
||||
</button>
|
||||
<button @click="bulkDelete"
|
||||
class="bg-slate-700 hover:bg-rose-600 px-4 py-2 rounded-xl text-xs font-black transition-all shadow-lg active:scale-95">
|
||||
삭제
|
||||
</button>
|
||||
<button @click="selectedIds = []"
|
||||
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 hover:text-white transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -236,7 +251,8 @@
|
|||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<template x-if="asset.status === 'disposed'">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter bg-rose-50 text-rose-600 border-rose-100 w-fit">매각됨</span>
|
||||
<span
|
||||
class="px-3 py-1 rounded-full text-[10px] font-black border uppercase tracking-tighter bg-rose-50 text-rose-600 border-rose-100 w-fit">매각됨</span>
|
||||
<div class="text-[10px] font-bold text-slate-400 group-hover:text-slate-600">
|
||||
매각: <span x-text="asset.disposal_recipient"></span><br>
|
||||
실사용: <span x-text="asset.real_user || '-'"></span><br>
|
||||
|
|
@ -285,25 +301,28 @@
|
|||
<div class="flex justify-between items-center mb-8">
|
||||
<div class="flex items-center gap-6">
|
||||
<h3 class="text-2xl font-black text-slate-900" x-text="isEdit ? '자산 정보 수정' : '신규 자산 등록'"></h3>
|
||||
|
||||
|
||||
<!-- 자산 운영 상태 (버튼형 선택기) -->
|
||||
<div class="flex bg-slate-100 p-1 rounded-2xl">
|
||||
<button type="button" @click="setStatus('stock')"
|
||||
:class="formData.status === 'stock' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
|
||||
class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2">
|
||||
<div class="w-1.5 h-1.5 rounded-full" :class="formData.status === 'stock' ? 'bg-blue-600' : 'bg-slate-300'"></div>
|
||||
<div class="w-1.5 h-1.5 rounded-full"
|
||||
:class="formData.status === 'stock' ? 'bg-blue-600' : 'bg-slate-300'"></div>
|
||||
재고
|
||||
</button>
|
||||
<button type="button" @click="setStatus('assigned')"
|
||||
:class="formData.status === 'assigned' ? 'bg-white text-indigo-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
|
||||
class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2">
|
||||
<div class="w-1.5 h-1.5 rounded-full" :class="formData.status === 'assigned' ? 'bg-indigo-600' : 'bg-slate-300'"></div>
|
||||
<div class="w-1.5 h-1.5 rounded-full"
|
||||
:class="formData.status === 'assigned' ? 'bg-indigo-600' : 'bg-slate-300'"></div>
|
||||
직원배정
|
||||
</button>
|
||||
<button type="button" @click="setStatus('disposed')"
|
||||
:class="formData.status === 'disposed' ? 'bg-white text-rose-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
|
||||
class="px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2">
|
||||
<div class="w-1.5 h-1.5 rounded-full" :class="formData.status === 'disposed' ? 'bg-rose-600' : 'bg-slate-300'"></div>
|
||||
<div class="w-1.5 h-1.5 rounded-full"
|
||||
:class="formData.status === 'disposed' ? 'bg-rose-600' : 'bg-slate-300'"></div>
|
||||
매각
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -325,7 +344,8 @@
|
|||
:class="isAssetTagDuplicate ? 'border-rose-500 ring-2 ring-rose-200' : 'border-slate-200'"
|
||||
class="w-full px-4 py-3 bg-slate-50 border rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all font-bold">
|
||||
<template x-if="isAssetTagDuplicate">
|
||||
<p class="text-[10px] text-rose-500 font-bold mt-1.5 ml-1 animate-pulse italic">이미 등록된 자산번호입니다.</p>
|
||||
<p class="text-[10px] text-rose-500 font-bold mt-1.5 ml-1 animate-pulse italic">이미 등록된
|
||||
자산번호입니다.</p>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -334,7 +354,8 @@
|
|||
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 appearance-none font-bold text-sm text-slate-700">
|
||||
<option value="">모델 선택</option>
|
||||
<template x-for="model in models" :key="model.id">
|
||||
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name"></option>
|
||||
<option :value="model.id" x-text="'[' + model.manufacturer + '] ' + model.model_name">
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -346,20 +367,23 @@
|
|||
</div>
|
||||
|
||||
<!-- 매각 정보 (상태가 매각일 때만 노출) -->
|
||||
<div x-show="formData.status === 'disposed'" x-transition
|
||||
<div x-show="formData.status === 'disposed'" x-transition
|
||||
class="bg-rose-50 p-6 rounded-3xl border border-rose-100 space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="relative" x-on:click.outside="showDisposalDropdown = false">
|
||||
<label class="block text-[10px] font-black text-rose-400 uppercase mb-2 ml-1">매각 대상자 (사내 직원)</label>
|
||||
<label class="block text-[10px] font-black text-rose-400 uppercase mb-2 ml-1">매각 대상자 (사내
|
||||
직원)</label>
|
||||
<div class="relative">
|
||||
<input type="text" x-model="disposalSearchQuery"
|
||||
@focus="showDisposalDropdown = true; disposalSearchQuery = ''"
|
||||
@input="showDisposalDropdown = true" placeholder="매각 받은 직원 검색"
|
||||
class="w-full px-4 py-3 bg-white border border-rose-200 rounded-xl focus:ring-2 focus:ring-rose-500 outline-none text-sm font-bold text-slate-700">
|
||||
|
||||
|
||||
<div class="absolute right-3 top-3.5">
|
||||
<svg class="w-4 h-4 text-rose-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
<svg class="w-4 h-4 text-rose-300" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -367,12 +391,16 @@
|
|||
<div x-show="showDisposalDropdown" x-transition
|
||||
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar">
|
||||
<template x-for="user in filteredDisposalUsers" :key="user.id">
|
||||
<div @click="selectDisposalUser(user)" class="px-4 py-3 hover:bg-rose-50 cursor-pointer flex items-center justify-between transition-colors group">
|
||||
<div @click="selectDisposalUser(user)"
|
||||
class="px-4 py-3 hover:bg-rose-50 cursor-pointer flex items-center justify-between transition-colors group">
|
||||
<div>
|
||||
<div class="text-sm font-bold text-slate-700 group-hover:text-rose-600" x-text="user.name"></div>
|
||||
<div class="text-[10px] text-slate-400 font-medium" x-text="user.dept_name || '부서 정보 없음'"></div>
|
||||
<div class="text-sm font-bold text-slate-700 group-hover:text-rose-600"
|
||||
x-text="user.name"></div>
|
||||
<div class="text-[10px] text-slate-400 font-medium"
|
||||
x-text="user.dept_name || '부서 정보 없음'"></div>
|
||||
</div>
|
||||
<div class="text-[10px] font-black text-slate-300 group-hover:text-rose-400" x-text="user.emp_id"></div>
|
||||
<div class="text-[10px] font-black text-slate-300 group-hover:text-rose-400"
|
||||
x-text="user.emp_id"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -394,13 +422,15 @@
|
|||
<div class="bg-blue-50/50 p-6 rounded-[2rem] border border-blue-100/50 space-y-4">
|
||||
<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">Individual Asset Info</h4>
|
||||
<h4 class="text-xs font-black text-blue-600 uppercase tracking-widest">Individual Asset Info
|
||||
</h4>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="relative" x-on:click.outside="showUserDropdown = false">
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">
|
||||
배정 사용자
|
||||
<span x-show="formData.status === 'assigned' && !formData.current_user_id" class="text-rose-500 ml-2 animate-pulse font-bold">[값이 없음]</span>
|
||||
배정 사용자
|
||||
<span x-show="formData.status === 'assigned' && !formData.current_user_id"
|
||||
class="text-rose-500 ml-2 animate-pulse font-bold">[값이 없음]</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="text" x-model="userSearchQuery"
|
||||
|
|
@ -409,15 +439,19 @@
|
|||
:disabled="formData.status !== 'assigned'"
|
||||
:class="formData.status !== 'assigned' ? 'bg-slate-100 border-dashed cursor-not-allowed opacity-75' : 'bg-white'"
|
||||
class="w-full px-4 py-3 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 class="absolute right-3 top-3.5 flex items-center gap-2">
|
||||
<button type="button" x-show="userSearchQuery && formData.status === 'assigned'" @click="clearUser()" class="text-slate-400 hover:text-slate-600">
|
||||
<button type="button" x-show="userSearchQuery && formData.status === 'assigned'"
|
||||
@click="clearUser()" class="text-slate-400 hover:text-slate-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -425,24 +459,30 @@
|
|||
<div x-show="showUserDropdown" x-transition
|
||||
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar">
|
||||
<template x-for="user in filteredUsers" :key="user.id">
|
||||
<div @click="selectUser(user)" class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
|
||||
<div @click="selectUser(user)"
|
||||
class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
|
||||
<div>
|
||||
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600" x-text="user.name"></div>
|
||||
<div class="text-[10px] text-slate-400 font-medium" x-text="user.dept_name || '부서 정보 없음'"></div>
|
||||
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600"
|
||||
x-text="user.name"></div>
|
||||
<div class="text-[10px] text-slate-400 font-medium"
|
||||
x-text="user.dept_name || '부서 정보 없음'"></div>
|
||||
</div>
|
||||
<div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400" x-text="user.emp_id"></div>
|
||||
<div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400"
|
||||
x-text="user.emp_id"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP 주소</label>
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase mb-2 ml-1">고정 IP
|
||||
주소</label>
|
||||
<input type="text" x-model="formData.ip_address" placeholder="192.168.x.x"
|
||||
@focus="if(!formData.ip_address) formData.ip_address = '192.168.'"
|
||||
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>
|
||||
<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>
|
||||
|
|
@ -463,15 +503,18 @@
|
|||
<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">CPU</label>
|
||||
<input type="text" x-model="formData.cpu" class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium">
|
||||
<input type="text" x-model="formData.cpu"
|
||||
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">NPU</label>
|
||||
<input type="text" x-model="formData.npu" class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium">
|
||||
<input type="text" x-model="formData.npu"
|
||||
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-500 uppercase mb-2">RAM</label>
|
||||
<input type="text" x-model="formData.ram" class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium">
|
||||
<input type="text" x-model="formData.ram"
|
||||
class="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm font-medium">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -479,15 +522,19 @@
|
|||
<div class="bg-slate-50 p-4 rounded-xl space-y-3">
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase">Storage 0</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input type="text" x-model="formData.hdd0_model" placeholder="모델명/타입" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
||||
<input type="text" x-model="formData.hdd0_capacity" placeholder="용량" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
||||
<input type="text" x-model="formData.hdd0_model" placeholder="모델명/타입"
|
||||
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
||||
<input type="text" x-model="formData.hdd0_capacity" placeholder="용량"
|
||||
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-slate-50 p-4 rounded-xl space-y-3">
|
||||
<label class="block text-[10px] font-black text-slate-400 uppercase">Storage 1</label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input type="text" x-model="formData.hdd1_model" placeholder="모델명/타입" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
||||
<input type="text" x-model="formData.hdd1_capacity" placeholder="용량" class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
||||
<input type="text" x-model="formData.hdd1_model" placeholder="모델명/타입"
|
||||
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
||||
<input type="text" x-model="formData.hdd1_capacity" placeholder="용량"
|
||||
class="w-full px-3 py-2 bg-white border border-slate-200 rounded-lg text-sm font-medium">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -496,7 +543,8 @@
|
|||
<div class="bg-slate-50 p-6 rounded-[2rem] border border-slate-100 space-y-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-slate-400"></div>
|
||||
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">Model Base Info (Read Only)</h4>
|
||||
<h4 class="text-xs font-black text-slate-400 uppercase tracking-widest">Model Base Info (Read
|
||||
Only)</h4>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
|
|
@ -524,8 +572,11 @@
|
|||
</div>
|
||||
|
||||
<div class="pt-4 flex gap-4">
|
||||
<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>
|
||||
<button type="submit" class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all" x-text="isEdit ? '수정 완료' : '자산 등록하기'"></button>
|
||||
<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>
|
||||
<button type="submit"
|
||||
class="flex-1 py-4 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-200 hover:bg-blue-700 transition-all"
|
||||
x-text="isEdit ? '수정 완료' : '자산 등록하기'"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -656,7 +707,7 @@
|
|||
})
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.success) {
|
||||
window.showAlert(`${this.selectedIds.length}개의 자산 정보가 일괄 변경되었습니다.`, '변경 성공', 'success');
|
||||
window.showAlert(`${this.selectedIds.length}개의 자산 속성이 변경되었습니다.`, '변경 성공', 'success');
|
||||
this.selectedIds = [];
|
||||
this.bulkStatus = '';
|
||||
this.bulkModelId = '';
|
||||
|
|
@ -667,6 +718,50 @@
|
|||
});
|
||||
},
|
||||
|
||||
applyBulkDisposal() {
|
||||
if (this.selectedIds.length === 0) return;
|
||||
if (!confirm(`선택한 ${this.selectedIds.length}개의 자산을 '매각 완료' 처리하시겠습니까?\n이 작업은 되돌릴 수 없으며 자산이 매각 탭으로 이동합니다.`)) return;
|
||||
|
||||
fetch('api.php?action=bulk_update_laptops', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ids: this.selectedIds,
|
||||
status: 'disposed'
|
||||
})
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.success) {
|
||||
window.showAlert(`${this.selectedIds.length}개의 자산이 매각 처리되었습니다.`, '매각 완료', 'success');
|
||||
this.selectedIds = [];
|
||||
this.fetchAssets();
|
||||
this.currentTab = 'disposed'; // 매각 탭으로 보냄
|
||||
} else {
|
||||
window.showAlert('매각 처리에 실패했습니다.', '오류', 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
bulkDelete() {
|
||||
if (this.selectedIds.length === 0) return;
|
||||
if (!confirm(`선택한 ${this.selectedIds.length}개의 자산을 시스템에서 영구적으로 삭제하시겠습니까?`)) return;
|
||||
|
||||
fetch('api.php?action=bulk_delete_laptops', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ids: this.selectedIds
|
||||
})
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.success) {
|
||||
window.showAlert(`${this.selectedIds.length}개의 자산이 삭제되었습니다.`, '삭제 완료', 'success');
|
||||
this.selectedIds = [];
|
||||
this.fetchAssets();
|
||||
} else {
|
||||
window.showAlert('자산 삭제에 실패했습니다.', '오류', 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
fetchAssets() {
|
||||
const scrollPos = window.scrollY;
|
||||
this.loading = true;
|
||||
|
|
@ -679,7 +774,7 @@
|
|||
|
||||
get filteredAssets() {
|
||||
let result = [...this.assets];
|
||||
|
||||
|
||||
// Tab filtering
|
||||
if (this.currentTab === 'stock') {
|
||||
result = result.filter(a => a.status === 'stock');
|
||||
|
|
@ -798,7 +893,7 @@
|
|||
window.showAlert('직원배정 상태인 경우 반드시 사용자를 선택해야 합니다.', '입력 오류', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (this.formData.status === 'disposed' && !this.formData.disposal_recipient) {
|
||||
window.showAlert('매각 상태인 경우 매각 대상자를 입력해야 합니다.', '입력 오류', 'error');
|
||||
return;
|
||||
|
|
|
|||
140
models.php
140
models.php
|
|
@ -5,7 +5,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DB 관리 | FKI ASSET</title>
|
||||
<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">
|
||||
|
|
@ -33,8 +33,8 @@
|
|||
<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">DB 마스터 관리</h2>
|
||||
<p class="text-slate-500 mt-1">시스템 운영을 위한 기초 자산 모델 및 조직 정보를 관리합니다.</p>
|
||||
<h2 class="text-3xl font-extrabold text-slate-900 tracking-tight">시스템 관리</h2>
|
||||
<p class="text-slate-500 mt-1">시스템 운영을 위한 기초 자산 모델, 조직 정보 및 메뉴 노출 설정을 관리합니다.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Search Bar for Models -->
|
||||
|
|
@ -82,6 +82,9 @@
|
|||
<button @click="currentTab = 'depts'"
|
||||
:class="currentTab === 'depts' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'"
|
||||
class="px-6 py-2 rounded-xl text-sm font-black transition-all">부서 관리</button>
|
||||
<button @click="currentTab = 'menus'"
|
||||
:class="currentTab === 'menus' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'"
|
||||
class="px-6 py-2 rounded-xl text-sm font-black transition-all">메뉴 관리</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: Laptop Models -->
|
||||
|
|
@ -103,6 +106,9 @@
|
|||
class="bg-slate-800 border-none rounded-lg text-xs font-bold px-3 py-1.5 focus:ring-1 focus:ring-blue-500 w-40 text-white">
|
||||
<button @click="applyBulkUpdate"
|
||||
class="bg-blue-500 hover:bg-blue-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">적용</button>
|
||||
<div class="h-4 w-px bg-slate-700 mx-1"></div>
|
||||
<button @click="bulkDelete"
|
||||
class="bg-slate-700 hover:bg-rose-600 px-4 py-1.5 rounded-lg text-xs font-black transition-all">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="selectedIds = []" class="text-slate-400 hover:text-white transition-colors">
|
||||
|
|
@ -200,7 +206,8 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div x-show="filteredModels.length === 0" class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300 mt-6">
|
||||
<div x-show="filteredModels.length === 0"
|
||||
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300 mt-6">
|
||||
<p class="text-slate-400 font-medium italic">일치하는 모델 정보가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -250,10 +257,87 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div x-show="filteredDepts.length === 0" class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300 mt-6">
|
||||
<div x-show="filteredDepts.length === 0"
|
||||
class="p-20 text-center bg-white rounded-3xl border border-dashed border-slate-300 mt-6">
|
||||
<p class="text-slate-400 font-medium italic">일치하는 부서 정보가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 3: Menu Management -->
|
||||
<div x-show="currentTab === 'menus'" x-transition>
|
||||
<div class="bg-white rounded-[2.5rem] shadow-sm border border-slate-200 overflow-hidden p-10">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h3 class="text-2xl font-black text-slate-900 mb-2">상단 메뉴 노출 관리</h3>
|
||||
<p class="text-slate-500 mb-10 font-medium">네비게이션 바에서 특정 메뉴의 활성화 여부를 실시간으로 제어합니다.</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- 출입증 현황 -->
|
||||
<div
|
||||
class="flex items-center justify-between p-6 bg-slate-50 rounded-3xl border border-slate-100 transition-all hover:bg-white hover:shadow-xl hover:border-blue-100 group">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-12 h-12 bg-white rounded-2xl flex items-center justify-center shadow-sm group-hover:bg-blue-50 transition-colors">
|
||||
<svg class="w-6 h-6 text-slate-400 group-hover:text-blue-500 transition-colors"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="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" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-black text-slate-800">출입증 현황</h4>
|
||||
<p class="text-xs text-slate-500 font-bold uppercase tracking-wider">Access Card
|
||||
Status</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="toggleMenu('menu_cards_visible')"
|
||||
:class="settings.menu_cards_visible === '1' ? 'bg-blue-600' : 'bg-slate-300'"
|
||||
class="relative inline-flex h-7 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
<span :class="settings.menu_cards_visible === '1' ? 'translate-x-5' : 'translate-x-0'"
|
||||
class="pointer-events-none inline-block h-6 w-6 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 복합기 계정 -->
|
||||
<div
|
||||
class="flex items-center justify-between p-6 bg-slate-50 rounded-3xl border border-slate-100 transition-all hover:bg-white hover:shadow-xl hover:border-blue-100 group">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-12 h-12 bg-white rounded-2xl flex items-center justify-center shadow-sm group-hover:bg-blue-50 transition-colors">
|
||||
<svg class="w-6 h-6 text-slate-400 group-hover:text-blue-500 transition-colors"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="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" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-black text-slate-800">복합기 계정</h4>
|
||||
<p class="text-xs text-slate-500 font-bold uppercase tracking-wider">MFP Account
|
||||
Management</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="toggleMenu('menu_mfp_visible')"
|
||||
:class="settings.menu_mfp_visible === '1' ? 'bg-blue-600' : 'bg-slate-300'"
|
||||
class="relative inline-flex h-7 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
<span :class="settings.menu_mfp_visible === '1' ? 'translate-x-5' : 'translate-x-0'"
|
||||
class="pointer-events-none inline-block h-6 w-6 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 p-6 bg-blue-50 rounded-3xl border border-blue-100 flex items-start gap-4">
|
||||
<div class="w-10 h-10 bg-blue-600 rounded-2xl flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-5 text-white" 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>
|
||||
<p class="text-sm font-bold text-blue-800 leading-relaxed">
|
||||
메뉴 설정을 변경하면 즉시 시스템에 반영됩니다. 특정 기능을 한시적으로 제한하거나 보안 정책에 따라 메뉴를 숨기고 싶을 때 활용하세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Model Modal -->
|
||||
|
|
@ -474,6 +558,9 @@
|
|||
return {
|
||||
currentTab: 'models',
|
||||
|
||||
// Settings Data
|
||||
settings: { menu_cards_visible: '1', menu_mfp_visible: '1' },
|
||||
|
||||
// Model Data
|
||||
models: [],
|
||||
showModal: false,
|
||||
|
|
@ -522,6 +609,7 @@
|
|||
init() {
|
||||
this.fetchModels();
|
||||
this.fetchDepts();
|
||||
this.fetchSettings();
|
||||
},
|
||||
|
||||
// Model Methods
|
||||
|
|
@ -551,6 +639,7 @@
|
|||
})
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.success) {
|
||||
window.showAlert(`${this.selectedIds.length}개의 모델 정보가 일괄 변경되었습니다.`, '성공', 'success');
|
||||
this.selectedIds = [];
|
||||
this.bulkManufacturer = '';
|
||||
this.bulkProductName = '';
|
||||
|
|
@ -558,6 +647,30 @@
|
|||
}
|
||||
});
|
||||
},
|
||||
bulkDelete() {
|
||||
if (this.selectedIds.length === 0) return;
|
||||
window.showConfirm(
|
||||
`선택한 ${this.selectedIds.length}개의 모델 정보를 시스템에서 영구적으로 삭제하시겠습니까?`,
|
||||
() => {
|
||||
fetch('api.php?action=bulk_delete_models', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ids: this.selectedIds
|
||||
})
|
||||
}).then(res => res.json()).then(data => {
|
||||
if (data.success) {
|
||||
window.showAlert(`${this.selectedIds.length}개의 모델 정보가 삭제되었습니다.`, '삭제 완료', 'success');
|
||||
this.selectedIds = [];
|
||||
this.fetchModels();
|
||||
} else {
|
||||
window.showAlert(data.error || '모델 삭제에 실패했습니다.', '오류', 'error');
|
||||
}
|
||||
});
|
||||
},
|
||||
'모델 영구 삭제'
|
||||
);
|
||||
},
|
||||
openAddModal() {
|
||||
this.isEdit = false;
|
||||
this.formData = {
|
||||
|
|
@ -710,6 +823,23 @@
|
|||
},
|
||||
'부서 영구 삭제'
|
||||
);
|
||||
},
|
||||
fetchSettings() {
|
||||
fetch('api.php?action=get_settings')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) this.settings = data.settings;
|
||||
});
|
||||
},
|
||||
toggleMenu(key) {
|
||||
const finalValue = this.settings[key] === '1' ? '0' : '1';
|
||||
this.settings[key] = finalValue;
|
||||
|
||||
fetch('api.php?action=update_settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ settings: { [key]: finalValue } })
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
nav.php
25
nav.php
|
|
@ -3,6 +3,7 @@
|
|||
* nav.php
|
||||
* 공통 네비게이션 컴포넌트 (최상단 고정형 리본 메뉴)
|
||||
*/
|
||||
require_once 'config.php';
|
||||
$current_page = basename($_SERVER['PHP_SELF']);
|
||||
?>
|
||||
<nav class="fixed top-0 left-0 right-0 bg-[#0f172a] text-white z-[100] shadow-2xl border-b border-blue-500/20 px-6">
|
||||
|
|
@ -25,18 +26,34 @@ $current_page = basename($_SERVER['PHP_SELF']);
|
|||
<!-- Horizontal Menu -->
|
||||
<div class="flex-1 flex items-center justify-center space-x-1 lg:space-x-4 overflow-x-auto no-scrollbar py-2">
|
||||
<?php
|
||||
// 시스템 설정 로드
|
||||
$settings = [];
|
||||
try {
|
||||
$settings = $db->query("SELECT key_name, value FROM system_settings")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||
} catch (Exception $e) {
|
||||
// 테이블이 없거나 에러 발생 시 기본값
|
||||
$settings = ['menu_cards_visible' => '1', 'menu_mfp_visible' => '1'];
|
||||
}
|
||||
|
||||
$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' => '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' => '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' => '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' => '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']
|
||||
];
|
||||
|
||||
if (($settings['menu_cards_visible'] ?? '1') === '1') {
|
||||
$menu_items[] = ['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'];
|
||||
}
|
||||
|
||||
if (($settings['menu_mfp_visible'] ?? '1') === '1') {
|
||||
$menu_items[] = ['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'];
|
||||
}
|
||||
|
||||
$menu_items[] = ['url' => 'models.php', 'name' => '관리자페이지', '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'];
|
||||
$menu_items[] = ['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):
|
||||
$is_active = ($current_page == $item['url']);
|
||||
?>
|
||||
|
|
|
|||
Loading…
Reference in a new issue