1차완료
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user