242 lines
13 KiB
HTML
242 lines
13 KiB
HTML
<!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>
|
|
.cb-cell { width:36px; text-align:center; padding:8px 4px; }
|
|
input[type=checkbox] { width:16px; height:16px; cursor:pointer; }
|
|
tr.selected td { background:var(--gray2) !important; }
|
|
#btnDelete { display:none; }
|
|
</style></head>
|
|
<body>
|
|
<nav class="nav"><div style="display:flex;align-items:center;gap:2px;"><button class="nav-hamburger" onclick="toggleSidebar()">☰</button><span class="nav-brand">⚡ EV AS 관리 — 관리자</span></div><div id="navUser"></div></nav>
|
|
<div class="mobile-nav-overlay" id="navOverlay" onclick="toggleSidebar()"></div>
|
|
<div class="layout">
|
|
<div class="sidebar" id="sidebar">
|
|
<div class="sidebar-section">AS 관리</div>
|
|
<a href="/pages/admin/dashboard.html">📊 대시보드</a>
|
|
<a href="/pages/admin/reports.html">📋 신고 목록</a>
|
|
<a href="/pages/admin/costs.html">💰 출장비 관리</a>
|
|
<div class="sidebar-section">시스템</div>
|
|
<a href="/pages/admin/improvements.html">🔧 개선항목</a>
|
|
<a href="/pages/admin/chargers.html">⚡ 충전기 관리</a>
|
|
<a href="/pages/admin/charger-types.html">🏷 충전기 종류</a>
|
|
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
|
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
|
<a href="/pages/admin/accounts.html" class="active">👥 계정 관리</a>
|
|
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
|
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
|
</div>
|
|
<div class="main">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:18px;flex-wrap:wrap;gap:10px;">
|
|
<h2 style="font-size:18px;font-weight:700;color:var(--navy)">계정 관리</h2>
|
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
|
<button id="btnDelete" class="btn btn-sm" style="background:#e53e3e;color:#fff;border:none;" onclick="bulkDelete()">🗑 선택 비활성화 (<span id="selCount">0</span>개)</button>
|
|
<button class="btn btn-primary" onclick="openModal()">+ 계정 생성</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 승인 대기 섹션 -->
|
|
<div id="pendingSection" style="display:none;margin-bottom:20px;">
|
|
<div class="card" style="border:2px solid #F59E0B;background:#FFFBEB;">
|
|
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">
|
|
<span style="font-size:18px">⏳</span>
|
|
<div>
|
|
<div style="font-size:15px;font-weight:700;color:#92400E">가입 승인 대기</div>
|
|
<div style="font-size:12px;color:#B45309">승인 후 정비사 계정으로 이용 가능합니다.</div>
|
|
</div>
|
|
<span id="pendingBadge" style="margin-left:auto;background:#F59E0B;color:white;font-size:12px;font-weight:700;padding:3px 10px;border-radius:10px;"></span>
|
|
</div>
|
|
<div class="tbl-wrap">
|
|
<table>
|
|
<thead><tr><th>이름</th><th>아이디</th><th>회사명</th><th>전화번호</th><th>신청일시</th><th style="width:150px">처리</th></tr></thead>
|
|
<tbody id="pendingTbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div style="display:flex;gap:10px;margin-bottom:14px;align-items:center;flex-wrap:wrap;">
|
|
<select id="fRole" onchange="load()" style="width:auto">
|
|
<option value="">전체</option><option value="mechanic">정비사</option>
|
|
<option value="observer">옵저버</option>
|
|
<option value="manufacturer">제조사</option><option value="admin">관리자</option>
|
|
</select>
|
|
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
|
<input type="checkbox" id="chkInactive" onchange="load()" style="width:14px;height:14px;">
|
|
비활성 계정 포함
|
|
</label>
|
|
</div>
|
|
<div class="tbl-wrap"><table>
|
|
<thead><tr>
|
|
<th class="cb-cell"><input type="checkbox" id="chkAll" onchange="toggleAll(this)"></th>
|
|
<th>ID</th><th>아이디</th><th>역할</th><th>이름</th><th>회사/제조사</th><th>전화번호</th><th>상태</th><th>수정</th>
|
|
</tr></thead>
|
|
<tbody id="tbody"></tbody>
|
|
</table></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-bg hidden" id="modal">
|
|
<div class="modal" style="max-width:480px">
|
|
<div class="modal-title" id="modalTitle">계정 생성</div>
|
|
<input type="hidden" id="eId">
|
|
<div class="form-row">
|
|
<div class="form-group"><label>역할 <span class="req">*</span></label>
|
|
<select id="eRole" onchange="toggleFields()">
|
|
<option value="mechanic">정비사</option>
|
|
<option value="observer">옵저버</option>
|
|
<option value="manufacturer">제조사</option>
|
|
<option value="admin">관리자</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group"><label>로그인 ID <span class="req">*</span></label><input type="text" id="eUsername"></div>
|
|
</div>
|
|
<div class="form-group"><label>비밀번호 <span class="req" id="pwReq">*</span></label><input type="password" id="ePassword" placeholder="수정 시 변경할 경우에만 입력"></div>
|
|
<div class="form-row">
|
|
<div class="form-group"><label>이름 <span class="req">*</span></label><input type="text" id="eName"></div>
|
|
<div class="form-group" id="companyField"><label>회사명</label><input type="text" id="eCompany"></div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group"><label>전화번호</label><input type="tel" id="ePhone"></div>
|
|
<div class="form-group" id="emailField"><label>이메일</label><input type="email" id="eEmail"></div>
|
|
</div>
|
|
<div class="form-group"><label>계정 활성화</label>
|
|
<select id="eActive"><option value="true">활성</option><option value="false">비활성</option></select>
|
|
</div>
|
|
<div id="modalErr" class="alert alert-danger" style="display:none"></div>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-outline" onclick="closeModal()">취소</button>
|
|
<button class="btn btn-primary" onclick="save()">저장</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="/js/api.js"></script><script src="/js/auth.js"></script>
|
|
<script>
|
|
Auth.require(['admin']); Auth.renderNav(document.getElementById('navUser'));
|
|
|
|
function updateDeleteBtn() {
|
|
const checked = document.querySelectorAll('.row-chk:checked');
|
|
document.getElementById('selCount').textContent = checked.length;
|
|
document.getElementById('btnDelete').style.display = checked.length > 0 ? 'inline-flex' : 'none';
|
|
}
|
|
function toggleAll(chkAll) {
|
|
document.querySelectorAll('.row-chk').forEach(c => {
|
|
c.checked = chkAll.checked;
|
|
c.closest('tr').classList.toggle('selected', chkAll.checked);
|
|
});
|
|
updateDeleteBtn();
|
|
}
|
|
async function bulkDelete() {
|
|
const checked = [...document.querySelectorAll('.row-chk:checked')];
|
|
if (!checked.length) return;
|
|
if (!confirm(`선택한 계정 ${checked.length}개를 비활성화합니다. 계속하시겠습니까?`)) return;
|
|
const ids = checked.map(c => parseInt(c.dataset.id));
|
|
try { await API.delete('/accounts/bulk', ids); load(); }
|
|
catch(e) { alert('처리 중 오류가 발생했습니다: ' + e.message); }
|
|
}
|
|
|
|
const ROLE_LABEL = {admin:'관리자',mechanic:'정비사',manufacturer:'제조사',observer:'옵저버'};
|
|
|
|
async function loadPending() {
|
|
const all = await API.get('/accounts');
|
|
const pending = all.filter(u => u.is_pending);
|
|
const sec = document.getElementById('pendingSection');
|
|
sec.style.display = pending.length ? 'block' : 'none';
|
|
if (!pending.length) return;
|
|
document.getElementById('pendingBadge').textContent = pending.length + '명 대기 중';
|
|
document.getElementById('pendingTbody').innerHTML = pending.map(u => `
|
|
<tr>
|
|
<td><strong>${u.name}</strong> <span style="font-size:11px;background:#F3F4F6;color:#374151;padding:1px 7px;border-radius:8px;font-weight:600;">${ROLE_LABEL[u.role]||u.role}</span></td>
|
|
<td style="color:var(--gray4)">${u.username}</td>
|
|
<td>${u.company ? `<span style="background:#EFF6FF;color:#1E40AF;font-size:11px;font-weight:600;padding:2px 8px;border-radius:8px">${u.company}</span>` : '<span style="color:var(--gray4)">-</span>'}</td>
|
|
<td>${u.phone||'-'}</td>
|
|
<td style="font-size:12px">${Auth.fmtDt(u.created_at)}</td>
|
|
<td>
|
|
<div style="display:flex;gap:6px">
|
|
<button class="btn btn-success btn-sm" onclick="approveUser(${u.id},'${u.name}')">✅ 승인</button>
|
|
<button class="btn btn-sm" style="background:#fee2e2;color:#991b1b;border:none" onclick="rejectUser(${u.id},'${u.name}')">✕ 거절</button>
|
|
</div>
|
|
</td>
|
|
</tr>`).join('');
|
|
}
|
|
|
|
async function approveUser(id, name) {
|
|
if (!confirm(`"${name}" 계정을 승인하시겠습니까?\n승인 후 바로 로그인 가능합니다.`)) return;
|
|
try {
|
|
await API.patch('/accounts/'+id+'/approve');
|
|
loadPending(); load();
|
|
} catch(e) { alert('오류: '+e.message); }
|
|
}
|
|
|
|
async function rejectUser(id, name) {
|
|
if (!confirm(`"${name}" 가입 신청을 거절하고 계정을 삭제하시겠습니까?`)) return;
|
|
try {
|
|
await API.delete('/accounts/'+id);
|
|
loadPending(); load();
|
|
} catch(e) { alert('오류: '+e.message); }
|
|
}
|
|
|
|
async function load() {
|
|
const role = document.getElementById('fRole').value;
|
|
const showInactive = document.getElementById('chkInactive').checked;
|
|
const users = await API.get('/accounts'+(role?'?role='+role:''));
|
|
document.getElementById('chkAll').checked = false;
|
|
updateDeleteBtn();
|
|
const filtered = users.filter(u => !u.is_pending && (showInactive || u.is_active));
|
|
document.getElementById('tbody').innerHTML = filtered.map(u=>`
|
|
<tr>
|
|
<td class="cb-cell" onclick="event.stopPropagation()">
|
|
<input type="checkbox" class="row-chk" data-id="${u.id}"
|
|
onchange="this.closest('tr').classList.toggle('selected',this.checked);updateDeleteBtn()">
|
|
</td>
|
|
<td>${u.id}</td><td>${u.username}</td><td>${ROLE_LABEL[u.role]||u.role}</td>
|
|
<td>${u.name}</td><td>${u.company||'-'}</td><td>${u.phone||'-'}</td>
|
|
<td><span class="badge ${u.is_active?'s-done':'s-waiting'}">${u.is_active?'활성':'비활성'}</span></td>
|
|
<td><button class="btn btn-outline btn-sm" onclick="editUser(${u.id})">수정</button>
|
|
${u.is_active ? `<button class="btn btn-danger btn-sm" onclick="delUser(${u.id})">삭제</button>` : ''}</td>
|
|
</tr>`).join('');
|
|
}
|
|
function openModal() { document.getElementById('modal').classList.remove('hidden'); document.getElementById('eId').value=''; document.getElementById('eUsername').disabled=false; document.getElementById('pwReq').style.display='inline'; }
|
|
function closeModal() { document.getElementById('modal').classList.add('hidden'); document.getElementById('modalErr').style.display='none'; ['eUsername','ePassword','eName','eCompany','ePhone','eEmail'].forEach(id=>document.getElementById(id).value=''); }
|
|
function toggleFields() {}
|
|
async function editUser(id) {
|
|
const users = await API.get('/accounts');
|
|
const u = users.find(x=>x.id===id);
|
|
openModal();
|
|
document.getElementById('eId').value=id;
|
|
document.getElementById('eRole').value=u.role;
|
|
document.getElementById('eUsername').value=u.username; document.getElementById('eUsername').disabled=true;
|
|
document.getElementById('eName').value=u.name;
|
|
document.getElementById('eCompany').value=u.company||'';
|
|
document.getElementById('ePhone').value=u.phone||'';
|
|
document.getElementById('eEmail').value=u.email||'';
|
|
document.getElementById('eActive').value=String(u.is_active);
|
|
document.getElementById('pwReq').style.display='none';
|
|
document.getElementById('modalTitle').textContent='계정 수정';
|
|
}
|
|
async function save() {
|
|
const id = document.getElementById('eId').value;
|
|
const fd = new FormData();
|
|
if (!id) { fd.append('username',document.getElementById('eUsername').value.trim()); fd.append('role',document.getElementById('eRole').value); }
|
|
const pw = document.getElementById('ePassword').value;
|
|
if (pw) fd.append('password', pw);
|
|
else if (!id) { alert('비밀번호를 입력하세요.'); return; }
|
|
fd.append('name',document.getElementById('eName').value.trim());
|
|
fd.append('company',document.getElementById('eCompany').value);
|
|
fd.append('phone',document.getElementById('ePhone').value);
|
|
fd.append('email',document.getElementById('eEmail').value);
|
|
fd.append('is_active',document.getElementById('eActive').value);
|
|
try {
|
|
if (id) await API.put('/accounts/'+id, fd);
|
|
else await API.post('/accounts', fd);
|
|
closeModal(); load();
|
|
} catch(e) { const el=document.getElementById('modalErr'); el.textContent=e.message; el.style.display='block'; }
|
|
}
|
|
async function delUser(id) {
|
|
if (!confirm('계정을 삭제하시겠습니까?\n(처리 이력이 있는 계정은 비활성 처리됩니다.)')) return;
|
|
try { await API.delete('/accounts/'+id); load(); }
|
|
catch(e) { alert('오류: ' + e.message); }
|
|
}
|
|
loadPending();
|
|
load();
|
|
</script></body></html>
|