기능 추가 — 옵저버 계정 및 현황 조회 포털
읽기 전용 옵저버 역할 추가. 신고 현황 확인만 가능하며 모든 쓰기 동작 차단. - auth.py: require_viewer(admin+observer) 의존성 추가 - auth_router.py: register 엔드포인트에 role 파라미터 추가 (mechanic/observer) - login.html: 회원가입 시 정비사/옵저버 역할 카드 선택 UI, 역할별 안내문구 - 로그인 후 observer → /pages/observer/dashboard.html 라우팅 - observer/dashboard.html: 통계 카드(상태별 건수) + 신고 현황 테이블(읽기전용) - observer/reports.html: 상태·충전기ID·충전소명 필터 신고 목록 - accounts.html: 옵저버 필터·생성·승인 대기 역할 표시 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
124
frontend/static/pages/observer/dashboard.html
Normal file
124
frontend/static/pages/observer/dashboard.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html><html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>현황 조회</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<style>
|
||||
.ro-badge{display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:700;background:#EDE9FE;color:#5B21B6;margin-left:8px;vertical-align:middle;}
|
||||
.stat-link{text-decoration:none;display:block;}
|
||||
.stat-link:hover .stat{box-shadow:0 4px 16px rgba(0,0,0,.12);transform:translateY(-1px);transition:all .15s;}
|
||||
.filter-bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:14px;}
|
||||
.filter-bar select,.filter-bar input{padding:7px 10px;border:1px solid var(--gray3);border-radius:6px;font-size:13px;font-family:inherit;color:var(--text);}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리 <span class="ro-badge">👁 읽기전용</span></span>
|
||||
<div id="navUser"></div>
|
||||
</nav>
|
||||
|
||||
<div class="layout">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-section">메뉴</div>
|
||||
<a href="/pages/observer/dashboard.html" class="active">📊 현황 대시보드</a>
|
||||
<a href="/pages/observer/reports.html">📋 신고 목록</a>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div class="stats" id="statsRow" style="margin-bottom:24px">
|
||||
<div class="stat"><div class="stat-num" id="sTotal">-</div><div class="stat-label">전체 신고</div></div>
|
||||
<div class="stat warn"><div class="stat-num" id="sPendingApproval">-</div><div class="stat-label">승인대기</div></div>
|
||||
<div class="stat warn"><div class="stat-num" id="sPending">-</div><div class="stat-label">접수</div></div>
|
||||
<div class="stat warn"><div class="stat-num" id="sInProgress">-</div><div class="stat-label">처리중</div></div>
|
||||
<div class="stat good"><div class="stat-num" id="sDone">-</div><div class="stat-label">완료</div></div>
|
||||
<div class="stat"><div class="stat-num" id="sClosed">-</div><div class="stat-label">상황종료</div></div>
|
||||
</div>
|
||||
|
||||
<!-- 신고 목록 -->
|
||||
<div class="card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;flex-wrap:wrap;gap:8px;">
|
||||
<div class="card-title" style="margin:0">📋 신고 현황</div>
|
||||
<div class="filter-bar" style="margin:0">
|
||||
<select id="fStatus" onchange="load()">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="pending_approval">승인대기</option>
|
||||
<option value="pending">접수</option>
|
||||
<option value="in_progress">처리중</option>
|
||||
<option value="waiting">부품대기</option>
|
||||
<option value="revisit">재방문</option>
|
||||
<option value="done">완료</option>
|
||||
<option value="closed">상황종료</option>
|
||||
</select>
|
||||
<input type="text" id="fCharger" placeholder="충전기ID 검색" style="width:130px" oninput="load()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>접수번호</th><th>충전기ID</th><th>충전소명</th><th>문제유형</th>
|
||||
<th>에러코드</th><th>신고일시</th><th>정비사</th><th>상태</th>
|
||||
</tr></thead>
|
||||
<tbody id="tbody"></tbody>
|
||||
</table>
|
||||
<div id="empty" style="display:none;text-align:center;padding:40px;color:var(--gray4);font-size:13px">신고 내역이 없습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/auth.js"></script>
|
||||
<script>
|
||||
Auth.require(['observer', 'admin']);
|
||||
Auth.renderNav(document.getElementById('navUser'));
|
||||
|
||||
let _allReports = [];
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const s = await API.get('/stats');
|
||||
document.getElementById('sTotal').textContent = s.total ?? '-';
|
||||
document.getElementById('sPendingApproval').textContent = s.pending_approval ?? 0;
|
||||
document.getElementById('sPending').textContent = s.pending ?? 0;
|
||||
document.getElementById('sInProgress').textContent = s.in_progress ?? 0;
|
||||
document.getElementById('sDone').textContent = s.done ?? 0;
|
||||
document.getElementById('sClosed').textContent = s.closed ?? 0;
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const status = document.getElementById('fStatus').value;
|
||||
const chargerId = document.getElementById('fCharger').value.trim();
|
||||
try {
|
||||
let url = '/reports';
|
||||
if (status) url += '?status=' + status;
|
||||
_allReports = await API.get(url);
|
||||
} catch(e) { _allReports = []; }
|
||||
render(chargerId);
|
||||
}
|
||||
|
||||
function render(chargerId) {
|
||||
let rows = _allReports;
|
||||
if (chargerId) rows = rows.filter(r => r.charger_id?.includes(chargerId));
|
||||
const tbody = document.getElementById('tbody');
|
||||
const empty = document.getElementById('empty');
|
||||
if (!rows.length) { tbody.innerHTML = ''; empty.style.display = 'block'; return; }
|
||||
empty.style.display = 'none';
|
||||
tbody.innerHTML = rows.map(r => `
|
||||
<tr style="cursor:default">
|
||||
<td>#${r.id}</td>
|
||||
<td><strong>${r.charger_id}</strong></td>
|
||||
<td>${r.station_name || '-'}</td>
|
||||
<td>${(r.issue_types || []).join(', ') || '-'}</td>
|
||||
<td>${r.error_code || '-'}</td>
|
||||
<td>${Auth.fmtDt(r.reported_at)}</td>
|
||||
<td>${r.mechanic_name || '-'}</td>
|
||||
<td>${Auth.statusBadge(r.status)}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
loadStats();
|
||||
load();
|
||||
</script>
|
||||
</body></html>
|
||||
Reference in New Issue
Block a user