1차완료
This commit is contained in:
@@ -6,9 +6,10 @@
|
||||
#btnDelete { display:none; }
|
||||
</style></head>
|
||||
<body>
|
||||
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
|
||||
<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">
|
||||
<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>
|
||||
@@ -20,6 +21,7 @@
|
||||
<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">
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리 — 관리자</span>
|
||||
<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">
|
||||
<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>
|
||||
@@ -31,6 +32,7 @@
|
||||
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
|
||||
@@ -37,9 +37,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
|
||||
<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">
|
||||
<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>
|
||||
@@ -51,6 +52,7 @@
|
||||
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
|
||||
<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">
|
||||
<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>
|
||||
@@ -26,6 +27,7 @@
|
||||
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
@@ -49,7 +51,7 @@
|
||||
<select id="fParty" style="width:auto">
|
||||
<option value="">전체 부담주체</option>
|
||||
<option value="cpo">CPO</option>
|
||||
<option value="manufacturer">제조사</option>
|
||||
<option value="manufacturer">업체</option>
|
||||
<option value="self">자체</option>
|
||||
<option value="user">사용자과실</option>
|
||||
<option value="other">기타</option>
|
||||
@@ -60,7 +62,7 @@
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th class="cb-cell"><input type="checkbox" id="chkAll" onchange="toggleAll(this)"></th>
|
||||
<th>신고#</th><th>충전기</th><th>충전소</th><th>정비사</th><th>부담주체</th><th>금액</th><th>상태</th><th>처리일시</th>
|
||||
<th>신고#</th><th>충전기</th><th>충전소</th><th>정비사</th><th>부담주체</th><th>수급주체</th><th>금액</th><th>상태</th><th>처리일시</th>
|
||||
</tr></thead>
|
||||
<tbody id="tbody"></tbody>
|
||||
</table>
|
||||
@@ -94,7 +96,7 @@ async function bulkDelete() {
|
||||
catch(e) { alert('삭제 중 오류가 발생했습니다: ' + e.message); }
|
||||
}
|
||||
|
||||
const PARTY_LABEL = {cpo:'CPO',manufacturer:'제조사',self:'자체',user:'사용자과실',other:'기타'};
|
||||
const PARTY_LABEL = {cpo:'CPO',manufacturer:'업체',self:'자체',user:'사용자과실',other:'기타'};
|
||||
|
||||
async function load() {
|
||||
const [statsData, costs] = await Promise.all([API.get('/costs/stats'), API.get('/costs?cost_status='+document.getElementById('fStatus').value+'&cost_party_type='+document.getElementById('fParty').value)]);
|
||||
@@ -115,7 +117,8 @@ async function load() {
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${(c.report_ids||[])[0]||''}'" style="cursor:pointer">${c.charger_id||'-'}</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${(c.report_ids||[])[0]||''}'" style="cursor:pointer">${c.station_name||'-'}</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${(c.report_ids||[])[0]||''}'" style="cursor:pointer">${c.mechanic_name||'-'}<br><small>${c.mechanic_company||''}</small></td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${(c.report_ids||[])[0]||''}'" style="cursor:pointer">${PARTY_LABEL[c.cost_party_type]||c.cost_party_type||'-'}${c.cost_party_type==='manufacturer'?`<br><small>${c.manufacturer_name||''}</small>`:''}</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${(c.report_ids||[])[0]||''}'" style="cursor:pointer">${PARTY_LABEL[c.cost_party_type]||c.cost_party_type||'-'}${c.cost_party_type==='manufacturer'&&c.cost_manufacturer_name?`<br><small>${c.cost_manufacturer_name}</small>`:''}</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${(c.report_ids||[])[0]||''}'" style="cursor:pointer">${c.recv_party_type?(PARTY_LABEL[c.recv_party_type]||c.recv_party_type):'-'}${c.recv_party_type==='manufacturer'&&c.recv_manufacturer_name?`<br><small>${c.recv_manufacturer_name}</small>`:''}</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${(c.report_ids||[])[0]||''}'" style="cursor:pointer;font-weight:700;color:var(--orange)">${(c.cost_amount||0).toLocaleString()}원</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${(c.report_ids||[])[0]||''}'" style="cursor:pointer">${Auth.costStatusBadge(c.cost_status)}</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${(c.report_ids||[])[0]||''}'" style="cursor:pointer">${Auth.fmtDt(c.reviewed_at)}</td>
|
||||
|
||||
@@ -70,12 +70,21 @@
|
||||
.charger-option.selected { background: #EFF6FF; }
|
||||
.charger-option .opt-name { font-weight: 600; color: var(--navy); }
|
||||
.charger-option .opt-sub { font-size: 11px; color: var(--gray4); margin-top: 2px; }
|
||||
.charger-selected-badge {
|
||||
display: none; margin-top: 6px; padding: 7px 10px;
|
||||
background: #EFF6FF; border: 1px solid #BFDBFE;
|
||||
border-radius: 6px; font-size: 12px; color: var(--navy2);
|
||||
.selected-chargers-list {
|
||||
display: none; flex-wrap: wrap; gap: 5px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.charger-selected-badge.show { display: flex; justify-content: space-between; align-items: center; }
|
||||
.selected-chargers-list.show { display: flex; }
|
||||
.sel-tag {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
padding: 4px 10px; background: #EFF6FF; border: 1px solid #BFDBFE;
|
||||
border-radius: 20px; font-size: 12px; color: var(--navy2);
|
||||
}
|
||||
.sel-tag button {
|
||||
background: none; border: none; cursor: pointer; color: var(--gray4);
|
||||
font-size: 13px; line-height: 1; padding: 0 1px;
|
||||
}
|
||||
.sel-tag button:hover { color: var(--red); }
|
||||
|
||||
/* ── 증상 체크박스 ── */
|
||||
.issue-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-top: 4px; }
|
||||
@@ -139,15 +148,31 @@
|
||||
}
|
||||
.detail-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
.detail-tab:hover:not(.active) { color: var(--navy2); }
|
||||
|
||||
/* ── 대시보드 레이아웃 클래스 ── */
|
||||
.dash-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:10px; }
|
||||
.time-metrics-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:14px; }
|
||||
.dash-chart-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:16px; flex-wrap:wrap; gap:8px; }
|
||||
.dash-chart-wrap { position:relative; height:220px; }
|
||||
.dash-bottom-grid { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
|
||||
|
||||
@media(max-width:768px) {
|
||||
.time-metrics-grid { grid-template-columns:repeat(2,1fr); }
|
||||
.dash-chart-wrap { height:170px; }
|
||||
.dash-bottom-grid { grid-template-columns:1fr; }
|
||||
#adminMapWrap { height:260px !important; }
|
||||
.btn-report-new { font-size:12px; padding:7px 12px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리 — 관리자</span>
|
||||
<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">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">AS 관리</div>
|
||||
<a href="/pages/admin/dashboard.html" class="active">📊 대시보드</a>
|
||||
<a href="/pages/admin/reports.html">📋 신고 목록</a>
|
||||
@@ -159,10 +184,11 @@
|
||||
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
|
||||
<div class="dash-header">
|
||||
<h2 style="font-size:18px;font-weight:700;color:var(--navy);margin:0">대시보드</h2>
|
||||
<button class="btn-report-new" onclick="openReportModal()">+ 신고 접수</button>
|
||||
</div>
|
||||
@@ -171,7 +197,7 @@
|
||||
<!-- 처리 시간 지표 카드 -->
|
||||
<div class="card" id="timeMetrics" style="margin-bottom:20px">
|
||||
<div class="card-title">⏱ 처리 시간 지표</div>
|
||||
<div id="timeMetricsBody" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:14px"></div>
|
||||
<div id="timeMetricsBody" class="time-metrics-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- 드릴다운 뒤로가기 -->
|
||||
@@ -183,7 +209,7 @@
|
||||
|
||||
<!-- 월별 처리시간 차트 -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px">
|
||||
<div class="dash-chart-head">
|
||||
<div class="card-title" style="margin:0" id="monthlyChartTitle">📈 월별 평균 처리시간</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
<span id="monthlyChartMode" style="font-size:11px;color:var(--gray4)"></span>
|
||||
@@ -194,28 +220,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position:relative;height:220px">
|
||||
<div class="dash-chart-wrap">
|
||||
<canvas id="monthlyChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 월별 신고 접수 건수 차트 -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px">
|
||||
<div class="dash-chart-head">
|
||||
<div class="card-title" style="margin:0" id="monthlyReportChartTitle">📊 월별 신고 접수 건수</div>
|
||||
<div style="display:flex;gap:10px;font-size:11px;color:var(--gray4)">
|
||||
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#3B82F6;margin-right:3px"></span>처리 완료</span>
|
||||
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#CBD5E1;margin-right:3px"></span>미처리</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position:relative;height:220px">
|
||||
<div class="dash-chart-wrap">
|
||||
<canvas id="monthlyReportChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 충전기별 누적 고장 Top 10 -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px">
|
||||
<div class="dash-chart-head">
|
||||
<div class="card-title" style="margin:0">🏆 충전기별 누적 고장 건수 Top 10</div>
|
||||
<div style="display:flex;gap:10px;font-size:11px;color:var(--gray4)">
|
||||
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#3B82F6;margin-right:3px"></span>처리 완료</span>
|
||||
@@ -227,18 +253,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 충전소별 누적 고장 Top 10 -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div class="dash-chart-head">
|
||||
<div class="card-title" style="margin:0">🏢 충전소별 누적 고장 건수 Top 10</div>
|
||||
<div style="display:flex;gap:10px;font-size:11px;color:var(--gray4)">
|
||||
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#3B82F6;margin-right:3px"></span>처리 완료</span>
|
||||
<span><span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:#FCA5A5;margin-right:3px"></span>미처리</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position:relative" id="topStationsWrap">
|
||||
<canvas id="topStationsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 충전기별 에러코드 누적 순위 -->
|
||||
<div class="card" style="margin-bottom:20px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px">
|
||||
<div class="dash-chart-head">
|
||||
<div class="card-title" style="margin:0">⚠️ 에러코드 누적 순위 Top 10</div>
|
||||
<span style="font-size:11px;color:var(--gray4)">에러코드 입력된 신고 기준</span>
|
||||
<span style="font-size:11px;color:var(--gray4)">전체 신고 기준 (에러코드 없음 포함)</span>
|
||||
</div>
|
||||
<div id="errorCodesChartWrap" style="position:relative">
|
||||
<canvas id="errorCodesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
|
||||
<div class="dash-bottom-grid">
|
||||
<div class="card">
|
||||
<div class="card-title">🔴 접수 대기 현황 <span id="pendingSort" style="font-size:11px;font-weight:400;color:var(--gray4)">(오래된 순)</span></div>
|
||||
<div id="recentReports"></div>
|
||||
@@ -284,10 +324,7 @@
|
||||
onfocus="openDropdown()" autocomplete="off">
|
||||
<div class="charger-dropdown" id="chargerDropdown"></div>
|
||||
</div>
|
||||
<div class="charger-selected-badge" id="selectedBadge">
|
||||
<span id="selectedBadgeText"></span>
|
||||
<button onclick="clearCharger()" style="background:none;border:none;cursor:pointer;color:var(--gray4);font-size:13px">✕</button>
|
||||
</div>
|
||||
<div class="selected-chargers-list" id="selectedChargersList"></div>
|
||||
</div>
|
||||
|
||||
<!-- 발생 일시 -->
|
||||
@@ -311,7 +348,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 신고 범위 -->
|
||||
<div class="form-row">
|
||||
<div class="form-row" id="scopeRow">
|
||||
<label class="form-label">신고 범위</label>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">
|
||||
@@ -320,11 +357,11 @@
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">
|
||||
<input type="radio" name="scope" value="station" style="width:auto;accent-color:var(--accent)">
|
||||
<span><strong>충전소 전체</strong> <span style="font-size:11px;color:var(--gray4)">— 같은 충전소의 모든 충전기</span></span>
|
||||
<span><strong>충전소 전체</strong> <span style="font-size:11px;color:var(--gray4)">— 신고 1건으로 충전소 내 모든 충전기 대상</span></span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:10px;font-size:13px;cursor:pointer">
|
||||
<input type="radio" name="scope" value="type" style="width:auto;accent-color:var(--accent)">
|
||||
<span><strong>동일 모델 전체</strong> <span style="font-size:11px;color:var(--gray4)">— 같은 충전기 모델 전체</span></span>
|
||||
<span><strong>동일 모델 전체</strong> <span style="font-size:11px;color:var(--gray4)">— 신고 1건으로 같은 모델 전체 대상</span></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,7 +433,7 @@ Auth.require(['admin']);
|
||||
Auth.renderNav(document.getElementById('navUser'));
|
||||
|
||||
let allChargers = [];
|
||||
let selectedChargerId = null;
|
||||
let selectedChargers = [];
|
||||
let cachedIssueTypes = null;
|
||||
let selectedChargerErrors = [];
|
||||
|
||||
@@ -425,6 +462,7 @@ async function load() {
|
||||
]);
|
||||
loadMonthlyChart();
|
||||
loadTopChargersChart();
|
||||
loadTopStationsChart();
|
||||
loadErrorCodesChart();
|
||||
|
||||
/* ── 통계 카드 ── */
|
||||
@@ -433,7 +471,7 @@ async function load() {
|
||||
<div class="stat stat-link" onclick="location.href='/pages/admin/reports.html'">
|
||||
<div class="stat-num">${stats.total}</div><div class="stat-label">전체 신고</div>
|
||||
</div>
|
||||
<div class="stat warn stat-link" onclick="location.href='/pages/admin/reports.html?status=pending'">
|
||||
<div class="stat warn stat-link" onclick="location.href='/pages/admin/reports.html?status=pending_all'">
|
||||
<div class="stat-num">${stats.pending}</div><div class="stat-label">접수 대기</div>
|
||||
</div>
|
||||
<div class="stat warn stat-link" onclick="location.href='/pages/admin/reports.html?status=in_progress'">
|
||||
@@ -448,11 +486,11 @@ async function load() {
|
||||
<div class="stat warn stat-link" onclick="location.href='/pages/admin/improvements.html'">
|
||||
<div class="stat-num">${stats.improvement_open}</div><div class="stat-label">개선항목 진행중</div>
|
||||
</div>
|
||||
<div class="stat stat-link" onclick="location.href='/pages/admin/reports.html?status=pending'" style="border-top:3px solid var(--accent)">
|
||||
<div class="stat stat-link" onclick="location.href='/pages/admin/reports.html?status=pending_all'" style="border-top:3px solid var(--accent)">
|
||||
<div class="stat-num" style="font-size:22px">${fmtHours(stats.avg_resolution_hours_30d)}</div>
|
||||
<div class="stat-label">평균 처리 시간<br><small>(최근 30일)</small></div>
|
||||
</div>
|
||||
<div class="stat ${over72Class} stat-link" onclick="location.href='/pages/admin/reports.html?status=pending'" style="border-top:3px solid ${stats.pending_over_72h>0?'var(--red)':'var(--green)'}">
|
||||
<div class="stat ${over72Class} stat-link" onclick="location.href='/pages/admin/reports.html?status=pending_all'" style="border-top:3px solid ${stats.pending_over_72h>0?'var(--red)':'var(--green)'}">
|
||||
<div class="stat-num">${stats.pending_over_72h}</div>
|
||||
<div class="stat-label">72h+ 장기 대기</div>
|
||||
</div>
|
||||
@@ -636,13 +674,103 @@ async function loadTopChargersChart() {
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Top 10 충전소별 누적 고장 차트 ── */
|
||||
let _topStationsChart = null;
|
||||
async function loadTopStationsChart() {
|
||||
const data = await API.get('/stats/top-stations');
|
||||
if (!data.length) return;
|
||||
|
||||
const rev = [...data].reverse();
|
||||
|
||||
const labels = rev.map(d => {
|
||||
const s = d.station_name || '-';
|
||||
return s.length > 22 ? s.slice(0, 20) + '…' : s;
|
||||
});
|
||||
|
||||
const rowH = 32;
|
||||
const height = rev.length * rowH + 40;
|
||||
document.getElementById('topStationsWrap').style.height = height + 'px';
|
||||
|
||||
if (_topStationsChart) _topStationsChart.destroy();
|
||||
const ctx = document.getElementById('topStationsChart').getContext('2d');
|
||||
_topStationsChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '처리 완료',
|
||||
data: rev.map(d => d.done),
|
||||
backgroundColor: '#3B82F6',
|
||||
borderRadius: 3,
|
||||
stack: 'top',
|
||||
},
|
||||
{
|
||||
label: '미처리',
|
||||
data: rev.map(d => d.active),
|
||||
backgroundColor: '#FCA5A5',
|
||||
borderRadius: 3,
|
||||
stack: 'top',
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
onClick: (evt, elems) => {
|
||||
if (!elems.length) return;
|
||||
const d = rev[elems[0].index];
|
||||
location.href = `/pages/admin/reports.html?station_name=${encodeURIComponent(d.station_name)}`;
|
||||
},
|
||||
onHover: (evt, elems) => {
|
||||
evt.native.target.style.cursor = elems.length ? 'pointer' : 'default';
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: items => rev[items[0].dataIndex].station_name,
|
||||
label: c => {
|
||||
const d = rev[c.dataIndex];
|
||||
return [
|
||||
`총 누적: ${d.total}건`,
|
||||
`처리 완료: ${d.done}건`,
|
||||
`미처리: ${d.active}건`,
|
||||
];
|
||||
},
|
||||
afterLabel: () => '(클릭하면 신고 목록으로)',
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: { color: '#F1F5F9' },
|
||||
border: { dash: [3, 3] },
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
font: { size: 11 }, color: '#64748B', stepSize: 1,
|
||||
callback: v => Number.isInteger(v) ? v + '건' : '',
|
||||
}
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
grid: { display: false },
|
||||
ticks: { font: { size: 12 }, color: '#334155' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ── 에러코드별 누적 순위 차트 ── */
|
||||
let _errorCodesChart = null;
|
||||
async function loadErrorCodesChart() {
|
||||
const data = await API.get('/stats/charger-error-codes');
|
||||
const wrap = document.getElementById('errorCodesChartWrap');
|
||||
if (!data.error_codes || !data.error_codes.length) {
|
||||
wrap.innerHTML = '<div style="color:var(--gray4);font-size:13px;text-align:center;padding:30px 0">에러코드가 입력된 신고가 없습니다.</div>';
|
||||
wrap.innerHTML = '<div style="color:var(--gray4);font-size:13px;text-align:center;padding:30px 0">신고 데이터가 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
const { error_codes } = data;
|
||||
@@ -655,7 +783,7 @@ async function loadErrorCodesChart() {
|
||||
labels: error_codes.map(e => e.error_code),
|
||||
datasets: [{
|
||||
data: error_codes.map(e => e.total),
|
||||
backgroundColor: '#1565C0',
|
||||
backgroundColor: error_codes.map(e => e.error_code === '에러코드 없음' ? '#94A3B8' : '#1565C0'),
|
||||
borderRadius: 4,
|
||||
}]
|
||||
},
|
||||
@@ -667,7 +795,9 @@ async function loadErrorCodesChart() {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: items => `에러코드: ${error_codes[items[0].dataIndex].error_code}`,
|
||||
title: items => error_codes[items[0].dataIndex].error_code === '에러코드 없음'
|
||||
? '에러코드 없음'
|
||||
: `에러코드: ${error_codes[items[0].dataIndex].error_code}`,
|
||||
label: c => `누적 ${c.raw}건`,
|
||||
}
|
||||
}
|
||||
@@ -983,11 +1113,12 @@ function closeReportModal() {
|
||||
}
|
||||
|
||||
function resetModal() {
|
||||
selectedChargerId = null;
|
||||
selectedChargers = [];
|
||||
selectedChargerErrors = [];
|
||||
document.getElementById('chargerSearchInput').value = '';
|
||||
document.getElementById('chargerDropdown').classList.remove('open');
|
||||
document.getElementById('selectedBadge').classList.remove('show');
|
||||
renderSelectedChargers();
|
||||
updateScopeVisibility();
|
||||
document.getElementById('occurredAt').value = '';
|
||||
document.getElementById('issueDetail').value = '';
|
||||
document.getElementById('contact').value = '';
|
||||
@@ -1016,7 +1147,7 @@ function filterChargers(q) {
|
||||
: allChargers.slice(0, 50);
|
||||
|
||||
dd.innerHTML = filtered.map(c => `
|
||||
<div class="charger-option ${c.id === selectedChargerId ? 'selected' : ''}"
|
||||
<div class="charger-option ${selectedChargers.some(s => s.id === c.id) ? 'selected' : ''}"
|
||||
onclick="selectCharger('${c.id}', '${escHtml(c.station_name)}', '${escHtml(c.name)}', '${escHtml(c.location_detail||'')}')">
|
||||
<div class="opt-name">${escHtml(c.station_name)} · ${escHtml(c.name)}</div>
|
||||
<div class="opt-sub">${c.id}${c.location_detail ? ' · ' + escHtml(c.location_detail) : ''}</div>
|
||||
@@ -1030,18 +1161,59 @@ function openDropdown() {
|
||||
}
|
||||
|
||||
async function selectCharger(id, station, name, region) {
|
||||
selectedChargerId = id;
|
||||
if (selectedChargers.some(c => c.id === id)) {
|
||||
document.getElementById('chargerDropdown').classList.remove('open');
|
||||
document.getElementById('chargerSearchInput').value = '';
|
||||
return;
|
||||
}
|
||||
selectedChargers.push({ id, station, name, region });
|
||||
document.getElementById('chargerSearchInput').value = '';
|
||||
document.getElementById('chargerDropdown').classList.remove('open');
|
||||
const badge = document.getElementById('selectedBadge');
|
||||
document.getElementById('selectedBadgeText').textContent =
|
||||
`${station} · ${name}${region ? ' (' + region + ')' : ''}`;
|
||||
badge.classList.add('show');
|
||||
// Load error codes for this charger type
|
||||
try {
|
||||
selectedChargerErrors = await API.get('/chargers/' + id + '/errors');
|
||||
} catch { selectedChargerErrors = []; }
|
||||
renderErrorCodeUI();
|
||||
renderSelectedChargers();
|
||||
updateScopeVisibility();
|
||||
if (selectedChargers.length === 1) {
|
||||
try {
|
||||
selectedChargerErrors = await API.get('/chargers/' + id + '/errors');
|
||||
} catch { selectedChargerErrors = []; }
|
||||
renderErrorCodeUI();
|
||||
} else {
|
||||
selectedChargerErrors = [];
|
||||
renderErrorCodeUI();
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectedChargers() {
|
||||
const el = document.getElementById('selectedChargersList');
|
||||
if (!selectedChargers.length) {
|
||||
el.innerHTML = '';
|
||||
el.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
el.classList.add('show');
|
||||
el.innerHTML = selectedChargers.map(c => `
|
||||
<div class="sel-tag">
|
||||
<span>${escHtml(c.station)} · ${escHtml(c.name)}</span>
|
||||
<button onclick="removeCharger('${escHtml(c.id)}')" title="제거">✕</button>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function removeCharger(id) {
|
||||
selectedChargers = selectedChargers.filter(c => c.id !== id);
|
||||
renderSelectedChargers();
|
||||
updateScopeVisibility();
|
||||
if (selectedChargers.length === 0) {
|
||||
selectedChargerErrors = [];
|
||||
renderErrorCodeUI();
|
||||
} else if (selectedChargers.length === 1) {
|
||||
API.get('/chargers/' + selectedChargers[0].id + '/errors')
|
||||
.then(e => { selectedChargerErrors = e; renderErrorCodeUI(); })
|
||||
.catch(() => { selectedChargerErrors = []; renderErrorCodeUI(); });
|
||||
}
|
||||
}
|
||||
|
||||
function updateScopeVisibility() {
|
||||
const row = document.getElementById('scopeRow');
|
||||
if (row) row.style.display = selectedChargers.length > 1 ? 'none' : '';
|
||||
}
|
||||
|
||||
function renderErrorCodeUI() {
|
||||
@@ -1075,10 +1247,11 @@ function getModalErrorCode() {
|
||||
}
|
||||
|
||||
function clearCharger() {
|
||||
selectedChargerId = null;
|
||||
selectedChargers = [];
|
||||
selectedChargerErrors = [];
|
||||
document.getElementById('chargerSearchInput').value = '';
|
||||
document.getElementById('selectedBadge').classList.remove('show');
|
||||
renderSelectedChargers();
|
||||
updateScopeVisibility();
|
||||
renderErrorCodeUI();
|
||||
}
|
||||
|
||||
@@ -1096,44 +1269,56 @@ function escHtml(s) {
|
||||
|
||||
/* ── 신고 제출 ── */
|
||||
async function submitReport() {
|
||||
if (!selectedChargerId) { alert('충전기를 선택해주세요.'); return; }
|
||||
if (!selectedChargers.length) { alert('충전기를 선택해주세요.'); return; }
|
||||
const issues = [...document.querySelectorAll('.issue-chk:checked')].map(c => c.value);
|
||||
if (!issues.length) { alert('증상을 하나 이상 선택해주세요.'); return; }
|
||||
|
||||
const btn = document.getElementById('submitBtn');
|
||||
btn.disabled = true; btn.textContent = '접수 중...';
|
||||
|
||||
const scope = document.querySelector('input[name="scope"]:checked')?.value || 'single';
|
||||
const scope = selectedChargers.length === 1
|
||||
? (document.querySelector('input[name="scope"]:checked')?.value || 'single')
|
||||
: 'single';
|
||||
|
||||
const issueDetail = document.getElementById('issueDetail').value;
|
||||
const errorCode = getModalErrorCode();
|
||||
const contact = document.getElementById('contact').value;
|
||||
const occurredAt = document.getElementById('occurredAt').value;
|
||||
const ocppLog = document.getElementById('ocppLog').value.trim();
|
||||
const photos = Array.from(document.getElementById('modalPhoto').files);
|
||||
|
||||
const isMulti = selectedChargers.length > 1;
|
||||
const fd = new FormData();
|
||||
fd.append('charger_id', selectedChargerId);
|
||||
fd.append('scope', scope);
|
||||
fd.append('charger_id', selectedChargers[0].id);
|
||||
fd.append('scope', isMulti ? 'multi' : scope);
|
||||
if (isMulti) fd.append('charger_ids', JSON.stringify(selectedChargers.map(c => c.id)));
|
||||
fd.append('source', 'dashboard');
|
||||
fd.append('issue_types', JSON.stringify(issues));
|
||||
fd.append('issue_detail', document.getElementById('issueDetail').value);
|
||||
fd.append('error_code', getModalErrorCode());
|
||||
fd.append('contact', document.getElementById('contact').value);
|
||||
fd.append('consent', 'false');
|
||||
const occ = document.getElementById('occurredAt').value;
|
||||
if (occ) fd.append('occurred_at', occ);
|
||||
const ocppLogText = document.getElementById('ocppLog').value.trim();
|
||||
if (ocppLogText) fd.append('ocpp_log', ocppLogText);
|
||||
Array.from(document.getElementById('modalPhoto').files).forEach(f => fd.append('photos', f));
|
||||
if (issueDetail) fd.append('issue_detail', issueDetail);
|
||||
if (errorCode) fd.append('error_code', errorCode);
|
||||
if (contact) fd.append('contact', contact);
|
||||
if (occurredAt) fd.append('occurred_at', occurredAt);
|
||||
if (ocppLog) fd.append('ocpp_log', ocppLog);
|
||||
photos.forEach(f => fd.append('photos', f));
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/reports/batch', { method: 'POST', body: fd,
|
||||
headers: { 'Authorization': 'Bearer ' + Auth.token() } });
|
||||
const res = await fetch('/api/reports/batch', {
|
||||
method: 'POST', body: fd,
|
||||
headers: { 'Authorization': 'Bearer ' + Auth.token() },
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const data = await res.json();
|
||||
closeReportModal();
|
||||
load();
|
||||
if (data.count > 1) {
|
||||
if (isMulti) {
|
||||
alert(`${selectedChargers.length}대 충전기 신고가 1건으로 접수되었습니다. (신고 #${data.primary_id})`);
|
||||
} else if (data.count > 1) {
|
||||
alert(`신고가 ${data.count}건 접수되었습니다. (첫 번째 신고 #${data.primary_id})`);
|
||||
location.href = `/pages/admin/report-detail.html?id=${data.primary_id}`;
|
||||
} else {
|
||||
alert(`신고가 접수되었습니다. (신고 #${data.primary_id})`);
|
||||
location.href = `/pages/admin/report-detail.html?id=${data.primary_id}`;
|
||||
}
|
||||
location.href = `/pages/admin/report-detail.html?id=${data.primary_id}`;
|
||||
} catch(e) {
|
||||
alert('오류: ' + e.message);
|
||||
} finally {
|
||||
|
||||
224
frontend/static/pages/admin/export.html
Normal file
224
frontend/static/pages/admin/export.html
Normal file
@@ -0,0 +1,224 @@
|
||||
<!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>
|
||||
.export-card { background:white; border-radius:10px; padding:24px; box-shadow:0 2px 8px rgba(0,0,0,.06); margin-bottom:20px; }
|
||||
.sheet-badge { display:inline-flex; align-items:center; gap:6px; padding:5px 14px; border-radius:20px; font-size:12px; font-weight:700; margin:3px; }
|
||||
.date-row { display:flex; gap:14px; align-items:flex-end; flex-wrap:wrap; margin-top:18px; }
|
||||
.date-row .form-group { margin:0; min-width:140px; }
|
||||
.quick-btns { display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; }
|
||||
.quick-btn { padding:4px 12px; border:1px solid var(--gray3); border-radius:6px; background:white; font-size:12px; color:var(--navy); cursor:pointer; }
|
||||
.quick-btn:hover { background:var(--gray1); border-color:var(--accent); }
|
||||
.download-btn { padding:12px 28px; font-size:15px; font-weight:700; border-radius:8px; border:none;
|
||||
background:var(--blue); color:white; cursor:pointer; display:flex; align-items:center; gap:8px;
|
||||
transition:background .15s; min-width:220px; justify-content:center; }
|
||||
.download-btn:hover { background:#1251A3; }
|
||||
.download-btn:disabled { background:var(--gray3); cursor:not-allowed; }
|
||||
.ind-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); gap:12px; margin-top:16px; }
|
||||
.ind-item { border:1px solid var(--gray2); border-radius:8px; padding:14px 16px; }
|
||||
.ind-item .ind-title { font-size:13px; font-weight:700; color:var(--navy); margin-bottom:8px; }
|
||||
.ind-item .ind-desc { font-size:11px; color:var(--gray4); margin-bottom:10px; line-height:1.5; }
|
||||
.status-msg { padding:10px 14px; border-radius:6px; font-size:13px; margin-top:12px; display:none; }
|
||||
@media(max-width:768px){
|
||||
.date-row { flex-direction:column; align-items:stretch; }
|
||||
.download-btn { width:100%; }
|
||||
.ind-grid { grid-template-columns:1fr; }
|
||||
}
|
||||
</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">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html" class="active">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:18px;flex-wrap:wrap;">
|
||||
<h2 style="font-size:18px;font-weight:700;color:var(--navy)">📥 데이터 내보내기</h2>
|
||||
</div>
|
||||
|
||||
<!-- 통합 다운로드 -->
|
||||
<div class="export-card">
|
||||
<div class="card-title">📊 기간별 통합 다운로드</div>
|
||||
<p style="font-size:13px;color:var(--gray4);margin-bottom:8px;line-height:1.7;">
|
||||
설정한 기간의 모든 데이터를 <strong>하나의 엑셀 파일(4개 시트)</strong>로 다운로드합니다.
|
||||
</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:4px;">
|
||||
<span class="sheet-badge" style="background:#0B1E3D;color:white;">① AS 신고이력</span>
|
||||
<span class="sheet-badge" style="background:#1B5E20;color:white;">② 조치이력</span>
|
||||
<span class="sheet-badge" style="background:#4A148C;color:white;">③ 개선항목</span>
|
||||
<span class="sheet-badge" style="background:#E65100;color:white;">④ 출장비정산</span>
|
||||
<span class="sheet-badge" style="background:#37474F;color:white;">+ 요약</span>
|
||||
</div>
|
||||
|
||||
<div class="date-row">
|
||||
<div class="form-group">
|
||||
<label>시작일</label>
|
||||
<input type="date" id="dateFrom">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>종료일</label>
|
||||
<input type="date" id="dateTo">
|
||||
</div>
|
||||
<button class="download-btn" id="fullBtn" onclick="downloadFull()">
|
||||
📥 통합 엑셀 다운로드
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="quick-btns">
|
||||
<span style="font-size:11px;color:var(--gray4);align-self:center;">빠른 선택:</span>
|
||||
<button class="quick-btn" onclick="setRange(7)">최근 7일</button>
|
||||
<button class="quick-btn" onclick="setRange(30)">최근 30일</button>
|
||||
<button class="quick-btn" onclick="setRange(90)">최근 3개월</button>
|
||||
<button class="quick-btn" onclick="setThisMonth()">이번 달</button>
|
||||
<button class="quick-btn" onclick="setLastMonth()">지난 달</button>
|
||||
<button class="quick-btn" onclick="setThisYear()">올해 전체</button>
|
||||
<button class="quick-btn" onclick="clearRange()">전체 기간</button>
|
||||
</div>
|
||||
|
||||
<div id="statusMsg" class="status-msg"></div>
|
||||
</div>
|
||||
|
||||
<!-- 개별 다운로드 -->
|
||||
<div class="export-card">
|
||||
<div class="card-title">📄 항목별 개별 다운로드</div>
|
||||
<p style="font-size:13px;color:var(--gray4);margin-bottom:4px;">위 기간 설정이 동일하게 적용됩니다.</p>
|
||||
<div class="ind-grid">
|
||||
<div class="ind-item">
|
||||
<div class="ind-title" style="color:#0B1E3D;">📋 AS 신고이력</div>
|
||||
<div class="ind-desc">신고 접수일 기준 · 충전기/신고자/상태/<br>정비사/조치내용/출장비 포함</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="downloadIndividual('reports')">📥 다운로드</button>
|
||||
</div>
|
||||
<div class="ind-item">
|
||||
<div class="ind-title" style="color:#1B5E20;">🔧 조치이력</div>
|
||||
<div class="ind-desc">조치 완료일 기준 · 정비사/조치유형/<br>소요시간/승인 여부 포함</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="downloadIndividual('repairs')">📥 다운로드</button>
|
||||
</div>
|
||||
<div class="ind-item">
|
||||
<div class="ind-title" style="color:#4A148C;">🔧 개선항목</div>
|
||||
<div class="ind-desc">등록일 기준 · 분류/우선순위/<br>담당업체/진행상태 포함</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="downloadIndividual('improvements')">📥 다운로드</button>
|
||||
</div>
|
||||
<div class="ind-item">
|
||||
<div class="ind-title" style="color:#E65100;">💰 출장비 정산</div>
|
||||
<div class="ind-desc">조치 완료일 기준 · 부담/수급 주체/<br>금액/정산상태 포함</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="downloadIndividual('costs')">📥 다운로드</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 toggleSidebar() {
|
||||
const s = document.getElementById('sidebar');
|
||||
const o = document.getElementById('navOverlay');
|
||||
if (s) s.classList.toggle('mobile-open');
|
||||
if (o) o.classList.toggle('show');
|
||||
}
|
||||
|
||||
function pad(n) { return String(n).padStart(2,'0'); }
|
||||
function fmtDate(d) { return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; }
|
||||
|
||||
function setRange(days) {
|
||||
const to = new Date();
|
||||
const from = new Date(); from.setDate(from.getDate() - days + 1);
|
||||
document.getElementById('dateFrom').value = fmtDate(from);
|
||||
document.getElementById('dateTo').value = fmtDate(to);
|
||||
}
|
||||
function setThisMonth() {
|
||||
const now = new Date();
|
||||
document.getElementById('dateFrom').value = `${now.getFullYear()}-${pad(now.getMonth()+1)}-01`;
|
||||
document.getElementById('dateTo').value = fmtDate(now);
|
||||
}
|
||||
function setLastMonth() {
|
||||
const now = new Date();
|
||||
const y = now.getMonth() === 0 ? now.getFullYear()-1 : now.getFullYear();
|
||||
const m = now.getMonth() === 0 ? 12 : now.getMonth();
|
||||
const last = new Date(y, m, 0);
|
||||
document.getElementById('dateFrom').value = `${y}-${pad(m)}-01`;
|
||||
document.getElementById('dateTo').value = fmtDate(last);
|
||||
}
|
||||
function setThisYear() {
|
||||
const now = new Date();
|
||||
document.getElementById('dateFrom').value = `${now.getFullYear()}-01-01`;
|
||||
document.getElementById('dateTo').value = fmtDate(now);
|
||||
}
|
||||
function clearRange() {
|
||||
document.getElementById('dateFrom').value = '';
|
||||
document.getElementById('dateTo').value = '';
|
||||
}
|
||||
|
||||
function buildQuery() {
|
||||
const from = document.getElementById('dateFrom').value;
|
||||
const to = document.getElementById('dateTo').value;
|
||||
const p = [];
|
||||
if (from) p.push('date_from=' + from);
|
||||
if (to) p.push('date_to=' + to);
|
||||
return p.length ? '?' + p.join('&') : '';
|
||||
}
|
||||
|
||||
function showStatus(msg, type='info') {
|
||||
const el = document.getElementById('statusMsg');
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
el.className = 'status-msg alert alert-' + type;
|
||||
}
|
||||
function hideStatus() { document.getElementById('statusMsg').style.display = 'none'; }
|
||||
|
||||
async function downloadFull() {
|
||||
const btn = document.getElementById('fullBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ 생성 중...';
|
||||
showStatus('엑셀 파일을 생성 중입니다. 데이터량에 따라 수 초가 걸릴 수 있습니다.', 'info');
|
||||
try {
|
||||
const from = document.getElementById('dateFrom').value;
|
||||
const to = document.getElementById('dateTo').value;
|
||||
const period = (from || to) ? `${from||'전체'}~${to||'전체'}` : '전체기간';
|
||||
await API.download('/export/full' + buildQuery(), `EV_AS_통합이력_${period}.xlsx`);
|
||||
showStatus('✅ 다운로드가 완료되었습니다.', 'success');
|
||||
} catch(e) {
|
||||
showStatus('❌ 오류: ' + e.message, 'danger');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '📥 통합 엑셀 다운로드';
|
||||
setTimeout(hideStatus, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadIndividual(type) {
|
||||
const names = {
|
||||
reports: 'AS신고이력',
|
||||
repairs: '조치이력',
|
||||
improvements: '개선항목',
|
||||
costs: '출장비정산',
|
||||
};
|
||||
try {
|
||||
await API.download('/export/' + type + buildQuery(), `${names[type]}.xlsx`);
|
||||
} catch(e) { alert('다운로드 오류: ' + e.message); }
|
||||
}
|
||||
|
||||
// 기본값: 이번 달
|
||||
setThisMonth();
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -1,8 +1,40 @@
|
||||
<!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>
|
||||
<!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>
|
||||
.imp-grid { display:grid; grid-template-columns:1fr 1fr; gap:18px; }
|
||||
.info-dl { display:grid; grid-template-columns:80px 1fr; gap:8px 14px; font-size:13px; align-items:start; }
|
||||
.info-dl dt { color:var(--gray4); font-weight:600; padding-top:1px; }
|
||||
.info-dl dd { word-break:break-word; }
|
||||
.report-link { display:flex; align-items:center; gap:10px; padding:10px 12px;
|
||||
border:1px solid var(--gray2); border-radius:8px; margin-bottom:6px;
|
||||
cursor:pointer; font-size:13px; color:var(--navy); text-decoration:none; transition:background .15s; }
|
||||
.report-link:hover { background:var(--gray1); }
|
||||
.report-link-num { font-weight:700; color:var(--blue); flex-shrink:0; }
|
||||
.file-link { display:flex; align-items:center; gap:8px; padding:9px 12px;
|
||||
border:1px solid var(--gray2); border-radius:8px; margin-bottom:6px;
|
||||
color:var(--navy); text-decoration:none; font-size:13px; transition:background .15s; overflow:hidden; }
|
||||
.file-link:hover { background:var(--gray1); }
|
||||
.file-link span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.page-header { display:flex; align-items:center; gap:10px; margin-bottom:18px; }
|
||||
.page-header h2 { font-size:17px; font-weight:700; color:var(--navy); flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.status-form { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
|
||||
@media(max-width:768px) {
|
||||
.imp-grid { grid-template-columns:1fr; }
|
||||
.status-form { grid-template-columns:1fr; }
|
||||
.info-dl { grid-template-columns:72px 1fr; gap:7px 10px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
|
||||
<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">
|
||||
<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>
|
||||
@@ -14,105 +46,139 @@
|
||||
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div style="margin-bottom:14px;display:flex;gap:10px;align-items:center;">
|
||||
<a href="/pages/admin/improvements.html" class="btn btn-outline btn-sm">← 목록</a>
|
||||
<h2 id="pageTitle" style="font-size:17px;font-weight:700;color:var(--navy)">개선항목 상세</h2>
|
||||
<div class="page-header">
|
||||
<a href="/pages/admin/improvements.html" class="btn btn-outline btn-sm" style="flex-shrink:0">← 목록</a>
|
||||
<h2 id="pageTitle">개선항목 상세</h2>
|
||||
</div>
|
||||
<div id="content"></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 toggleSidebar() {
|
||||
const s = document.getElementById('sidebar');
|
||||
const o = document.getElementById('navOverlay');
|
||||
if (s) s.classList.toggle('mobile-open');
|
||||
if (o) o.classList.toggle('show');
|
||||
}
|
||||
|
||||
const id = new URLSearchParams(location.search).get('id');
|
||||
const CAT={sw:'SW개선',hw:'HW개선',ui:'UI개선',firmware:'펌웨어',other:'기타'};
|
||||
const CAT = {hardware:'하드웨어',software:'소프트웨어',firmware:'펌웨어',installation:'설치환경',ui:'UI 개선',other:'기타'};
|
||||
const PRI = {urgent:'🔴 긴급',high:'🟠 높음',normal:'🟡 보통',low:'🟢 낮음'};
|
||||
const STATUS_OPTIONS = ['registered','reviewing','developing','deployed','done'];
|
||||
const STATUS_LABEL = {registered:'등록',reviewing:'검토중',developing:'개발중',deployed:'배포완료',done:'완료'};
|
||||
const STATUS_LABEL = {registered:'등록',reviewing:'검토중',developing:'개발중',deployed:'배포완료',done:'완료'};
|
||||
|
||||
async function load() {
|
||||
const imp = await API.get('/improvements/'+id);
|
||||
document.getElementById('pageTitle').textContent = `개선항목 #${imp.id}`;
|
||||
document.getElementById('pageTitle').textContent = `#${imp.id} ${imp.title}`;
|
||||
|
||||
document.getElementById('content').innerHTML = `
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
|
||||
<div class="imp-grid">
|
||||
<!-- 기본 정보 -->
|
||||
<div class="card">
|
||||
<div class="card-title">📋 기본 정보</div>
|
||||
<table class="no-hover" style="font-size:13px">
|
||||
<tr><td style="color:var(--gray4);width:90px">제목</td><td><strong>${imp.title}</strong></td></tr>
|
||||
<tr><td style="color:var(--gray4)">분류</td><td>${CAT[imp.category]||imp.category}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">우선순위</td><td>${imp.priority}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">관련 부품</td><td>${imp.part_name||'-'}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">담당 제조사</td><td><strong>${imp.manufacturer_company||'-'}</strong><br>${imp.manufacturer_name||''}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">등록자</td><td>${imp.created_by_name||'-'}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">등록일시</td><td>${Auth.fmtDt(imp.created_at)}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">배포 목표일</td><td>${imp.sw_deploy_target||'-'}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">실제 배포일</td><td>${imp.sw_deployed_at||'-'}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">현재 상태</td><td>${Auth.statusBadge(imp.status)}</td></tr>
|
||||
</table>
|
||||
<div style="margin-top:12px">
|
||||
<dl class="info-dl">
|
||||
<dt>제목</dt> <dd><strong>${imp.title}</strong></dd>
|
||||
<dt>분류</dt> <dd>${CAT[imp.category]||imp.category}</dd>
|
||||
<dt>우선순위</dt> <dd>${PRI[imp.priority]||imp.priority}</dd>
|
||||
<dt>관련 부품</dt> <dd>${imp.part_name||'-'}</dd>
|
||||
<dt>담당 업체</dt> <dd><strong>${imp.manufacturer_name||'-'}</strong></dd>
|
||||
<dt>등록자</dt> <dd>${imp.created_by_name||'-'}</dd>
|
||||
<dt>등록일시</dt> <dd>${Auth.fmtDt(imp.created_at)}</dd>
|
||||
<dt>배포 목표일</dt><dd>${imp.sw_deploy_target||'-'}</dd>
|
||||
<dt>실제 배포일</dt><dd>${imp.sw_deployed_at||'-'}</dd>
|
||||
<dt>상태</dt> <dd>${Auth.statusBadge(imp.status)}</dd>
|
||||
</dl>
|
||||
<div style="margin-top:14px">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--navy2);margin-bottom:6px">개선 내용</div>
|
||||
<div style="background:var(--gray1);padding:12px;border-radius:6px;font-size:13px;white-space:pre-wrap">${imp.description}</div>
|
||||
<div style="background:var(--gray1);padding:12px;border-radius:6px;font-size:13px;white-space:pre-wrap;line-height:1.7">${imp.description}</div>
|
||||
</div>
|
||||
${imp.manufacturer_memo?`<div style="margin-top:12px"><div style="font-size:12px;font-weight:700;color:var(--orange);margin-bottom:6px">제조사 메모</div><div style="background:#FFF5E6;padding:12px;border-radius:6px;font-size:13px">${imp.manufacturer_memo}</div></div>`:''}
|
||||
${imp.manufacturer_memo ? `
|
||||
<div style="margin-top:12px">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--orange);margin-bottom:6px">제조사 메모</div>
|
||||
<div style="background:#FFF5E6;padding:12px;border-radius:6px;font-size:13px;line-height:1.6">${imp.manufacturer_memo}</div>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">📎 연결된 AS 신고</div>
|
||||
${imp.report_ids.length ? imp.report_ids.map(rid=>`
|
||||
<div onclick="location.href='/pages/admin/report-detail.html?id=${rid}'"
|
||||
style="padding:8px;border:1px solid var(--gray2);border-radius:6px;margin-bottom:6px;cursor:pointer;font-size:13px">
|
||||
신고 #${rid}
|
||||
</div>`).join('') : '<div class="alert alert-info">연결된 신고 없음</div>'}
|
||||
|
||||
<div class="card-title" style="margin-top:16px">📁 첨부 파일</div>
|
||||
${imp.attachments.length ? imp.attachments.map(a=>`
|
||||
<a href="${a.path}" target="_blank" class="btn btn-outline btn-sm" style="margin-bottom:6px;display:block">
|
||||
📄 ${a.name||a.path.split('/').pop()}
|
||||
</a>`).join('') : '<div style="font-size:13px;color:var(--gray4)">첨부 파일 없음</div>'}
|
||||
<!-- 연결 AS + 첨부 -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-title">📎 연결된 AS 신고</div>
|
||||
${(imp.report_links||[]).length
|
||||
? (imp.report_links||[]).map(r => `
|
||||
<a class="report-link" href="/pages/admin/report-detail.html?id=${r.id}">
|
||||
<span class="report-link-num">#${r.seq}</span>
|
||||
<span style="color:var(--gray4);font-size:12px">신고 상세 보기 →</span>
|
||||
</a>`).join('')
|
||||
: '<div class="alert alert-info" style="margin:0">연결된 신고 없음</div>'}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">📁 첨부 파일</div>
|
||||
${imp.attachments.length
|
||||
? imp.attachments.map(a => `
|
||||
<a class="file-link" href="${a.path}" target="_blank">
|
||||
<span style="flex-shrink:0">📄</span>
|
||||
<span>${a.name||a.path.split('/').pop()}</span>
|
||||
</a>`).join('')
|
||||
: '<div style="font-size:13px;color:var(--gray4)">첨부 파일 없음</div>'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상태 변경 -->
|
||||
<div class="card" style="margin-top:0">
|
||||
<div class="card">
|
||||
<div class="card-title">🔄 상태 변경</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>상태 변경</label>
|
||||
<div class="status-form">
|
||||
<div class="form-group" style="margin:0">
|
||||
<label>상태</label>
|
||||
<select id="newStatus">
|
||||
${STATUS_OPTIONS.map(s=>`<option value="${s}" ${imp.status===s?'selected':''}>${STATUS_LABEL[s]}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>SW 실제 배포일 (배포완료 시)</label>
|
||||
<div class="form-group" style="margin:0">
|
||||
<label>SW 실제 배포일 <span style="color:var(--gray4);font-weight:400">(배포완료 시)</span></label>
|
||||
<input type="date" id="deployedAt" value="${imp.sw_deployed_at||''}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"><label>변경 메모</label><input type="text" id="changeMemo" placeholder="상태 변경 사유 또는 메모"></div>
|
||||
<button class="btn btn-primary" onclick="changeStatus()">상태 저장</button>
|
||||
<div class="form-group" style="margin-top:12px">
|
||||
<label>변경 메모</label>
|
||||
<input type="text" id="changeMemo" placeholder="상태 변경 사유 또는 메모">
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="changeStatus()">저장</button>
|
||||
</div>
|
||||
|
||||
<!-- 이력 로그 -->
|
||||
<div class="card" style="margin-top:0">
|
||||
<!-- 변경 이력 -->
|
||||
<div class="card">
|
||||
<div class="card-title">📜 변경 이력</div>
|
||||
${imp.logs.length ? `<div class="timeline">${imp.logs.map(l=>`
|
||||
<div class="tl-item">
|
||||
<div class="tl-time">${Auth.fmtDt(l.changed_at)} — ${l.by||'시스템'}</div>
|
||||
<div class="tl-text">${l.old_status?`${STATUS_LABEL[l.old_status]||l.old_status} → `:''}${STATUS_LABEL[l.new_status]||l.new_status}${l.memo?` / ${l.memo}`:''}</div>
|
||||
</div>`).join('')}</div>` : '<div style="color:var(--gray4);font-size:13px">이력 없음</div>'}
|
||||
${imp.logs.length
|
||||
? `<div class="timeline">${imp.logs.map(l=>`
|
||||
<div class="tl-item">
|
||||
<div class="tl-time">${Auth.fmtDt(l.changed_at)} — ${l.by||'시스템'}</div>
|
||||
<div class="tl-text">${l.old_status?`${STATUS_LABEL[l.old_status]||l.old_status} → `:''}${STATUS_LABEL[l.new_status]||l.new_status}${l.memo?` / ${l.memo}`:''}</div>
|
||||
</div>`).join('')}</div>`
|
||||
: '<div style="color:var(--gray4);font-size:13px">이력 없음</div>'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function changeStatus() {
|
||||
const status = document.getElementById('newStatus').value;
|
||||
const memo = document.getElementById('changeMemo').value;
|
||||
const date = document.getElementById('deployedAt').value;
|
||||
const fd = new FormData();
|
||||
fd.append('status', status); fd.append('memo', memo);
|
||||
fd.append('status', document.getElementById('newStatus').value);
|
||||
fd.append('memo', document.getElementById('changeMemo').value);
|
||||
const date = document.getElementById('deployedAt').value;
|
||||
if (date) fd.append('sw_deployed_at', date);
|
||||
await API.patch('/improvements/'+id+'/status', fd);
|
||||
load();
|
||||
}
|
||||
|
||||
load();
|
||||
</script></div></div></body></html>
|
||||
</script>
|
||||
</body></html>
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
#btnDelete { display:none; }
|
||||
</style></head>
|
||||
<body>
|
||||
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
|
||||
<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">
|
||||
<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>
|
||||
@@ -20,6 +21,7 @@
|
||||
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
@@ -62,9 +64,9 @@
|
||||
<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>
|
||||
<option value="hardware">하드웨어</option><option value="software">소프트웨어</option>
|
||||
<option value="firmware">펌웨어</option><option value="installation">설치환경</option>
|
||||
<option value="ui">UI 개선</option><option value="other">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -123,20 +125,25 @@ async function bulkDelete() {
|
||||
catch(e) { alert('삭제 중 오류가 발생했습니다: ' + e.message); }
|
||||
}
|
||||
|
||||
const CAT = {sw:'SW',hw:'HW',ui:'UI',firmware:'펌웨어',other:'기타'};
|
||||
const CAT = {hardware:'하드웨어',software:'소프트웨어',firmware:'펌웨어',installation:'설치환경',ui:'UI',other:'기타'};
|
||||
const PRI = {urgent:'🔴 긴급',high:'🟠 높음',normal:'🟡 보통',low:'🟢 낮음'};
|
||||
const selectedReports = new Set();
|
||||
let allReports = [];
|
||||
|
||||
async function load() {
|
||||
const st = document.getElementById('fStatus').value;
|
||||
const mfr = document.getElementById('fMfr').value;
|
||||
let impUrl = '/improvements?';
|
||||
if (st) impUrl += 'status=' + st + '&';
|
||||
if (mfr) impUrl += 'manufacturer_id=' + mfr + '&';
|
||||
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)
|
||||
API.get('/manufacturers'),
|
||||
API.get(impUrl)
|
||||
]);
|
||||
// 제조사 필터 드롭다운
|
||||
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); });
|
||||
mfrs.forEach(m => { const o=document.createElement('option'); o.value=m.id; o.textContent=m.name; mfrSel.appendChild(o); });
|
||||
|
||||
document.getElementById('chkAll').checked = false;
|
||||
updateDeleteBtn();
|
||||
@@ -151,7 +158,7 @@ async function load() {
|
||||
<td onclick="location.href='/pages/admin/improvement-detail.html?id=${i.id}'" style="cursor:pointer;max-width:200px"><strong>${i.title}</strong></td>
|
||||
<td onclick="location.href='/pages/admin/improvement-detail.html?id=${i.id}'" style="cursor:pointer">${CAT[i.category]||i.category}</td>
|
||||
<td onclick="location.href='/pages/admin/improvement-detail.html?id=${i.id}'" style="cursor:pointer">${PRI[i.priority]||i.priority}</td>
|
||||
<td onclick="location.href='/pages/admin/improvement-detail.html?id=${i.id}'" style="cursor:pointer">${i.manufacturer_company||'-'}<br><small>${i.manufacturer_name||''}</small></td>
|
||||
<td onclick="location.href='/pages/admin/improvement-detail.html?id=${i.id}'" style="cursor:pointer">${i.manufacturer_name||'-'}</td>
|
||||
<td onclick="location.href='/pages/admin/improvement-detail.html?id=${i.id}'" style="cursor:pointer"><span class="badge s-pending">${i.report_count}건</span></td>
|
||||
<td onclick="location.href='/pages/admin/improvement-detail.html?id=${i.id}'" style="cursor:pointer">${Auth.statusBadge(i.status)}</td>
|
||||
<td onclick="location.href='/pages/admin/improvement-detail.html?id=${i.id}'" style="cursor:pointer">${Auth.fmtDt(i.created_at)}</td>
|
||||
@@ -161,9 +168,9 @@ async function load() {
|
||||
|
||||
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('');
|
||||
const mfrs = await API.get('/manufacturers');
|
||||
document.getElementById('mMfr').innerHTML = '<option value="">업체 선택</option>' +
|
||||
mfrs.map(m=>`<option value="${m.id}">${m.name}</option>`).join('');
|
||||
allReports = await API.get('/reports');
|
||||
renderReportList('');
|
||||
}
|
||||
@@ -171,12 +178,12 @@ function closeModal() { document.getElementById('modal').classList.add('hidden')
|
||||
|
||||
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);
|
||||
const filtered = allReports.filter(r => !q || String(r.seq).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>
|
||||
<span><strong>#${r.seq}</strong> ${r.charger_id||''} — ${(r.issue_types||[]).join(', ')}</span>
|
||||
</label>`).join('') || '<div style="color:var(--gray4)">검색 결과 없음</div>';
|
||||
}
|
||||
|
||||
|
||||
@@ -59,9 +59,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
|
||||
<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">
|
||||
<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>
|
||||
@@ -73,6 +74,7 @@
|
||||
<a href="/pages/admin/issue-types.html" class="active">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<!DOCTYPE html><html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>QR 생성</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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -14,6 +15,7 @@
|
||||
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html" class="active">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
|
||||
@@ -114,15 +114,29 @@
|
||||
.issue-chk-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; margin-top:6px; }
|
||||
.issue-chk-item { display:flex; align-items:center; gap:6px; font-size:13px; }
|
||||
.issue-chk-item input { width:15px; height:15px; }
|
||||
/* 충전기 검색 드롭다운 */
|
||||
.ec-opt { padding:9px 12px; cursor:pointer; font-size:13px; border-bottom:1px solid #F1F5F9; }
|
||||
.ec-opt:hover { background:var(--gray1); }
|
||||
.ec-opt:last-child { border-bottom:none; }
|
||||
@media(max-width:768px) {
|
||||
.cost-summary-grid { grid-template-columns:1fr !important; }
|
||||
.issue-chk-grid { grid-template-columns:1fr !important; }
|
||||
.no-hover td:first-child { white-space:nowrap; }
|
||||
.no-hover td { word-break:break-word; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리 — 관리자</span>
|
||||
<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">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">AS 관리</div>
|
||||
<a href="/pages/admin/dashboard.html">📊 대시보드</a>
|
||||
<a href="/pages/admin/reports.html" class="active">📋 신고 목록</a>
|
||||
@@ -134,12 +148,13 @@
|
||||
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div style="margin-bottom:14px;display:flex;gap:10px;align-items:center;">
|
||||
<a href="/pages/admin/reports.html" class="btn btn-outline btn-sm">← 목록</a>
|
||||
<h2 id="pageTitle" style="font-size:17px;font-weight:700;color:var(--navy)">신고 상세</h2>
|
||||
<div style="margin-bottom:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
|
||||
<a href="/pages/admin/reports.html" class="btn btn-outline btn-sm" style="flex-shrink:0">← 목록</a>
|
||||
<h2 id="pageTitle" style="font-size:17px;font-weight:700;color:var(--navy);min-width:0;word-break:break-word;">신고 상세</h2>
|
||||
</div>
|
||||
<div id="content"></div>
|
||||
</div>
|
||||
@@ -152,12 +167,19 @@
|
||||
Auth.require(['admin']);
|
||||
Auth.renderNav(document.getElementById('navUser'));
|
||||
|
||||
function toggleSidebar() {
|
||||
const s = document.getElementById('sidebar');
|
||||
const o = document.getElementById('navOverlay');
|
||||
if (s) s.classList.toggle('mobile-open');
|
||||
if (o) o.classList.toggle('show');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const reportId = params.get('id');
|
||||
|
||||
const PARTY_LABEL = {
|
||||
cpo: 'CPO (운영사)',
|
||||
manufacturer: '제조사',
|
||||
manufacturer: '업체',
|
||||
self: '자체 부담',
|
||||
user: '사용자 과실',
|
||||
other: '기타',
|
||||
@@ -175,23 +197,36 @@ const COST_STATUS_ICON = {
|
||||
settled: '✅',
|
||||
};
|
||||
|
||||
let editOpen = false;
|
||||
|
||||
function toggleEdit() {
|
||||
editOpen = !editOpen;
|
||||
const wrap = document.getElementById('costEditWrap');
|
||||
const btn = document.getElementById('editToggleBtn');
|
||||
if (editOpen) {
|
||||
wrap.style.maxHeight = wrap.scrollHeight + 2000 + 'px';
|
||||
wrap.classList.remove('collapsed');
|
||||
btn.innerHTML = '▲ 수정 접기';
|
||||
function toggleCostEdit(repairId) {
|
||||
const wrap = document.getElementById('costEditWrap_' + repairId);
|
||||
const btn = document.getElementById('editToggleBtn_' + repairId);
|
||||
if (!wrap) return;
|
||||
const isOpen = wrap.style.display !== 'none';
|
||||
if (isOpen) {
|
||||
wrap.style.display = 'none';
|
||||
if (btn) btn.innerHTML = '✏️ 수정하기';
|
||||
} else {
|
||||
wrap.style.maxHeight = '0';
|
||||
wrap.classList.add('collapsed');
|
||||
btn.innerHTML = '✏️ 수정하기';
|
||||
wrap.style.display = 'block';
|
||||
if (btn) btn.innerHTML = '▲ 접기';
|
||||
}
|
||||
}
|
||||
|
||||
function togglePartySelect(repairId) {
|
||||
const v = document.getElementById('partyType_' + repairId)?.value;
|
||||
const mfr = document.getElementById('mfrWrap_' + repairId);
|
||||
const cus = document.getElementById('customWrap_' + repairId);
|
||||
if (mfr) mfr.style.display = v === 'manufacturer' ? 'block' : 'none';
|
||||
if (cus) cus.style.display = v === 'other' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function toggleRecvPartySelect(repairId) {
|
||||
const v = document.getElementById('recvPartyType_' + repairId)?.value;
|
||||
const mfr = document.getElementById('recvMfrWrap_' + repairId);
|
||||
const cus = document.getElementById('recvCustomWrap_' + repairId);
|
||||
if (mfr) mfr.style.display = v === 'manufacturer' ? 'block' : 'none';
|
||||
if (cus) cus.style.display = v === 'other' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
const IMP_CAT_LABEL = {
|
||||
hardware:'하드웨어', software:'소프트웨어', firmware:'펌웨어',
|
||||
installation:'설치환경', other:'기타'
|
||||
@@ -201,158 +236,143 @@ async function load() {
|
||||
const [r, issueTypes, manufacturers, improvements] = await Promise.all([
|
||||
API.get('/reports/' + reportId),
|
||||
API.get('/settings/issue-types'),
|
||||
API.get('/accounts?role=manufacturer'),
|
||||
API.get('/manufacturers/public'),
|
||||
API.get('/improvements'),
|
||||
]);
|
||||
const repair = r.repair;
|
||||
const cost = repair?.cost;
|
||||
const prevRepairs = r.prev_repairs || [];
|
||||
window._reportData = r; // saveReport 경고용
|
||||
|
||||
document.getElementById('pageTitle').textContent = `신고 #${r.seq} 상세`;
|
||||
|
||||
// ── 출장비 요약 HTML 생성 ──
|
||||
let costHtml = '';
|
||||
if (repair) {
|
||||
const hasCost = cost && cost.cost_party_type;
|
||||
const costStatus = cost?.cost_status || 'pending';
|
||||
// ── 조치별 출장비 HTML 생성 함수 ──
|
||||
function buildCostHtml(rep, mfrs) {
|
||||
const c = rep.cost;
|
||||
const rid = rep.id;
|
||||
const hasCost = c && c.cost_party_type;
|
||||
const costStatus = c?.cost_status || 'pending';
|
||||
const statusLabel = COST_STATUS_LABEL[costStatus] || costStatus;
|
||||
const statusIcon = COST_STATUS_ICON[costStatus] || '🕐';
|
||||
|
||||
// 부담 주체 텍스트
|
||||
let partyText = '-';
|
||||
if (cost?.cost_party_type) {
|
||||
partyText = PARTY_LABEL[cost.cost_party_type] || cost.cost_party_type;
|
||||
if (cost.cost_party_type === 'manufacturer' && cost.manufacturer_name) {
|
||||
partyText += ` (${cost.manufacturer_name})`;
|
||||
}
|
||||
if (cost.cost_party_type === 'other' && cost.cost_party_custom) {
|
||||
partyText += ` — ${cost.cost_party_custom}`;
|
||||
}
|
||||
if (c?.cost_party_type) {
|
||||
partyText = PARTY_LABEL[c.cost_party_type] || c.cost_party_type;
|
||||
if (c.cost_party_type === 'manufacturer' && c.cost_manufacturer_name) partyText += ` (${c.cost_manufacturer_name})`;
|
||||
if (c.cost_party_type === 'other' && c.cost_party_custom) partyText += ` — ${c.cost_party_custom}`;
|
||||
}
|
||||
|
||||
let recvText = '-';
|
||||
if (c?.recv_party_type) {
|
||||
recvText = PARTY_LABEL[c.recv_party_type] || c.recv_party_type;
|
||||
if (c.recv_party_type === 'manufacturer' && c.recv_manufacturer_name) recvText += ` (${c.recv_manufacturer_name})`;
|
||||
if (c.recv_party_type === 'other' && c.recv_party_custom) recvText += ` — ${c.recv_party_custom}`;
|
||||
}
|
||||
|
||||
// 요약 카드 (처리 내역이 있을 때만 표시)
|
||||
const summaryHtml = hasCost ? `
|
||||
<div class="cost-summary s-${costStatus}">
|
||||
<div class="cost-summary s-${costStatus}" style="margin-bottom:10px;">
|
||||
<div class="cost-summary-header">
|
||||
<div class="cost-summary-title">
|
||||
${statusIcon} 출장비 처리 내역
|
||||
</div>
|
||||
<div class="cost-summary-title">${statusIcon} 출장비 처리 내역</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span class="cost-status-badge csb-${costStatus}">${statusLabel}</span>
|
||||
<button class="edit-toggle-btn" id="editToggleBtn" onclick="toggleEdit()">✏️ 수정하기</button>
|
||||
<button class="edit-toggle-btn" id="editToggleBtn_${rid}" onclick="toggleCostEdit(${rid})">✏️ 수정하기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cost-summary-grid">
|
||||
<div class="cost-summary-item">
|
||||
<label>출장비 부담 주체</label>
|
||||
<span>${partyText}</span>
|
||||
</div>
|
||||
<div class="cost-summary-item">
|
||||
<label>출장비 금액</label>
|
||||
<span class="amount">${(cost.cost_amount || 0).toLocaleString()}원</span>
|
||||
</div>
|
||||
<div class="cost-summary-item">
|
||||
<label>처리 담당자</label>
|
||||
<span>${cost.reviewed_by_name || '-'}</span>
|
||||
</div>
|
||||
<div class="cost-summary-item">
|
||||
<label>처리 일시</label>
|
||||
<span>${Auth.fmtDt(cost.reviewed_at)}</span>
|
||||
</div>
|
||||
<div class="cost-summary-item"><label>부담 주체</label><span>${partyText}</span></div>
|
||||
<div class="cost-summary-item"><label>수급 주체</label><span>${recvText}</span></div>
|
||||
<div class="cost-summary-item"><label>금액</label><span class="amount">${(c.cost_amount||0).toLocaleString()}원</span></div>
|
||||
<div class="cost-summary-item"><label>담당자</label><span>${c.reviewed_by_name||'-'}</span></div>
|
||||
<div class="cost-summary-item"><label>처리일시</label><span>${Auth.fmtDt(c.reviewed_at)}</span></div>
|
||||
</div>
|
||||
${(c.root_cause||c.admin_note) ? `<hr class="cost-summary-divider">
|
||||
${c.root_cause ? `<div style="margin-bottom:6px;"><div style="font-size:10px;color:var(--gray4);margin-bottom:3px;">문제 원인</div><div class="cost-note-box">${c.root_cause}</div></div>` : ''}
|
||||
${c.admin_note ? `<div><div style="font-size:10px;color:var(--gray4);margin-bottom:3px;">비고</div><div class="cost-note-box">${c.admin_note}</div></div>` : ''}` : ''}
|
||||
</div>` : `<div style="font-size:12px;color:var(--gray4);margin-bottom:8px;">💰 출장비 미입력</div>`;
|
||||
|
||||
${(cost.root_cause || cost.admin_note) ? `
|
||||
<hr class="cost-summary-divider">
|
||||
${cost.root_cause ? `
|
||||
<div style="margin-bottom:8px;">
|
||||
<div style="font-size:10px;letter-spacing:1px;color:var(--gray4);text-transform:uppercase;margin-bottom:4px;">문제 원인</div>
|
||||
<div class="cost-note-box">${cost.root_cause}</div>
|
||||
</div>` : ''}
|
||||
${cost.admin_note ? `
|
||||
<div>
|
||||
<div style="font-size:10px;letter-spacing:1px;color:var(--gray4);text-transform:uppercase;margin-bottom:4px;">비고</div>
|
||||
<div class="cost-note-box">${cost.admin_note}</div>
|
||||
</div>` : ''}
|
||||
` : ''}
|
||||
</div>` : '';
|
||||
|
||||
// 수정 폼 (항상 존재, 기존 미처리면 바로 펼쳐져 있음)
|
||||
const formCollapsed = hasCost ? 'collapsed' : '';
|
||||
if (!hasCost) editOpen = true;
|
||||
|
||||
costHtml = `
|
||||
<div class="card" style="margin-top:0">
|
||||
<div class="card-title">💰 출장비 처리${hasCost ? '' : ' (관리자)'}</div>
|
||||
|
||||
const formDisplay = hasCost ? 'none' : 'block';
|
||||
return `
|
||||
<div style="border-top:1px dashed var(--gray3);margin-top:14px;padding-top:14px;">
|
||||
<div style="font-size:13px;font-weight:700;color:var(--navy);margin-bottom:10px;">💰 출장비 정산</div>
|
||||
${summaryHtml}
|
||||
|
||||
<!-- 입력 / 수정 폼 -->
|
||||
<div class="cost-edit-wrap ${formCollapsed}" id="costEditWrap">
|
||||
<div class="cost-edit-inner">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>문제 원인 파악 <span class="req">*</span></label>
|
||||
<textarea id="rootCause" rows="3"
|
||||
placeholder="조치 내용 검토 후 원인을 기재하세요.">${cost?.root_cause || ''}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>비고</label>
|
||||
<textarea id="adminNote" rows="3"
|
||||
placeholder="특이사항, 추가 메모 등">${cost?.admin_note || ''}</textarea>
|
||||
</div>
|
||||
<div id="costEditWrap_${rid}" style="display:${formDisplay}">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>문제 원인 <span class="req">*</span></label>
|
||||
<textarea id="rootCause_${rid}" rows="3" placeholder="조치 내용 검토 후 원인 기재">${c?.root_cause||''}</textarea>
|
||||
</div>
|
||||
<div class="form-row-3">
|
||||
<div class="form-group">
|
||||
<label>출장비 부담 주체 <span class="req">*</span></label>
|
||||
<select id="partyType" onchange="toggleParty()">
|
||||
<option value="">선택</option>
|
||||
<option value="cpo" ${cost?.cost_party_type === 'cpo' ? 'selected' : ''}>CPO (운영사)</option>
|
||||
<option value="manufacturer" ${cost?.cost_party_type === 'manufacturer' ? 'selected' : ''}>제조사</option>
|
||||
<option value="self" ${cost?.cost_party_type === 'self' ? 'selected' : ''}>자체 부담</option>
|
||||
<option value="user" ${cost?.cost_party_type === 'user' ? 'selected' : ''}>사용자 과실</option>
|
||||
<option value="other" ${cost?.cost_party_type === 'other' ? 'selected' : ''}>기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="mfrWrap"
|
||||
style="display:${cost?.cost_party_type === 'manufacturer' ? 'block' : 'none'}">
|
||||
<label>제조사 선택</label>
|
||||
<select id="partyMfr">
|
||||
<option value="">선택</option>
|
||||
${manufacturers.map(m =>
|
||||
`<option value="${m.id}"
|
||||
${cost?.cost_party_manufacturer_id == m.id ? 'selected' : ''}>
|
||||
${m.company || ''} / ${m.name}
|
||||
</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="customWrap"
|
||||
style="display:${cost?.cost_party_type === 'other' ? 'block' : 'none'}">
|
||||
<label>기타 직접 입력</label>
|
||||
<input type="text" id="partyCustom" value="${cost?.cost_party_custom || ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>비고</label>
|
||||
<textarea id="adminNote_${rid}" rows="3" placeholder="특이사항">${c?.admin_note||''}</textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>출장비 금액 (원)</label>
|
||||
<input type="number" id="costAmount"
|
||||
value="${cost?.cost_amount || 0}" min="0" step="1000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>처리 상태</label>
|
||||
<select id="costStatus">
|
||||
<option value="pending" ${(!cost || cost.cost_status === 'pending') ? 'selected' : ''}>미처리</option>
|
||||
<option value="billed" ${cost?.cost_status === 'billed' ? 'selected' : ''}>청구완료</option>
|
||||
<option value="waived" ${cost?.cost_status === 'waived' ? 'selected' : ''}>면제</option>
|
||||
<option value="settled" ${cost?.cost_status === 'settled' ? 'selected' : ''}>정산완료</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="costErr" class="alert alert-danger" style="display:none"></div>
|
||||
<button class="btn btn-primary" onclick="saveCost(${repair.id})">
|
||||
💾 출장비 처리 저장
|
||||
</button>
|
||||
</div>
|
||||
<div style="font-size:12px;font-weight:700;color:var(--navy2);margin-bottom:6px;">부담 주체 (비용 부담)</div>
|
||||
<div class="form-row-3">
|
||||
<div class="form-group">
|
||||
<label>유형 <span class="req">*</span></label>
|
||||
<select id="partyType_${rid}" onchange="togglePartySelect(${rid})">
|
||||
<option value="">선택</option>
|
||||
<option value="cpo" ${c?.cost_party_type==='cpo' ?'selected':''}>CPO (운영사)</option>
|
||||
<option value="manufacturer" ${c?.cost_party_type==='manufacturer' ?'selected':''}>업체</option>
|
||||
<option value="self" ${c?.cost_party_type==='self' ?'selected':''}>자체 부담</option>
|
||||
<option value="user" ${c?.cost_party_type==='user' ?'selected':''}>사용자 과실</option>
|
||||
<option value="other" ${c?.cost_party_type==='other' ?'selected':''}>기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="mfrWrap_${rid}" style="display:${c?.cost_party_type==='manufacturer'?'block':'none'}">
|
||||
<label>업체</label>
|
||||
<select id="partyMfr_${rid}">
|
||||
<option value="">선택</option>
|
||||
${mfrs.map(m=>`<option value="${m.id}" ${c?.cost_party_manufacturer_id==m.id?'selected':''}>${m.name}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="customWrap_${rid}" style="display:${c?.cost_party_type==='other'?'block':'none'}">
|
||||
<label>기타</label>
|
||||
<input type="text" id="partyCustom_${rid}" value="${c?.cost_party_custom||''}">
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:12px;font-weight:700;color:var(--navy2);margin-bottom:6px;margin-top:10px;">수급 주체 (비용 수령)</div>
|
||||
<div class="form-row-3">
|
||||
<div class="form-group">
|
||||
<label>유형</label>
|
||||
<select id="recvPartyType_${rid}" onchange="toggleRecvPartySelect(${rid})">
|
||||
<option value="">선택</option>
|
||||
<option value="cpo" ${c?.recv_party_type==='cpo' ?'selected':''}>CPO (운영사)</option>
|
||||
<option value="manufacturer" ${c?.recv_party_type==='manufacturer' ?'selected':''}>업체</option>
|
||||
<option value="self" ${c?.recv_party_type==='self' ?'selected':''}>자체 부담</option>
|
||||
<option value="user" ${c?.recv_party_type==='user' ?'selected':''}>사용자 과실</option>
|
||||
<option value="other" ${c?.recv_party_type==='other' ?'selected':''}>기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="recvMfrWrap_${rid}" style="display:${c?.recv_party_type==='manufacturer'?'block':'none'}">
|
||||
<label>업체</label>
|
||||
<select id="recvPartyMfr_${rid}">
|
||||
<option value="">선택</option>
|
||||
${mfrs.map(m=>`<option value="${m.id}" ${c?.recv_party_manufacturer_id==m.id?'selected':''}>${m.name}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="recvCustomWrap_${rid}" style="display:${c?.recv_party_type==='other'?'block':'none'}">
|
||||
<label>기타</label>
|
||||
<input type="text" id="recvPartyCustom_${rid}" value="${c?.recv_party_custom||''}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:10px;">
|
||||
<div class="form-group">
|
||||
<label>금액 (원)</label>
|
||||
<input type="number" id="costAmount_${rid}" value="${c?.cost_amount||0}" min="0" step="1000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>처리 상태</label>
|
||||
<select id="costStatus_${rid}">
|
||||
<option value="pending" ${(!c||c.cost_status==='pending') ?'selected':''}>미처리</option>
|
||||
<option value="billed" ${c?.cost_status==='billed' ?'selected':''}>청구완료</option>
|
||||
<option value="waived" ${c?.cost_status==='waived' ?'selected':''}>면제</option>
|
||||
<option value="settled" ${c?.cost_status==='settled' ?'selected':''}>정산완료</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="costErr_${rid}" class="alert alert-danger" style="display:none"></div>
|
||||
<button class="btn btn-primary" onclick="saveCost(${rid})">💾 출장비 저장</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -369,8 +389,19 @@ async function load() {
|
||||
|
||||
<!-- 보기 모드 -->
|
||||
<div class="report-view" id="reportView">
|
||||
${r.report_scope === 'station' ? `
|
||||
<div style="background:#F5F3FF;border:1px solid #DDD6FE;border-radius:8px;padding:9px 14px;margin-bottom:10px;font-size:13px;color:#5B21B6;font-weight:600;">
|
||||
🏢 충전소 전체 신고 · <strong>${r.scope_charger_count}대</strong> 대상
|
||||
</div>` : r.report_scope === 'type' ? `
|
||||
<div style="background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;padding:9px 14px;margin-bottom:10px;font-size:13px;color:#1D4ED8;font-weight:600;">
|
||||
🔧 동일 모델 전체 신고 · <strong>${r.scope_charger_count}대</strong> 대상
|
||||
</div>` : r.report_scope === 'multi' ? `
|
||||
<div style="background:#FEF3C7;border:1px solid #FDE68A;border-radius:8px;padding:9px 14px;margin-bottom:10px;font-size:13px;color:#92400E;font-weight:600;">
|
||||
📋 충전기 ${r.scope_charger_count}대 선택 신고
|
||||
${(r.charger_ids||[]).length > 1 ? `<div style="margin-top:6px;font-size:12px;font-weight:400;display:flex;flex-wrap:wrap;gap:4px">${(r.charger_ids||[]).map(cid=>`<span style="background:#FDE68A;padding:2px 8px;border-radius:10px">${cid}</span>`).join('')}</div>` : ''}
|
||||
</div>` : ''}
|
||||
<table class="no-hover" style="font-size:13px;">
|
||||
<tr><td style="color:var(--gray4);width:100px">충전기 ID</td><td><strong>${r.charger_id}</strong></td></tr>
|
||||
<tr><td style="color:var(--gray4);width:100px">충전기 ID</td><td><strong>${r.charger_id}</strong>${r.report_scope !== 'single' ? ` <span style="font-size:11px;color:var(--gray4)">(대표 충전기)</span>` : ''}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">충전기명</td><td>${r.charger_name || '-'}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">충전소</td><td>${r.station_name || '-'}</td></tr>
|
||||
<tr><td style="color:var(--gray4)">CPO</td><td>${r.cpo_name || '-'}</td></tr>
|
||||
@@ -437,6 +468,60 @@ async function load() {
|
||||
|
||||
<!-- 편집 모드 -->
|
||||
<div class="report-edit" id="reportEdit">
|
||||
<!-- 충전기 변경 -->
|
||||
<div class="form-group" style="margin-bottom:10px">
|
||||
<label style="font-size:12px;font-weight:700;color:var(--navy2)">충전기</label>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 10px;background:var(--gray1);border-radius:6px;font-size:13px;margin-bottom:6px">
|
||||
<span id="rEditChargerCurrent"><strong>${r.charger_id}</strong>${r.station_name ? ` · ${r.station_name}` : ''}</span>
|
||||
<button type="button" onclick="toggleChargerSearch()" class="edit-toggle-btn" style="flex-shrink:0">변경</button>
|
||||
</div>
|
||||
<div id="rEditChargerSearch" style="display:none">
|
||||
<div style="position:relative">
|
||||
<input type="text" id="rEditChargerInput" autocomplete="off"
|
||||
placeholder="충전소명 또는 충전기 ID 검색..."
|
||||
oninput="filterEditChargers(this.value)" onfocus="filterEditChargers(this.value)"
|
||||
style="width:100%;padding:8px 10px;border:1px solid var(--gray3);border-radius:6px;font-size:13px;box-sizing:border-box;outline:none">
|
||||
<div id="rEditChargerDropdown"
|
||||
style="display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;
|
||||
background:white;border:1px solid var(--gray3);border-radius:7px;
|
||||
max-height:200px;overflow-y:auto;z-index:20;box-shadow:0 4px 12px rgba(0,0,0,.12)"></div>
|
||||
</div>
|
||||
<div id="rEditChargerSelected"
|
||||
style="display:none;margin-top:6px;padding:6px 10px;background:#EFF6FF;
|
||||
border:1px solid #BFDBFE;border-radius:6px;font-size:12px;color:var(--navy2);">
|
||||
<span id="rEditChargerSelectedText"></span>
|
||||
<button type="button" onclick="clearEditCharger()" style="float:right;background:none;border:none;cursor:pointer;color:var(--gray4);font-size:13px;padding:0 2px;margin-left:8px">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 신고 범위 -->
|
||||
<div class="form-group" style="margin-bottom:10px">
|
||||
<label style="font-size:12px;font-weight:700;color:var(--navy2)">신고 범위</label>
|
||||
<div style="display:flex;flex-direction:column;gap:7px;margin-top:5px">
|
||||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">
|
||||
<input type="radio" name="rEditScope" value="single" style="width:auto;accent-color:var(--accent)"
|
||||
${!r.report_scope || r.report_scope === 'single' ? 'checked' : ''}>
|
||||
<span><strong>이 충전기만</strong></span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">
|
||||
<input type="radio" name="rEditScope" value="station" style="width:auto;accent-color:var(--accent)"
|
||||
${r.report_scope === 'station' ? 'checked' : ''}>
|
||||
<span><strong>충전소 전체</strong> <span style="font-size:11px;color:var(--gray4)">— 같은 충전소의 모든 충전기 대상</span></span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">
|
||||
<input type="radio" name="rEditScope" value="type" style="width:auto;accent-color:var(--accent)"
|
||||
${r.report_scope === 'type' ? 'checked' : ''}>
|
||||
<span><strong>동일 모델 전체</strong> <span style="font-size:11px;color:var(--gray4)">— 같은 충전기 종류 전체 대상</span></span>
|
||||
</label>
|
||||
${r.report_scope === 'multi' ? `
|
||||
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">
|
||||
<input type="radio" name="rEditScope" value="multi" style="width:auto;accent-color:var(--accent)" checked>
|
||||
<span><strong>선택 충전기 ${r.scope_charger_count}대</strong> <span style="font-size:11px;color:var(--gray4)">— 접수 시 선택한 충전기들</span></span>
|
||||
</label>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:10px">
|
||||
<label style="font-size:12px;font-weight:700;color:var(--navy2)">문제 유형 <span class="req">*</span></label>
|
||||
<div class="issue-chk-grid">
|
||||
@@ -545,6 +630,7 @@ async function load() {
|
||||
${(pr.photos_after||[]).length ? `<label style="font-size:12px;font-weight:700;color:var(--navy2);margin-top:8px;display:block">조치 후 사진</label>
|
||||
<div class="photo-preview">${(pr.photos_after||[]).map(p=>`<img src="${p}" onclick="window.open('${p}')" style="cursor:zoom-in">`).join('')}</div>` : ''}
|
||||
</div>` : ''}
|
||||
${buildCostHtml(pr, manufacturers)}
|
||||
</div>`).join('');
|
||||
})() : ''}
|
||||
|
||||
@@ -592,6 +678,7 @@ async function load() {
|
||||
</div>
|
||||
</div>
|
||||
${renderLocationMap(repair)}
|
||||
${buildCostHtml(repair, manufacturers)}
|
||||
|
||||
${/* ── 연결된 개선항목 표시 (승인 완료 후) ── */
|
||||
repair.linked_improvements && repair.linked_improvements.length ? `
|
||||
@@ -678,7 +765,7 @@ async function load() {
|
||||
<select id="impMfr">
|
||||
<option value="">미지정 (나중에 설정)</option>
|
||||
${manufacturers.map(m =>
|
||||
`<option value="${m.id}">${m.company ? m.company+' / ' : ''}${m.name}</option>`
|
||||
`<option value="${m.id}">${m.name}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
@@ -695,7 +782,6 @@ async function load() {
|
||||
</div>
|
||||
|
||||
|
||||
${costHtml}
|
||||
`;
|
||||
|
||||
// 신고 편집 폼 사진 압축 설정
|
||||
@@ -705,11 +791,6 @@ async function load() {
|
||||
|
||||
// 지도 초기화 (수리 정보가 있을 때만)
|
||||
if (repair) initRepairMap(repair);
|
||||
|
||||
// 폼이 처음부터 열려 있는 경우 (미처리) max-height 설정
|
||||
if (!editOpen) return;
|
||||
const wrap = document.getElementById('costEditWrap');
|
||||
if (wrap) wrap.style.maxHeight = wrap.scrollHeight + 2000 + 'px';
|
||||
}
|
||||
|
||||
/* ── 방문 위치 지도 ── */
|
||||
@@ -829,6 +910,7 @@ function toggleReportEdit() {
|
||||
edit.classList.remove('active');
|
||||
view.classList.remove('hidden');
|
||||
btn.innerHTML = '✏️ 내용 수정';
|
||||
clearEditCharger();
|
||||
} else {
|
||||
view.classList.add('hidden');
|
||||
edit.classList.add('active');
|
||||
@@ -836,6 +918,79 @@ function toggleReportEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 충전기 변경 (수정 폼) ── */
|
||||
let editChargerList = [];
|
||||
let editChargerFiltered = [];
|
||||
let editNewChargerId = null;
|
||||
|
||||
function _ecEsc(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function toggleChargerSearch() {
|
||||
const wrap = document.getElementById('rEditChargerSearch');
|
||||
if (!wrap) return;
|
||||
const opening = wrap.style.display === 'none';
|
||||
wrap.style.display = opening ? 'block' : 'none';
|
||||
if (opening) {
|
||||
if (!editChargerList.length) {
|
||||
API.get('/chargers').then(cs => { editChargerList = cs; filterEditChargers(''); });
|
||||
} else {
|
||||
filterEditChargers('');
|
||||
}
|
||||
setTimeout(() => document.getElementById('rEditChargerInput')?.focus(), 50);
|
||||
}
|
||||
}
|
||||
|
||||
function filterEditChargers(q) {
|
||||
const dd = document.getElementById('rEditChargerDropdown');
|
||||
if (!dd) return;
|
||||
q = (q || '').trim().toLowerCase();
|
||||
editChargerFiltered = (q
|
||||
? editChargerList.filter(c =>
|
||||
c.station_name.toLowerCase().includes(q) ||
|
||||
c.name.toLowerCase().includes(q) ||
|
||||
c.id.toLowerCase().includes(q))
|
||||
: editChargerList).slice(0, 50);
|
||||
dd.style.display = editChargerFiltered.length ? 'block' : 'none';
|
||||
dd.innerHTML = editChargerFiltered.map((c, i) => `
|
||||
<div class="ec-opt" onclick="selectEditCharger(${i})">
|
||||
<div style="font-weight:600;color:var(--navy)">${_ecEsc(c.station_name)} · ${_ecEsc(c.name)}</div>
|
||||
<div style="font-size:11px;color:var(--gray4);margin-top:2px">${_ecEsc(c.id)}</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function selectEditCharger(idx) {
|
||||
const c = editChargerFiltered[idx];
|
||||
if (!c) return;
|
||||
editNewChargerId = c.id;
|
||||
document.getElementById('rEditChargerDropdown').style.display = 'none';
|
||||
document.getElementById('rEditChargerInput').value = '';
|
||||
document.getElementById('rEditChargerSelectedText').textContent =
|
||||
`${c.station_name} · ${c.name} (${c.id})`;
|
||||
document.getElementById('rEditChargerSelected').style.display = 'block';
|
||||
}
|
||||
|
||||
function clearEditCharger() {
|
||||
editNewChargerId = null;
|
||||
const sel = document.getElementById('rEditChargerSelected');
|
||||
if (sel) sel.style.display = 'none';
|
||||
const inp = document.getElementById('rEditChargerInput');
|
||||
if (inp) inp.value = '';
|
||||
const dd = document.getElementById('rEditChargerDropdown');
|
||||
if (dd) dd.style.display = 'none';
|
||||
const wrap = document.getElementById('rEditChargerSearch');
|
||||
if (wrap) wrap.style.display = 'none';
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const inp = document.getElementById('rEditChargerInput');
|
||||
const dd = document.getElementById('rEditChargerDropdown');
|
||||
if (inp && dd && !inp.contains(e.target) && !dd.contains(e.target)) {
|
||||
dd.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
async function saveReport(reportId) {
|
||||
const issues = [...document.querySelectorAll('.r-issue-chk:checked')].map(c => c.value);
|
||||
if (!issues.length) {
|
||||
@@ -844,6 +999,14 @@ async function saveReport(reportId) {
|
||||
err.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
// 재조치 대기 중인데 상태를 pending 이외로 바꾸면 정비사 목록에서 사라짐 → 경고
|
||||
const selStatus = document.getElementById('rEditStatus').value;
|
||||
if (selStatus !== 'pending') {
|
||||
const latestRepair = (window._reportData && window._reportData.repair);
|
||||
if (latestRepair && latestRepair.re_dispatch_requested && !latestRepair.approved_at) {
|
||||
if (!confirm('⚠️ 현재 재조치 대기 중인 건입니다.\n상태를 "접수(pending)" 이외로 변경하면 정비사 AS 목록에서 사라집니다.\n\n계속 저장하시겠습니까?')) return;
|
||||
}
|
||||
}
|
||||
document.getElementById('rEditErr').style.display = 'none';
|
||||
const fd = new FormData();
|
||||
fd.append('issue_types', JSON.stringify(issues));
|
||||
@@ -851,8 +1014,11 @@ async function saveReport(reportId) {
|
||||
fd.append('error_code', document.getElementById('rEditErrorCode').value);
|
||||
fd.append('contact', document.getElementById('rEditContact').value);
|
||||
fd.append('occurred_at', document.getElementById('rEditOccurred').value);
|
||||
fd.append('status', document.getElementById('rEditStatus').value);
|
||||
fd.append('status', selStatus);
|
||||
fd.append('ocpp_log', document.getElementById('rEditOcppLog').value);
|
||||
if (editNewChargerId) fd.append('charger_id', editNewChargerId);
|
||||
const newScope = document.querySelector('input[name="rEditScope"]:checked')?.value;
|
||||
if (newScope) fd.append('scope', newScope);
|
||||
const newPhotos = document.getElementById('rEditPhoto')?.files || [];
|
||||
Array.from(newPhotos).forEach(f => fd.append('photos', f));
|
||||
try {
|
||||
@@ -865,11 +1031,7 @@ async function saveReport(reportId) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleParty() {
|
||||
const v = document.getElementById('partyType').value;
|
||||
document.getElementById('mfrWrap').style.display = v === 'manufacturer' ? 'block' : 'none';
|
||||
document.getElementById('customWrap').style.display = v === 'other' ? 'block' : 'none';
|
||||
}
|
||||
function toggleParty() {} // 레거시 — togglePartySelect(repairId) 사용
|
||||
|
||||
function toggleApprovePanel() {
|
||||
const panel = document.getElementById('approvePanel');
|
||||
@@ -985,28 +1147,31 @@ async function submitClose(id) {
|
||||
}
|
||||
|
||||
async function saveCost(repairId) {
|
||||
const partyType = document.getElementById('partyType').value;
|
||||
if (!partyType) { showCostErr('출장비 부담 주체를 선택해 주세요.'); return; }
|
||||
const partyType = document.getElementById('partyType_' + repairId)?.value;
|
||||
if (!partyType) {
|
||||
const err = document.getElementById('costErr_' + repairId);
|
||||
if (err) { err.textContent = '출장비 부담 주체를 선택해 주세요.'; err.style.display = 'block'; }
|
||||
return;
|
||||
}
|
||||
const fd = new FormData();
|
||||
fd.append('root_cause', document.getElementById('rootCause').value);
|
||||
fd.append('admin_note', document.getElementById('adminNote').value);
|
||||
fd.append('cost_party_type', partyType);
|
||||
fd.append('cost_party_manufacturer_id', document.getElementById('partyMfr')?.value || '');
|
||||
fd.append('cost_party_custom', document.getElementById('partyCustom')?.value || '');
|
||||
fd.append('cost_amount', document.getElementById('costAmount').value || 0);
|
||||
fd.append('cost_status', document.getElementById('costStatus').value);
|
||||
fd.append('root_cause', document.getElementById('rootCause_' + repairId)?.value || '');
|
||||
fd.append('admin_note', document.getElementById('adminNote_' + repairId)?.value || '');
|
||||
fd.append('cost_party_type', partyType);
|
||||
fd.append('cost_party_manufacturer_id', document.getElementById('partyMfr_' + repairId)?.value || '');
|
||||
fd.append('cost_party_custom', document.getElementById('partyCustom_' + repairId)?.value || '');
|
||||
fd.append('recv_party_type', document.getElementById('recvPartyType_' + repairId)?.value || '');
|
||||
fd.append('recv_party_manufacturer_id', document.getElementById('recvPartyMfr_' + repairId)?.value || '');
|
||||
fd.append('recv_party_custom', document.getElementById('recvPartyCustom_'+ repairId)?.value || '');
|
||||
fd.append('cost_amount', document.getElementById('costAmount_' + repairId)?.value || 0);
|
||||
fd.append('cost_status', document.getElementById('costStatus_' + repairId)?.value || 'pending');
|
||||
try {
|
||||
await API.post(`/costs/repair/${repairId}`, fd);
|
||||
alert('✅ 출장비 처리가 저장되었습니다.');
|
||||
editOpen = false;
|
||||
load();
|
||||
} catch(e) { showCostErr(e.message); }
|
||||
}
|
||||
|
||||
function showCostErr(msg) {
|
||||
const el = document.getElementById('costErr');
|
||||
el.textContent = msg;
|
||||
el.style.display = 'block';
|
||||
} catch(e) {
|
||||
const err = document.getElementById('costErr_' + repairId);
|
||||
if (err) { err.textContent = e.message; err.style.display = 'block'; }
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
@@ -40,9 +40,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
|
||||
<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">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">AS 관리</div>
|
||||
<a href="/pages/admin/dashboard.html">📊 대시보드</a>
|
||||
<a href="/pages/admin/reports.html" class="active">📋 신고 목록</a>
|
||||
@@ -54,6 +55,7 @@
|
||||
<a href="/pages/admin/issue-types.html">📝 유형관리</a>
|
||||
<a href="/pages/admin/qr.html">📷 QR 생성</a>
|
||||
<a href="/pages/admin/accounts.html">👥 계정 관리</a>
|
||||
<a href="/pages/admin/export.html">📥 데이터 내보내기</a>
|
||||
<a href="/pages/admin/settings.html">⚙️ 설정</a>
|
||||
</div>
|
||||
<div class="main">
|
||||
@@ -74,6 +76,7 @@
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;">
|
||||
<select id="fStatus" style="width:auto">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="pending_all">접수 대기 (전체)</option>
|
||||
<option value="pending_approval">승인대기</option>
|
||||
<option value="pending">접수</option>
|
||||
<option value="in_progress">처리중</option>
|
||||
@@ -130,8 +133,9 @@ let mapMarkers = [];
|
||||
|
||||
// ── URL 파라미터 초기값 ──
|
||||
const _p = new URLSearchParams(location.search);
|
||||
if (_p.get('status')) document.getElementById('fStatus').value = _p.get('status');
|
||||
if (_p.get('charger_id')) document.getElementById('fCharger').value = _p.get('charger_id');
|
||||
if (_p.get('status')) document.getElementById('fStatus').value = _p.get('status');
|
||||
if (_p.get('charger_id')) document.getElementById('fCharger').value = _p.get('charger_id');
|
||||
let _stationNameFilter = _p.get('station_name') || '';
|
||||
|
||||
// ── 뷰 전환 ──
|
||||
function setView(v) {
|
||||
@@ -189,8 +193,23 @@ async function load() {
|
||||
const c = document.getElementById('fCharger').value.trim();
|
||||
if (s) url += 'status=' + s + '&';
|
||||
if (c) url += 'charger_id=' + c + '&';
|
||||
if (_stationNameFilter) url += 'station_name=' + encodeURIComponent(_stationNameFilter) + '&';
|
||||
allRows = await API.get(url);
|
||||
|
||||
// 충전소 필터 배너
|
||||
const existing = document.getElementById('stationFilterBanner');
|
||||
if (_stationNameFilter) {
|
||||
if (!existing) {
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'stationFilterBanner';
|
||||
banner.style.cssText = 'background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;padding:8px 14px;margin-bottom:12px;font-size:13px;color:var(--navy2);display:flex;justify-content:space-between;align-items:center;';
|
||||
banner.innerHTML = `<span>🏢 충전소 필터: <strong>${_stationNameFilter}</strong></span><button onclick="_stationNameFilter='';this.closest('#stationFilterBanner').remove();load()" style="background:none;border:none;cursor:pointer;color:var(--gray4);font-size:13px;">✕ 해제</button>`;
|
||||
document.querySelector('.main').insertBefore(banner, document.querySelector('.card'));
|
||||
}
|
||||
} else if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
document.getElementById('resultCount').textContent = allRows.length + '건';
|
||||
renderTable();
|
||||
if (curView === 'map') renderReportMap();
|
||||
@@ -209,7 +228,10 @@ function renderTable() {
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${r.id}'" style="cursor:pointer">
|
||||
<span style="font-weight:700">${r.seq}</span>
|
||||
</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${r.id}'" style="cursor:pointer"><strong>${r.charger_id}</strong></td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${r.id}'" style="cursor:pointer">
|
||||
<strong>${r.charger_id}</strong>
|
||||
${r.report_scope === 'station' ? `<div style="font-size:11px;color:#7C3AED;font-weight:600;margin-top:2px">🏢 충전소 전체 · ${r.scope_charger_count}대</div>` : r.report_scope === 'type' ? `<div style="font-size:11px;color:#0369A1;font-weight:600;margin-top:2px">🔧 동일모델 전체 · ${r.scope_charger_count}대</div>` : r.report_scope === 'multi' ? `<div style="font-size:11px;color:#B45309;font-weight:600;margin-top:2px">📋 충전기 ${r.scope_charger_count}대 선택</div>` : ''}
|
||||
</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${r.id}'" style="cursor:pointer">${r.station_name||'-'}</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${r.id}'" style="cursor:pointer">${r.charger_type||'-'}</td>
|
||||
<td onclick="location.href='/pages/admin/report-detail.html?id=${r.id}'" style="cursor:pointer;max-width:200px">${(r.issue_types||[]).join(', ')}</td>
|
||||
|
||||
@@ -25,9 +25,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav"><span class="nav-brand">⚡ EV AS 관리 — 관리자</span><div id="navUser"></div></nav>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<!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>
|
||||
<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">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">메뉴</div>
|
||||
<a href="/pages/manufacturer/dashboard.html" class="active">📋 개선항목 목록</a>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!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>
|
||||
<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="main" style="max-width:760px;margin:0 auto;">
|
||||
<div style="margin-bottom:14px;display:flex;gap:10px;align-items:center;">
|
||||
<a href="/pages/manufacturer/dashboard.html" class="btn btn-outline btn-sm">← 목록</a>
|
||||
|
||||
@@ -47,16 +47,17 @@
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리</span>
|
||||
<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="mech-tab-bar">
|
||||
<a href="/pages/mechanic/dashboard.html" class="active">📋<span>AS 목록</span></a>
|
||||
<a href="/pages/mechanic/scan.html">📷<span>QR 스캔</span></a>
|
||||
<a href="/pages/mechanic/history.html">🗂<span>처리 이력</span></a>
|
||||
</div>
|
||||
<div class="layout">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">메뉴</div>
|
||||
<a href="/pages/mechanic/dashboard.html" class="active">📋 AS 목록</a>
|
||||
<a href="/pages/mechanic/scan.html">📷 QR 스캔</a>
|
||||
@@ -157,7 +158,7 @@ function renderList() {
|
||||
: `/pages/mechanic/repair.html?charger_id=${r.charger_id}&report_id=${r.id}`;
|
||||
return `
|
||||
<tr onclick="location.href='${href}'">
|
||||
<td>#${r.id}${r.re_dispatch_count > 0 ? ' <span style="font-size:10px;background:#FEF3C7;color:#92400E;padding:1px 6px;border-radius:8px;font-weight:700;vertical-align:middle;">🔁재조치</span>' : ''}</td>
|
||||
<td>#${r.id}${r.re_dispatch_count > 0 ? ' <span style="font-size:10px;background:#FEF3C7;color:#92400E;padding:1px 6px;border-radius:8px;font-weight:700;vertical-align:middle;">🔁 ' + (r.re_dispatch_count + 1) + '차 조치</span>' : ''}</td>
|
||||
<td><strong>${r.charger_id}</strong><br><small>${r.charger_name||''}</small></td>
|
||||
<td>${r.station_name||'-'}</td>
|
||||
<td>${r.charger_type||'-'}</td>
|
||||
|
||||
@@ -28,16 +28,17 @@
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리</span>
|
||||
<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="mech-tab-bar">
|
||||
<a href="/pages/mechanic/dashboard.html">📋<span>AS 목록</span></a>
|
||||
<a href="/pages/mechanic/scan.html">📷<span>QR 스캔</span></a>
|
||||
<a href="/pages/mechanic/history.html" class="active">🗂<span>처리 이력</span></a>
|
||||
</div>
|
||||
<div class="layout">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">메뉴</div>
|
||||
<a href="/pages/mechanic/dashboard.html">📋 AS 목록</a>
|
||||
<a href="/pages/mechanic/scan.html">📷 QR 스캔</a>
|
||||
@@ -136,6 +137,7 @@ function render() {
|
||||
<span class="${isApproved ? 'badge-approved' : 'badge-pending'}">
|
||||
${isApproved ? '✅ 승인완료' : '⏳ 승인대기'}
|
||||
</span>
|
||||
${r.attempt > 1 ? `<span style="font-size:11px;background:#FEF3C7;color:#92400E;padding:2px 8px;border-radius:10px;font-weight:700;">🔁 ${r.attempt}차 조치</span>` : ''}
|
||||
<span style="font-size:11px;color:var(--gray4)">${RESULT_LABEL[r.result_status] || r.result_status}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,20 +10,29 @@
|
||||
.photo-preview{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px;}
|
||||
.photo-preview img{width:72px;height:72px;object-fit:cover;border-radius:6px;border:1px solid var(--gray3);}
|
||||
.photo-info{font-size:11px;margin-top:4px;min-height:14px;color:var(--gray4);}
|
||||
@media(max-width:768px){
|
||||
.upload-area{padding:16px 12px;font-size:13px;}
|
||||
.photo-preview img{width:88px;height:88px;}
|
||||
.main > div{max-width:100% !important;padding:0;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리</span>
|
||||
<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="mech-tab-bar">
|
||||
<a href="/pages/mechanic/dashboard.html">📋<span>AS 목록</span></a>
|
||||
<a href="/pages/mechanic/scan.html">📷<span>QR 스캔</span></a>
|
||||
<a href="/pages/mechanic/history.html">🗂<span>처리 이력</span></a>
|
||||
</div>
|
||||
<div class="layout">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">메뉴</div>
|
||||
<a href="/pages/mechanic/dashboard.html">📋 AS 목록</a>
|
||||
<a href="/pages/mechanic/scan.html">📷 QR 스캔</a>
|
||||
@@ -42,7 +51,8 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">🔧 조치 내역 입력</div>
|
||||
<div class="card-title" id="repairCardTitle">🔧 조치 내역 입력</div>
|
||||
<div id="attemptBanner"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>조치 유형 <span class="req">*</span></label>
|
||||
@@ -125,6 +135,13 @@
|
||||
Auth.require(['mechanic','admin']);
|
||||
Auth.renderNav(document.getElementById('navUser'));
|
||||
|
||||
function toggleSidebar() {
|
||||
const s = document.getElementById('sidebar');
|
||||
const o = document.getElementById('navOverlay');
|
||||
if (s) s.classList.toggle('mobile-open');
|
||||
if (o) o.classList.toggle('show');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const repairId = params.get('repair_id'); // 편집 모드
|
||||
const chargerId = params.get('charger_id'); // 신규 모드
|
||||
@@ -185,13 +202,24 @@ async function loadCreate() {
|
||||
' onchange="toggleReport(' + r.id + ',this.checked,this.closest(\'label\'))">' +
|
||||
'<div>' +
|
||||
'<div><strong>#' + r.id + '</strong> ' + Auth.statusBadge(r.status) +
|
||||
(r.re_dispatch_count > 0 ? ' <span style="font-size:11px;background:#FEF3C7;color:#92400E;padding:1px 7px;border-radius:8px;font-weight:700;">🔁 재조치 ' + r.re_dispatch_count + '회</span>' : '') + '</div>' +
|
||||
(r.re_dispatch_count > 0 ? ' <span style="font-size:11px;background:#FEF3C7;color:#92400E;padding:1px 7px;border-radius:8px;font-weight:700;">🔁 ' + (r.re_dispatch_count + 1) + '차 조치</span>' : '') + '</div>' +
|
||||
'<div style="font-size:12px;color:var(--text2);margin-top:2px">' + ((r.issue_types || []).join(', ')) + '</div>' +
|
||||
'<div style="font-size:11px;color:var(--gray4)">' + Auth.fmtDt(r.reported_at) + '</div>' +
|
||||
photoHtml +
|
||||
'</div>' +
|
||||
'</label>';
|
||||
}).join('');
|
||||
|
||||
// 차수 배너: 대상 신고(initReportId 또는 첫 번째)의 re_dispatch_count 기준
|
||||
var targetReport = reports.find(function(r) { return r.id === parseInt(initReportId); }) || reports[0];
|
||||
if (targetReport && targetReport.re_dispatch_count > 0) {
|
||||
var nth = targetReport.re_dispatch_count + 1;
|
||||
document.getElementById('repairCardTitle').textContent = '🔧 조치 내역 입력 (' + nth + '차 조치)';
|
||||
document.getElementById('attemptBanner').innerHTML =
|
||||
'<div style="background:#FFF7E6;border:1px solid #F59E0B;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:13px;font-weight:600;color:#92400E;">' +
|
||||
'🔁 이 건은 <strong>' + nth + '차 조치</strong> 대상입니다. (이전 조치 후 관리자 재조치 요청됨)' +
|
||||
'</div>';
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('chargerCard').innerHTML =
|
||||
'<div class="alert alert-danger">충전기 정보를 불러오지 못했습니다.<br><small style="opacity:.8">' + e.message + '</small></div>';
|
||||
@@ -214,8 +242,18 @@ async function loadEdit() {
|
||||
// 헤더 업데이트
|
||||
var h2el = document.querySelector('.main > div > h2') || document.querySelector('h2');
|
||||
if (h2el) h2el.parentNode.removeChild(h2el);
|
||||
const attemptLabel = repair.attempt > 1 ? ` · ${repair.attempt}차 조치` : '';
|
||||
document.querySelector('a.btn-outline.btn-sm').insertAdjacentHTML('afterend',
|
||||
`<span style="font-size:17px;font-weight:700;color:var(--navy)">조치 수정 #${repair.id}</span>`);
|
||||
`<span style="font-size:17px;font-weight:700;color:var(--navy)">조치 수정 #${repair.id}${attemptLabel}</span>`);
|
||||
|
||||
// 차수 배너 (2차 이상일 때)
|
||||
if (repair.attempt > 1) {
|
||||
document.getElementById('repairCardTitle').textContent = `🔧 조치 내역 입력 (${repair.attempt}차 조치)`;
|
||||
document.getElementById('attemptBanner').innerHTML =
|
||||
`<div style="background:#FFF7E6;border:1px solid #F59E0B;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:13px;font-weight:600;color:#92400E;">` +
|
||||
`🔁 이 건은 <strong>${repair.attempt}차 조치</strong> 대상입니다. (이전 조치 후 관리자 재조치 요청됨)` +
|
||||
`</div>`;
|
||||
}
|
||||
|
||||
// 충전기 카드
|
||||
document.getElementById('chargerCard').innerHTML = `
|
||||
|
||||
@@ -10,16 +10,17 @@
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리</span>
|
||||
<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="mech-tab-bar">
|
||||
<a href="/pages/mechanic/dashboard.html">📋<span>AS 목록</span></a>
|
||||
<a href="/pages/mechanic/scan.html" class="active">📷<span>QR 스캔</span></a>
|
||||
<a href="/pages/mechanic/history.html">🗂<span>처리 이력</span></a>
|
||||
</div>
|
||||
<div class="layout">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">메뉴</div>
|
||||
<a href="/pages/mechanic/dashboard.html">📋 AS 목록</a>
|
||||
<a href="/pages/mechanic/scan.html" class="active">📷 QR 스캔</a>
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리 <span class="ro-badge">👁 읽기전용</span></span>
|
||||
<div style="display:flex;align-items:center;gap:2px;"><button class="nav-hamburger" onclick="toggleSidebar()">☰</button><span class="nav-brand">⚡ EV AS 관리 <span class="ro-badge">👁 읽기전용</span></span></div>
|
||||
<div id="navUser"></div>
|
||||
</nav>
|
||||
<div class="mobile-nav-overlay" id="navOverlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">메뉴</div>
|
||||
<a href="/pages/observer/dashboard.html" class="active">📊 현황 대시보드</a>
|
||||
<a href="/pages/observer/reports.html">📋 신고 목록</a>
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<span class="nav-brand">⚡ EV AS 관리 <span class="ro-badge">👁 읽기전용</span></span>
|
||||
<div style="display:flex;align-items:center;gap:2px;"><button class="nav-hamburger" onclick="toggleSidebar()">☰</button><span class="nav-brand">⚡ EV AS 관리 <span class="ro-badge">👁 읽기전용</span></span></div>
|
||||
<div id="navUser"></div>
|
||||
</nav>
|
||||
<div class="mobile-nav-overlay" id="navOverlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<div class="layout">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-section">메뉴</div>
|
||||
<a href="/pages/observer/dashboard.html">📊 현황 대시보드</a>
|
||||
<a href="/pages/observer/reports.html" class="active">📋 신고 목록</a>
|
||||
|
||||
Reference in New Issue
Block a user