초기 커밋 - EV AS 관리 시스템
This commit is contained in:
164
frontend/static/pages/admin/improvements.html
Normal file
164
frontend/static/pages/admin/improvements.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<!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"></head>
|
||||
<body>
|
||||
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
|
||||
<div class="layout">
|
||||
<div class="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" class="active">🔧 개선항목</a>
|
||||
<a href="/pages/admin/chargers.html">⚡ 충전기 관리</a>
|
||||
<a href="/pages/admin/charger-types.html">🏷 충전기 종류</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.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">
|
||||
<button class="btn btn-success btn-sm" onclick="API.download('/export/improvements','개선항목목록.xlsx')">📥 엑셀</button>
|
||||
<button class="btn btn-primary" onclick="openModal()">+ 개선항목 등록</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div style="display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap;">
|
||||
<select id="fStatus" style="width:auto">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="registered">등록</option><option value="reviewing">검토중</option>
|
||||
<option value="developing">개발중</option><option value="deployed">배포완료</option>
|
||||
<option value="done">완료</option>
|
||||
</select>
|
||||
<select id="fMfr" style="width:auto"><option value="">전체 제조사</option></select>
|
||||
<button class="btn btn-outline btn-sm" onclick="load()">🔍 검색</button>
|
||||
</div>
|
||||
<div class="tbl-wrap"><table>
|
||||
<thead><tr><th>#</th><th>제목</th><th>분류</th><th>우선순위</th><th>담당제조사</th><th>연결AS</th><th>상태</th><th>등록일</th><th>SW배포일</th></tr></thead>
|
||||
<tbody id="tbody"></tbody>
|
||||
</table></div>
|
||||
<div id="empty" class="alert alert-info" style="display:none">등록된 개선항목이 없습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 등록 모달 -->
|
||||
<div class="modal-bg hidden" id="modal">
|
||||
<div class="modal" style="max-width:680px">
|
||||
<div class="modal-title">개선항목 등록</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>분류 <span class="req">*</span></label>
|
||||
<select id="mCat">
|
||||
<option value="sw">SW 개선</option><option value="hw">HW 개선</option>
|
||||
<option value="ui">UI 개선</option><option value="firmware">펌웨어</option>
|
||||
<option value="other">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>우선순위</label>
|
||||
<select id="mPri">
|
||||
<option value="urgent">🔴 긴급</option><option value="high">🟠 높음</option>
|
||||
<option value="normal" selected>🟡 보통</option><option value="low">🟢 낮음</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label>제목 <span class="req">*</span></label><input type="text" id="mTitle" placeholder="개선 항목 제목"></div>
|
||||
<div class="form-group"><label>개선 내용 <span class="req">*</span></label><textarea id="mDesc" rows="4" placeholder="문제점 및 개선 요구 사항을 상세히 작성하세요."></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>관련 부품명</label><input type="text" id="mPart" placeholder="예: 전력변환모듈"></div>
|
||||
<div class="form-group"><label>담당 제조사 <span class="req">*</span></label><select id="mMfr"></select></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>관련 AS 신고 연결 (복수 선택)</label>
|
||||
<input type="text" id="mReportSearch" placeholder="신고번호 또는 충전기 ID로 검색" oninput="searchReports()" style="margin-bottom:8px">
|
||||
<div id="mReportList" style="max-height:150px;overflow-y:auto;border:1px solid var(--gray3);border-radius:6px;padding:8px;font-size:12px"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>보고서 첨부</label><input type="file" id="mFiles" multiple></div>
|
||||
<div class="form-group"><label>SW 배포 목표일</label><input type="date" id="mTarget"></div>
|
||||
</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="saveImprovement()">등록</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'));
|
||||
const CAT = {sw:'SW',hw:'HW',ui:'UI',firmware:'펌웨어',other:'기타'};
|
||||
const PRI = {urgent:'🔴 긴급',high:'🟠 높음',normal:'🟡 보통',low:'🟢 낮음'};
|
||||
const selectedReports = new Set();
|
||||
let allReports = [];
|
||||
|
||||
async function load() {
|
||||
const [mfrs, imps] = await Promise.all([
|
||||
API.get('/accounts?role=manufacturer'),
|
||||
API.get('/improvements?status='+document.getElementById('fStatus').value+'&manufacturer_id='+document.getElementById('fMfr').value)
|
||||
]);
|
||||
// 제조사 필터 드롭다운
|
||||
const mfrSel = document.getElementById('fMfr');
|
||||
if (mfrSel.options.length <= 1)
|
||||
mfrs.forEach(m => { const o=document.createElement('option'); o.value=m.id; o.textContent=`${m.company||''} / ${m.name}`; mfrSel.appendChild(o); });
|
||||
|
||||
document.getElementById('empty').style.display = imps.length ? 'none' : 'block';
|
||||
document.getElementById('tbody').innerHTML = imps.map(i => `
|
||||
<tr onclick="location.href='/pages/admin/improvement-detail.html?id=${i.id}'">
|
||||
<td>#${i.id}</td>
|
||||
<td style="max-width:200px"><strong>${i.title}</strong></td>
|
||||
<td>${CAT[i.category]||i.category}</td>
|
||||
<td>${PRI[i.priority]||i.priority}</td>
|
||||
<td>${i.manufacturer_company||'-'}<br><small>${i.manufacturer_name||''}</small></td>
|
||||
<td><span class="badge s-pending">${i.report_count}건</span></td>
|
||||
<td>${Auth.statusBadge(i.status)}</td>
|
||||
<td>${Auth.fmtDt(i.created_at)}</td>
|
||||
<td>${i.sw_deployed_at||'-'}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
async function openModal() {
|
||||
document.getElementById('modal').classList.remove('hidden');
|
||||
const mfrs = await API.get('/accounts?role=manufacturer');
|
||||
document.getElementById('mMfr').innerHTML = '<option value="">제조사 선택</option>' +
|
||||
mfrs.map(m=>`<option value="${m.id}">${m.company||''} / ${m.name}</option>`).join('');
|
||||
allReports = await API.get('/reports');
|
||||
renderReportList('');
|
||||
}
|
||||
function closeModal() { document.getElementById('modal').classList.add('hidden'); selectedReports.clear(); document.getElementById('modalErr').style.display='none'; }
|
||||
|
||||
function searchReports() { renderReportList(document.getElementById('mReportSearch').value.toLowerCase()); }
|
||||
function renderReportList(q) {
|
||||
const filtered = allReports.filter(r => !q || String(r.id).includes(q) || (r.charger_id||'').toLowerCase().includes(q)).slice(0,20);
|
||||
document.getElementById('mReportList').innerHTML = filtered.map(r => `
|
||||
<label style="display:flex;gap:8px;align-items:center;padding:5px;cursor:pointer;${selectedReports.has(r.id)?'background:#E3EDFF;border-radius:4px':''}">
|
||||
<input type="checkbox" ${selectedReports.has(r.id)?'checked':''} value="${r.id}" style="accent-color:var(--accent);flex-shrink:0"
|
||||
onchange="${selectedReports.has(r.id)?'selectedReports.delete':'selectedReports.add'}(${r.id}); renderReportList('${q}')">
|
||||
<span><strong>#${r.id}</strong> ${r.charger_id||''} — ${(r.issue_types||[]).join(', ')}</span>
|
||||
</label>`).join('') || '<div style="color:var(--gray4)">검색 결과 없음</div>';
|
||||
}
|
||||
|
||||
async function saveImprovement() {
|
||||
const title = document.getElementById('mTitle').value.trim();
|
||||
const desc = document.getElementById('mDesc').value.trim();
|
||||
const mfr = document.getElementById('mMfr').value;
|
||||
if (!title) { showErr('제목을 입력하세요.'); return; }
|
||||
if (!desc) { showErr('내용을 입력하세요.'); return; }
|
||||
if (!mfr) { showErr('담당 제조사를 선택하세요.'); return; }
|
||||
const fd = new FormData();
|
||||
fd.append('title',title); fd.append('category',document.getElementById('mCat').value);
|
||||
fd.append('description',desc); fd.append('priority',document.getElementById('mPri').value);
|
||||
fd.append('part_name',document.getElementById('mPart').value);
|
||||
fd.append('manufacturer_id',mfr);
|
||||
fd.append('report_ids', JSON.stringify([...selectedReports]));
|
||||
fd.append('sw_deploy_target',document.getElementById('mTarget').value);
|
||||
Array.from(document.getElementById('mFiles').files).forEach(f => fd.append('attachments',f));
|
||||
try { await API.post('/improvements',fd); closeModal(); load(); }
|
||||
catch(e) { showErr(e.message); }
|
||||
}
|
||||
function showErr(m) { const el=document.getElementById('modalErr'); el.textContent=m; el.style.display='block'; }
|
||||
load();
|
||||
</script></body></html>
|
||||
Reference in New Issue
Block a user