Files
tk-factory-services/tkpurchase/web/static/js/tkpurchase-workreport.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

220 lines
11 KiB
JavaScript

/* tkpurchase-workreport.js - Work report monitoring */
let reportPage = 1;
const reportLimit = 20;
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 loadReports() {
const companyId = document.getElementById('filterCompany').value;
const dateFrom = document.getElementById('filterDateFrom').value;
const dateTo = document.getElementById('filterDateTo').value;
const confirmed = document.getElementById('filterConfirmed').value;
let query = `?page=${reportPage}&limit=${reportLimit}`;
if (companyId) query += '&company_id=' + companyId;
if (dateFrom) query += '&date_from=' + dateFrom;
if (dateTo) query += '&date_to=' + dateTo;
if (confirmed) query += '&confirmed=' + confirmed;
try {
const r = await api('/work-reports' + query);
renderReportTable(r.data || [], r.total || 0);
} catch(e) {
console.warn('Report load error:', e);
document.getElementById('reportTableBody').innerHTML = '<tr><td colspan="9" class="text-center text-red-400 py-8">로딩 실패</td></tr>';
}
}
function renderReportTable(list, total) {
const tbody = document.getElementById('reportTableBody');
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-gray-400 py-8">업무현황이 없습니다</td></tr>';
document.getElementById('reportPagination').innerHTML = '';
return;
}
tbody.innerHTML = list.map(r => {
const progressColor = r.progress_rate >= 80 ? 'bg-emerald-500' : r.progress_rate >= 50 ? 'bg-blue-500' : r.progress_rate >= 20 ? 'bg-amber-500' : 'bg-red-500';
const confirmedBadge = r.confirmed_at
? '<span class="badge badge-green">확인</span>'
: '<button onclick="confirmReport(' + r.id + ')" class="badge badge-amber cursor-pointer hover:opacity-80">미확인</button>';
return `<tr class="cursor-pointer hover:bg-gray-50" onclick="viewReportDetail(${r.id})">
<td>${formatDate(r.report_date || r.created_at)}</td>
<td class="text-center hide-mobile">${r.report_seq || 1}</td>
<td class="font-medium">${escapeHtml(r.company_name || '')}</td>
<td class="max-w-xs truncate">${escapeHtml(r.work_content || '')}</td>
<td class="text-center">${r.actual_workers || 0}명</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-[3rem]">
<div class="${progressColor} rounded-full h-2" style="width: ${r.progress_rate || 0}%"></div>
</div>
<span class="text-xs text-gray-500 whitespace-nowrap">${r.progress_rate || 0}%</span>
</div>
</td>
<td class="hide-mobile max-w-[8rem] truncate text-xs">${escapeHtml(r.issues || '')}</td>
<td class="text-center" onclick="event.stopPropagation()">${confirmedBadge}</td>
<td class="text-right" onclick="event.stopPropagation()">
<button onclick="viewReportDetail(${r.id})" class="text-blue-600 hover:text-blue-800 text-xs" title="상세보기"><i class="fas fa-eye"></i></button>
</td>
</tr>`;
}).join('');
// Pagination
const totalPages = Math.ceil(total / reportLimit);
renderReportPagination(totalPages);
}
function renderReportPagination(totalPages) {
const container = document.getElementById('reportPagination');
if (totalPages <= 1) { container.innerHTML = ''; return; }
let html = '';
if (reportPage > 1) {
html += `<button onclick="goToReportPage(${reportPage - 1})" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">&laquo;</button>`;
}
for (let i = 1; i <= totalPages; i++) {
if (i === reportPage) {
html += `<button class="px-3 py-1 bg-emerald-600 text-white rounded text-sm">${i}</button>`;
} else if (Math.abs(i - reportPage) <= 2 || i === 1 || i === totalPages) {
html += `<button onclick="goToReportPage(${i})" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">${i}</button>`;
} else if (Math.abs(i - reportPage) === 3) {
html += '<span class="text-gray-400">...</span>';
}
}
if (reportPage < totalPages) {
html += `<button onclick="goToReportPage(${reportPage + 1})" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">&raquo;</button>`;
}
container.innerHTML = html;
}
function goToReportPage(p) {
reportPage = p;
loadReports();
}
async function confirmReport(id) {
if (!confirm('이 업무현황을 확인 처리하시겠습니까?')) return;
try {
await api('/work-reports/' + id + '/confirm', { method: 'PUT' });
showToast('확인 처리되었습니다');
loadReports();
} catch(e) {
showToast(e.message || '확인 처리 실패', 'error');
}
}
async function viewReportDetail(id) {
try {
const r = await api('/work-reports/' + id);
const d = r.data || r;
const progressColor = d.progress_rate >= 80 ? 'bg-emerald-500' : d.progress_rate >= 50 ? 'bg-blue-500' : d.progress_rate >= 20 ? 'bg-amber-500' : 'bg-red-500';
// 작업자 목록 테이블
let workersHtml = '';
if (d.workers && d.workers.length > 0) {
const totalHours = d.workers.reduce((sum, w) => sum + Number(w.hours_worked || 0), 0);
workersHtml = `<div class="sm:col-span-2">
<div class="text-xs text-gray-500 mb-1">작업자 목록</div>
<div class="bg-gray-50 rounded-lg p-3">
<table class="w-full text-sm">
<thead><tr class="text-xs text-gray-500 border-b"><th class="text-left py-1">작업자</th><th class="text-left py-1">직위</th><th class="text-right py-1">투입시간</th></tr></thead>
<tbody>
${d.workers.map(w => `<tr class="border-b border-gray-100"><td class="py-1">${escapeHtml(w.worker_name)}</td><td class="py-1 text-gray-500">${escapeHtml(w.position || '')}</td><td class="py-1 text-right">${w.hours_worked || 0}h</td></tr>`).join('')}
</tbody>
<tfoot><tr class="font-medium"><td colspan="2" class="py-1">합계 (${d.workers.length}명)</td><td class="py-1 text-right">${totalHours}h</td></tr></tfoot>
</table>
</div>
</div>`;
}
const html = `
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<div class="text-xs text-gray-500 mb-1">업체</div>
<div class="text-sm font-medium">${escapeHtml(d.company_name || '')}</div>
</div>
<div>
<div class="text-xs text-gray-500 mb-1">보고일 (보고 #${d.report_seq || 1})</div>
<div class="text-sm">${formatDateTime(d.report_date || d.created_at)}</div>
</div>
<div>
<div class="text-xs text-gray-500 mb-1">실투입 인원</div>
<div class="text-sm">${d.actual_workers || 0}명</div>
</div>
<div>
<div class="text-xs text-gray-500 mb-1">진행률</div>
<div class="flex items-center gap-2">
<div class="flex-1 bg-gray-200 rounded-full h-3">
<div class="${progressColor} rounded-full h-3" style="width: ${d.progress_rate || 0}%"></div>
</div>
<span class="text-sm font-medium">${d.progress_rate || 0}%</span>
</div>
</div>
${workersHtml}
<div class="sm:col-span-2">
<div class="text-xs text-gray-500 mb-1">작업내용</div>
<div class="text-sm whitespace-pre-wrap bg-gray-50 rounded-lg p-3">${escapeHtml(d.work_content || '-')}</div>
</div>
${d.issues ? `<div class="sm:col-span-2">
<div class="text-xs text-gray-500 mb-1">이슈사항</div>
<div class="text-sm whitespace-pre-wrap bg-red-50 rounded-lg p-3 text-red-700">${escapeHtml(d.issues)}</div>
</div>` : ''}
${d.next_plan ? `<div class="sm:col-span-2">
<div class="text-xs text-gray-500 mb-1">향후 계획</div>
<div class="text-sm whitespace-pre-wrap bg-blue-50 rounded-lg p-3 text-blue-700">${escapeHtml(d.next_plan)}</div>
</div>` : ''}
<div>
<div class="text-xs text-gray-500 mb-1">확인 상태</div>
<div class="text-sm">${d.confirmed_at ? '<span class="badge badge-green">확인완료</span> ' + formatDateTime(d.confirmed_at) : '<span class="badge badge-amber">미확인</span>'}</div>
</div>
${d.confirmed_by_name ? `<div>
<div class="text-xs text-gray-500 mb-1">확인자</div>
<div class="text-sm">${escapeHtml(d.confirmed_by_name)}</div>
</div>` : ''}
</div>
${!d.confirmed_at ? `<div class="mt-4 flex justify-end">
<button onclick="confirmReport(${d.id})" class="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm hover:bg-emerald-700">
<i class="fas fa-check mr-1"></i>확인 처리
</button>
</div>` : ''}`;
document.getElementById('reportDetailContent').innerHTML = html;
document.getElementById('reportDetailPanel').classList.remove('hidden');
document.getElementById('reportDetailPanel').scrollIntoView({ behavior: 'smooth', block: 'start' });
} catch(e) {
showToast('상세 정보를 불러올 수 없습니다', 'error');
}
}
function closeReportDetail() {
document.getElementById('reportDetailPanel').classList.add('hidden');
}
function initWorkReportPage() {
if (!initAuth()) return;
// Set default date range to this month
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();
loadReports();
}