- API에 Cache-Control: no-store 미들웨어 추가 (304 캐시 문제 해결) - toLocalDate() 유틸 추가, 전체 8개 JS의 toISOString 타임존 버그 수정 - scheduleModel.findAll에 total COUNT 추가, 컨트롤러에서 total 반환 - HTML 캐시 버스팅 ?v=2026031601 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
5.9 KiB
JavaScript
150 lines
5.9 KiB
JavaScript
/* tkpurchase-workreport-summary.js - Work report summary page */
|
|
|
|
async function loadCompaniesForFilter() {
|
|
try {
|
|
const r = await api('/partners?limit=100');
|
|
const list = r.data || [];
|
|
const sel = document.getElementById('filterCompany');
|
|
list.forEach(c => {
|
|
const opt = document.createElement('option');
|
|
opt.value = c.id;
|
|
opt.textContent = c.company_name;
|
|
sel.appendChild(opt);
|
|
});
|
|
} catch(e) { console.warn('Load companies error:', e); }
|
|
}
|
|
|
|
async function loadSchedulesForFilter() {
|
|
try {
|
|
const r = await api('/schedules?limit=200');
|
|
const list = r.data || [];
|
|
const sel = document.getElementById('filterSchedule');
|
|
list.forEach(s => {
|
|
const opt = document.createElement('option');
|
|
opt.value = s.id;
|
|
opt.textContent = (s.workplace_name || '') + ' - ' + (s.work_description || '').substring(0, 30);
|
|
sel.appendChild(opt);
|
|
});
|
|
} catch(e) { console.warn('Load schedules error:', e); }
|
|
}
|
|
|
|
function getFilterQuery() {
|
|
const companyId = document.getElementById('filterCompany').value;
|
|
const scheduleId = document.getElementById('filterSchedule').value;
|
|
const dateFrom = document.getElementById('filterDateFrom').value;
|
|
const dateTo = document.getElementById('filterDateTo').value;
|
|
|
|
if (!dateFrom || !dateTo) {
|
|
showToast('기간을 선택해주세요', 'error');
|
|
return null;
|
|
}
|
|
|
|
let query = `?date_from=${dateFrom}&date_to=${dateTo}`;
|
|
if (companyId) query += '&company_id=' + companyId;
|
|
if (scheduleId) query += '&schedule_id=' + scheduleId;
|
|
return query;
|
|
}
|
|
|
|
async function loadSummary() {
|
|
const query = getFilterQuery();
|
|
if (!query) return;
|
|
|
|
const tbody = document.getElementById('summaryTableBody');
|
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-8">로딩 중...</td></tr>';
|
|
|
|
try {
|
|
const r = await api('/work-reports/summary' + query);
|
|
renderSummaryTable(r.data || []);
|
|
} catch(e) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-red-400 py-8">' + escapeHtml(e.message || '로딩 실패') + '</td></tr>';
|
|
}
|
|
}
|
|
|
|
function renderSummaryTable(list) {
|
|
const tbody = document.getElementById('summaryTableBody');
|
|
if (!list.length) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-8">데이터가 없습니다</td></tr>';
|
|
return;
|
|
}
|
|
|
|
let totalReports = 0, totalWorkers = 0, totalHours = 0;
|
|
|
|
tbody.innerHTML = list.map(r => {
|
|
totalReports += parseInt(r.report_count) || 0;
|
|
totalWorkers += parseInt(r.total_workers) || 0;
|
|
totalHours += parseFloat(r.total_hours) || 0;
|
|
|
|
const progressColor = r.avg_progress >= 80 ? 'bg-emerald-500' : r.avg_progress >= 50 ? 'bg-blue-500' : r.avg_progress >= 20 ? 'bg-amber-500' : 'bg-red-500';
|
|
const confirmText = r.confirmed_count + '/' + r.report_count;
|
|
|
|
return `<tr class="hover:bg-gray-50">
|
|
<td>${formatDate(r.report_date)}</td>
|
|
<td class="font-medium">${escapeHtml(r.company_name || '')}</td>
|
|
<td class="text-sm">${escapeHtml(r.workplace_name || '')}</td>
|
|
<td class="text-center">${r.report_count}건</td>
|
|
<td class="text-center">${r.total_workers}명</td>
|
|
<td class="text-center">${Number(r.total_hours).toFixed(1)}h</td>
|
|
<td class="text-center">
|
|
<div class="flex items-center gap-2">
|
|
<div class="flex-1 bg-gray-200 rounded-full h-2 min-w-[2rem]">
|
|
<div class="${progressColor} rounded-full h-2" style="width: ${r.avg_progress || 0}%"></div>
|
|
</div>
|
|
<span class="text-xs text-gray-500">${r.avg_progress || 0}%</span>
|
|
</div>
|
|
</td>
|
|
<td class="text-center">
|
|
<span class="${parseInt(r.confirmed_count) === parseInt(r.report_count) ? 'text-emerald-600' : 'text-amber-500'} text-sm">${confirmText}</span>
|
|
</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
// 합계 행
|
|
tbody.insertAdjacentHTML('beforeend', `<tr class="bg-gray-50 font-semibold border-t-2">
|
|
<td colspan="3" class="text-right">합계</td>
|
|
<td class="text-center">${totalReports}건</td>
|
|
<td class="text-center">${totalWorkers}명</td>
|
|
<td class="text-center">${totalHours.toFixed(1)}h</td>
|
|
<td colspan="2"></td>
|
|
</tr>`);
|
|
}
|
|
|
|
function exportExcel() {
|
|
const query = getFilterQuery();
|
|
if (!query) return;
|
|
|
|
const token = getToken();
|
|
const url = API_BASE + '/work-reports/export' + query;
|
|
|
|
// fetch로 다운로드 (인증 헤더 포함)
|
|
fetch(url, { headers: { 'Authorization': token ? 'Bearer ' + token : '' } })
|
|
.then(res => {
|
|
if (!res.ok) return res.json().then(d => { throw new Error(d.error || '다운로드 실패'); });
|
|
return res.blob();
|
|
})
|
|
.then(blob => {
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
const dateFrom = document.getElementById('filterDateFrom').value;
|
|
const dateTo = document.getElementById('filterDateTo').value;
|
|
a.download = '업무현황_' + dateFrom + '_' + dateTo + '.xlsx';
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
showToast('엑셀 다운로드 완료');
|
|
})
|
|
.catch(e => showToast(e.message || '다운로드 실패', 'error'));
|
|
}
|
|
|
|
function initSummaryPage() {
|
|
if (!initAuth()) return;
|
|
|
|
// 기본 기간: 이번 달
|
|
const now = new Date();
|
|
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
document.getElementById('filterDateFrom').value = toLocalDate(firstDay);
|
|
document.getElementById('filterDateTo').value = toLocalDate(now);
|
|
|
|
loadCompaniesForFilter();
|
|
loadSchedulesForFilter();
|
|
loadSummary();
|
|
}
|