기능업데이트
This commit is contained in:
parent
9ab5065026
commit
acc2969dbe
5 changed files with 186 additions and 18 deletions
32
api.php
32
api.php
|
|
@ -94,13 +94,41 @@ try {
|
||||||
$users_general = $db->query("SELECT COUNT(*) FROM users WHERE accounting_type = '일반회계'")->fetchColumn();
|
$users_general = $db->query("SELECT COUNT(*) FROM users WHERE accounting_type = '일반회계'")->fetchColumn();
|
||||||
$users_special = $db->query("SELECT COUNT(*) FROM users WHERE accounting_type = '특별회계'")->fetchColumn();
|
$users_special = $db->query("SELECT COUNT(*) FROM users WHERE accounting_type = '특별회계'")->fetchColumn();
|
||||||
|
|
||||||
|
// Asset Replacement Analysis (4 years)
|
||||||
|
$current_year = (int)date('Y');
|
||||||
|
|
||||||
|
// 4년 이상 경과 (매각 대상)
|
||||||
|
$replacement_targets = $db->query("SELECT a.*, m.model_name, m.manufacturer
|
||||||
|
FROM laptop_assets a
|
||||||
|
LEFT JOIN laptop_models m ON a.model_id = m.id
|
||||||
|
WHERE a.status != 'disposed'
|
||||||
|
AND a.purchase_date IS NOT NULL
|
||||||
|
AND CAST(SUBSTR(a.purchase_date, 1, 4) AS INTEGER) <= " . ($current_year - 4))
|
||||||
|
->fetchAll();
|
||||||
|
|
||||||
|
// 4년차 도래 (올해 매각 예정 대상)
|
||||||
|
// 예: 2022년 구매 -> 2026년 매각대상 (만 4년이 되는 해)
|
||||||
|
$upcoming_replacements = $db->query("SELECT a.*, m.model_name, m.manufacturer
|
||||||
|
FROM laptop_assets a
|
||||||
|
LEFT JOIN laptop_models m ON a.model_id = m.id
|
||||||
|
WHERE a.status != 'disposed'
|
||||||
|
AND a.purchase_date IS NOT NULL
|
||||||
|
AND CAST(SUBSTR(a.purchase_date, 1, 4) AS INTEGER) = " . ($current_year - 3))
|
||||||
|
->fetchAll();
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'laptops' => ['total' => (int) $laptop_total, 'assigned' => (int) $laptop_assigned, 'disposed' => (int) $laptop_disposed],
|
'laptops' => ['total' => (int) $laptop_total, 'assigned' => (int) $laptop_assigned, 'disposed' => (int) $laptop_disposed],
|
||||||
'cards' => ['total' => (int) $card_total, 'assigned' => (int) $card_assigned],
|
'cards' => ['total' => (int) $card_total, 'assigned' => (int) $card_assigned],
|
||||||
'mfp' => ['total' => (int) $mfp_total, 'active' => (int) $mfp_active],
|
'mfp' => ['total' => (int) $mfp_total, 'active' => (int) $mfp_active],
|
||||||
'users' => ['general' => (int) $users_general, 'special' => (int) $users_special]
|
'users' => ['general' => (int) $users_general, 'special' => (int) $users_special],
|
||||||
|
'replacements' => [
|
||||||
|
'targets' => $replacement_targets,
|
||||||
|
'upcoming' => $upcoming_replacements,
|
||||||
|
'current_year' => $current_year
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'get_users':
|
case 'get_users':
|
||||||
$query = "WITH RECURSIVE dept_path(id, path) AS (
|
$query = "WITH RECURSIVE dept_path(id, path) AS (
|
||||||
|
|
@ -226,7 +254,7 @@ LEFT JOIN users u ON a.current_user_id = u.id";
|
||||||
$query = "SELECT a.*, m.model_name, m.manufacturer, m.product_name
|
$query = "SELECT a.*, m.model_name, m.manufacturer, m.product_name
|
||||||
FROM laptop_assets a
|
FROM laptop_assets a
|
||||||
LEFT JOIN laptop_models m ON a.model_id = m.id
|
LEFT JOIN laptop_models m ON a.model_id = m.id
|
||||||
WHERE (a.assigned_user_name LIKE '%업무용%' OR a.current_user_id IS NULL)";
|
WHERE (a.assigned_user_name LIKE '%업무용%' OR a.current_user_id IS NULL) AND a.status != 'disposed'";
|
||||||
echo json_encode($db->query($query)->fetchAll());
|
echo json_encode($db->query($query)->fetchAll());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
||||||
BIN
dda/assets.db
BIN
dda/assets.db
Binary file not shown.
121
index.php
121
index.php
|
|
@ -38,7 +38,18 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
<!-- Stats Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-8">
|
<?php
|
||||||
|
$visible_count = 2; // 노트북, 조직현황은 상시 노출
|
||||||
|
if (($settings['menu_cards_visible'] ?? '1') === '1') $visible_count++;
|
||||||
|
if (($settings['menu_mfp_visible'] ?? '1') === '1') $visible_count++;
|
||||||
|
|
||||||
|
$grid_cols_class = match ($visible_count) {
|
||||||
|
2 => 'xl:grid-cols-2 max-w-[800px]',
|
||||||
|
3 => 'xl:grid-cols-3 max-w-[1200px]',
|
||||||
|
default => 'xl:grid-cols-4'
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 transition-all duration-500 mx-auto <?php echo $grid_cols_class; ?>">
|
||||||
<div
|
<div
|
||||||
class="bg-white p-7 rounded-3xl shadow-sm border border-slate-100 hover:shadow-xl hover:-translate-y-1 transition-all">
|
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="flex justify-between mb-6">
|
||||||
|
|
@ -124,7 +135,7 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M13 10V3L4 14h7v7l9-11h-7z" />
|
d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
</svg>퀵 네비게이션</h3>
|
</svg>퀵 네비게이션</h3>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-<?php echo $visible_count; ?> gap-6">
|
||||||
<a href="users.php"
|
<a href="users.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">
|
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-blue-50 transition-colors"><svg
|
<div class="bg-slate-50 p-3 rounded-xl group-hover:bg-blue-50 transition-colors"><svg
|
||||||
|
|
@ -184,6 +195,112 @@
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- 자산 교체/매각 대상 리스트 -->
|
||||||
|
<section class="mt-16 mb-20" x-show="stats.replacements?.targets.length > 0 || stats.replacements?.upcoming.length > 0">
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-2xl font-black text-slate-900 flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 bg-rose-100 rounded-2xl flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-rose-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
자산 교체 및 매각 관리
|
||||||
|
</h3>
|
||||||
|
<p class="text-slate-500 mt-2 text-sm font-medium">취득 후 4년이 경과하거나 도래한 자산 리스트입니다. (정책: 4년 단위 교체)</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span class="px-4 py-2 bg-rose-50 text-rose-600 rounded-xl text-xs font-black border border-rose-100 uppercase tracking-tighter" x-text="'Current: ' + stats.replacements?.current_year"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<!-- 01. 매각 완료 대상 (4년 이상) -->
|
||||||
|
<div class="bg-white rounded-[2.5rem] border border-slate-200 shadow-sm overflow-hidden flex flex-col">
|
||||||
|
<div class="p-6 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-8 h-8 bg-rose-600 text-white rounded-xl flex items-center justify-center text-xs font-black shadow-lg shadow-rose-200">01</span>
|
||||||
|
<h4 class="font-black text-slate-800">매각 대상 자산 <span class="text-rose-600 ml-1" x-text="'(' + stats.replacements?.targets.length + ')'"></span></h4>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-black text-rose-500 bg-rose-50 px-2.5 py-1 rounded-lg border border-rose-100 uppercase">Immediate Action</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto max-h-[500px] no-scrollbar">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead class="sticky top-0 bg-white/95 backdrop-blur-md z-10">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Asset TAG</th>
|
||||||
|
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Model & Spec</th>
|
||||||
|
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">Purchase Year</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50">
|
||||||
|
<template x-for="item in stats.replacements?.targets" :key="item.id">
|
||||||
|
<tr class="hover:bg-rose-50/30 transition-colors group">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span class="text-sm font-black text-slate-900 group-hover:text-rose-600 transition-colors" x-text="item.asset_tag"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="text-xs font-bold text-slate-700" x-text="item.model_name"></div>
|
||||||
|
<div class="text-[10px] text-slate-400 font-bold" x-text="item.manufacturer"></div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-right">
|
||||||
|
<div class="inline-flex flex-col items-end">
|
||||||
|
<span class="text-xs font-black text-slate-900" x-text="item.purchase_date.substring(0,4) + '년'"></span>
|
||||||
|
<span class="text-[9px] font-black text-rose-500 bg-rose-50 px-1.5 py-0.5 rounded mt-1" x-text="(stats.replacements.current_year - parseInt(item.purchase_date.substring(0,4))) + '년 경과'"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div x-show="stats.replacements?.targets.length === 0" class="py-20 text-center text-slate-300 italic text-sm font-medium">관리 대상 자산이 없습니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 02. 올해 매각 예정 (4년차 도래) -->
|
||||||
|
<div class="bg-white rounded-[2.5rem] border border-slate-200 shadow-sm overflow-hidden flex flex-col">
|
||||||
|
<div class="p-6 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="w-8 h-8 bg-amber-500 text-white rounded-xl flex items-center justify-center text-xs font-black shadow-lg shadow-amber-200">02</span>
|
||||||
|
<h4 class="font-black text-slate-800">교체/매각 준비 <span class="text-amber-600 ml-1" x-text="'(' + stats.replacements?.upcoming.length + ')'"></span></h4>
|
||||||
|
</div>
|
||||||
|
<span class="text-[10px] font-black text-amber-500 bg-amber-50 px-2.5 py-1 rounded-lg border border-amber-100 uppercase" x-text="stats.replacements?.current_year + ' Replacement'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto max-h-[500px] no-scrollbar">
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead class="sticky top-0 bg-white/95 backdrop-blur-md z-10">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Asset TAG</th>
|
||||||
|
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Model & Spec</th>
|
||||||
|
<th class="px-6 py-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">Target Year</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-50">
|
||||||
|
<template x-for="item in stats.replacements?.upcoming" :key="item.id">
|
||||||
|
<tr class="hover:bg-amber-50/30 transition-colors group">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span class="text-sm font-black text-slate-900 group-hover:text-amber-600 transition-colors" x-text="item.asset_tag"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="text-xs font-bold text-slate-700" x-text="item.model_name"></div>
|
||||||
|
<div class="text-[10px] text-slate-400 font-bold" x-text="item.manufacturer"></div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-right">
|
||||||
|
<div class="inline-flex flex-col items-end">
|
||||||
|
<span class="text-xs font-black text-slate-900" x-text="(parseInt(item.purchase_date.substring(0,4)) + 4) + '년 매각 대상'"></span>
|
||||||
|
<span class="text-[9px] font-black text-blue-500 bg-blue-50 px-1.5 py-0.5 rounded mt-1" x-text="'취득: ' + item.purchase_date.substring(0,4) + '년'"></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div x-show="stats.replacements?.upcoming.length === 0" class="py-20 text-center text-slate-300 italic text-sm font-medium">관리 대상 자산이 없습니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
30
models.php
30
models.php
|
|
@ -324,6 +324,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 설정 적용 버튼 추가 -->
|
||||||
|
<div class="mt-8 flex justify-end">
|
||||||
|
<button @click="saveSettings()"
|
||||||
|
class="px-8 py-4 bg-slate-900 text-white rounded-2xl font-black shadow-xl hover:bg-slate-800 transition-all active:scale-95 flex items-center gap-2 group">
|
||||||
|
<svg class="w-5 h-5 text-blue-400 group-hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
메뉴 설정 적용하기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-12 p-6 bg-blue-50 rounded-3xl border border-blue-100 flex items-start gap-4">
|
<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">
|
<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">
|
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -828,17 +839,26 @@
|
||||||
fetch('api.php?action=get_settings')
|
fetch('api.php?action=get_settings')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) this.settings = data.settings;
|
if (data.success) {
|
||||||
|
// DB에 설정이 없는 경우를 대비해 초기 기본값과 병합
|
||||||
|
this.settings = { ...this.settings, ...data.settings };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggleMenu(key) {
|
toggleMenu(key) {
|
||||||
const finalValue = this.settings[key] === '1' ? '0' : '1';
|
this.settings[key] = this.settings[key] === '1' ? '0' : '1';
|
||||||
this.settings[key] = finalValue;
|
},
|
||||||
|
saveSettings() {
|
||||||
fetch('api.php?action=update_settings', {
|
fetch('api.php?action=update_settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ settings: { [key]: finalValue } })
|
body: JSON.stringify({ settings: this.settings })
|
||||||
|
}).then(res => res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.showAlert('메뉴 설정이 저장되었습니다. 시스템에 즉시 반영됩니다.', '설정 완료', 'success');
|
||||||
|
// 페이지 새로고침하여 nav.php의 반영사항 확인
|
||||||
|
setTimeout(() => location.reload(), 1500);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
nav.php
21
nav.php
|
|
@ -5,6 +5,18 @@
|
||||||
*/
|
*/
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
$current_page = basename($_SERVER['PHP_SELF']);
|
$current_page = basename($_SERVER['PHP_SELF']);
|
||||||
|
|
||||||
|
// 시스템 설정 로드 (최상단으로 이동하여 하위 include 페이지에서 즉시 사용 가능하도록 함)
|
||||||
|
$settings = [];
|
||||||
|
try {
|
||||||
|
$settings = $db->query("SELECT key_name, value FROM system_settings")->fetchAll(PDO::FETCH_KEY_PAIR);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// 테이블이 없거나 에러 발생 시 기본값
|
||||||
|
$settings = [];
|
||||||
|
}
|
||||||
|
// 기본값 보정
|
||||||
|
if (!isset($settings['menu_cards_visible'])) $settings['menu_cards_visible'] = '1';
|
||||||
|
if (!isset($settings['menu_mfp_visible'])) $settings['menu_mfp_visible'] = '1';
|
||||||
?>
|
?>
|
||||||
<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">
|
<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">
|
||||||
<div class="max-w-[1600px] mx-auto h-20 flex items-center justify-between">
|
<div class="max-w-[1600px] mx-auto h-20 flex items-center justify-between">
|
||||||
|
|
@ -26,15 +38,6 @@ $current_page = basename($_SERVER['PHP_SELF']);
|
||||||
<!-- Horizontal Menu -->
|
<!-- 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">
|
<div class="flex-1 flex items-center justify-center space-x-1 lg:space-x-4 overflow-x-auto no-scrollbar py-2">
|
||||||
<?php
|
<?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 = [
|
$menu_items = [
|
||||||
['url' => 'index.php', 'name' => '대시보드', 'icon' => 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'],
|
['url' => 'index.php', 'name' => '대시보드', 'icon' => 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'],
|
||||||
['url' => 'rental.php', 'name' => '노트북 임대', 'icon' => 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4'],
|
['url' => 'rental.php', 'name' => '노트북 임대', 'icon' => 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4'],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue