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