Files
tk-factory-services/tkpurchase/web/static/js/tkpurchase-workreport-summary.js
Hyungi Ahn 976e55d672 feat(tkpurchase): 업무현황 다건 입력 + 작업자 시간 추적 + 종합 페이지
- DB: 유니크 제약 제거, report_seq 컬럼, work_report_workers 테이블
- API: 트랜잭션 기반 다건 생성/수정, 작업자 CRUD, 요약/엑셀 엔드포인트
- 협력업체 포탈: 다건 보고 UI, 작업자+시간 입력(자동완성), 수정 기능
- 업무현황 페이지: 보고순번/작업자 상세 표시
- 종합 페이지(NEW): 업체별/프로젝트별 취합, 엑셀 추출

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:43:33 +09:00

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 = firstDay.toISOString().substring(0, 10);
document.getElementById('filterDateTo').value = now.toISOString().substring(0, 10);
loadCompaniesForFilter();
loadSchedulesForFilter();
loadSummary();
}