Files
tk-factory-services/tkpurchase/web/static/js/tkpurchase-partner-portal.js
Hyungi Ahn efc3c14db5 fix: 배포 후 버그 수정 — 테이블명/컬럼명 불일치, navbar active, API 검증 강화, 대시보드 통계 라우트 추가
- checkinModel: partner_checkins → partner_work_checkins, countActive() 추가
- workReportModel: partner_work_reports → daily_work_reports
- partner-portal: check_out_at/check_in_at → check_out_time/check_in_time
- checkinModel findTodayByCompany: LEFT JOIN has_work_report
- tkpurchase-core/tksafety-core: navbar match '' 제거
- checkinController: checkOut에 업무현황 검증, stats() 추가
- workReportController: checkin_id 필수 + schedule 일치 검증
- checkinRoutes: GET / 대시보드 통계 라우트 추가
- nginx.conf: visit.html → tksafety 리다이렉트
- migration-purchase-safety.sql: DDL 동기화
- migration-purchase-safety-patch.sql: 신규 패치

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

242 lines
11 KiB
JavaScript

/* tkpurchase-partner-portal.js - Partner portal logic */
let portalSchedules = [];
let portalCheckins = {};
let partnerCompanyId = null;
async function loadMySchedules() {
try {
const r = await api('/schedules/my');
portalSchedules = r.data || [];
} catch(e) {
console.warn('Load schedules error:', e);
portalSchedules = [];
}
}
async function loadMyCheckins() {
try {
const r = await api('/checkins/my');
const list = r.data || [];
portalCheckins = {};
list.forEach(c => {
if (c.schedule_id) portalCheckins[c.schedule_id] = c;
});
} catch(e) {
console.warn('Load checkins error:', e);
portalCheckins = {};
}
}
async function renderScheduleCards() {
await Promise.all([loadMySchedules(), loadMyCheckins()]);
const container = document.getElementById('scheduleCards');
const noMsg = document.getElementById('noScheduleMessage');
if (!portalSchedules.length) {
container.innerHTML = '';
noMsg.classList.remove('hidden');
return;
}
noMsg.classList.add('hidden');
container.innerHTML = portalSchedules.map(s => {
const checkin = portalCheckins[s.id];
const isCheckedIn = checkin && !checkin.check_out_time;
const isCheckedOut = checkin && checkin.check_out_time;
const hasReport = checkin && checkin.has_work_report;
// Step indicators
const step1Class = checkin ? 'text-emerald-600' : 'text-gray-400';
const step2Class = isCheckedIn || isCheckedOut ? 'text-emerald-600' : 'text-gray-400';
const step3Class = isCheckedOut ? 'text-emerald-600' : 'text-gray-400';
return `<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<!-- 일정 정보 -->
<div class="p-5 border-b">
<div class="flex items-center justify-between mb-2">
<h3 class="text-base font-semibold text-gray-800">${escapeHtml(s.workplace_name || '작업장 미지정')}</h3>
<span class="text-xs text-gray-500">${formatDate(s.work_date)}</span>
</div>
${s.work_description ? `<p class="text-sm text-gray-600 mb-2">${escapeHtml(s.work_description)}</p>` : ''}
<div class="flex gap-4 text-xs text-gray-500">
<span><i class="fas fa-users mr-1"></i>예상 ${s.expected_workers || 0}명</span>
${s.notes ? `<span><i class="fas fa-sticky-note mr-1"></i>${escapeHtml(s.notes)}</span>` : ''}
</div>
</div>
<!-- 3-step 진행 표시 -->
<div class="px-5 py-3 bg-gray-50 border-b">
<div class="flex items-center justify-between text-xs">
<div class="flex items-center gap-1 ${step1Class}">
<i class="fas ${checkin ? 'fa-check-circle' : 'fa-circle'}"></i>
<span>1. 작업 시작</span>
</div>
<div class="flex-1 border-t border-gray-300 mx-2"></div>
<div class="flex items-center gap-1 ${step2Class}">
<i class="fas ${(isCheckedIn || isCheckedOut) ? 'fa-check-circle' : 'fa-circle'}"></i>
<span>2. 업무현황</span>
</div>
<div class="flex-1 border-t border-gray-300 mx-2"></div>
<div class="flex items-center gap-1 ${step3Class}">
<i class="fas ${isCheckedOut ? 'fa-check-circle' : 'fa-circle'}"></i>
<span>3. 작업 종료</span>
</div>
</div>
</div>
<!-- Step 1: 작업 시작 (체크인) -->
<div class="p-5 ${checkin ? 'bg-gray-50' : ''}">
${!checkin ? `
<div id="checkinForm_${s.id}">
<h4 class="text-sm font-semibold text-gray-700 mb-3"><i class="fas fa-play-circle text-emerald-500 mr-1"></i>작업 시작</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">실투입 인원 <span class="text-red-400">*</span></label>
<input type="number" id="checkinWorkers_${s.id}" min="1" value="${s.expected_workers || 1}" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">작업자 명단</label>
<input type="text" id="checkinNames_${s.id}" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="홍길동, 김철수">
</div>
</div>
<button onclick="doCheckIn(${s.id})" class="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm hover:bg-emerald-700">
<i class="fas fa-play mr-1"></i>작업 시작
</button>
</div>
` : `
<div class="text-sm text-emerald-600 mb-1">
<i class="fas fa-check-circle mr-1"></i>체크인 완료 (${formatTime(checkin.check_in_time)})
· ${checkin.actual_worker_count || 0}
</div>
`}
</div>
<!-- Step 2: 업무현황 입력 (체크인 후 표시) -->
${isCheckedIn ? `
<div class="p-5 border-t">
<h4 class="text-sm font-semibold text-gray-700 mb-3"><i class="fas fa-clipboard-list text-blue-500 mr-1"></i>업무현황 입력</h4>
<div class="space-y-3">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">실투입 인원</label>
<input type="number" id="reportWorkers_${checkin.id}" min="0" value="${checkin.actual_worker_count || 0}" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">진행률 (%)</label>
<input type="number" id="reportProgress_${checkin.id}" min="0" max="100" value="0" class="input-field w-full px-3 py-2 rounded-lg text-sm">
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">작업내용 <span class="text-red-400">*</span></label>
<textarea id="reportContent_${checkin.id}" rows="3" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="오늘 수행한 작업 내용"></textarea>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">이슈사항</label>
<textarea id="reportIssues_${checkin.id}" rows="2" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="문제점이나 특이사항"></textarea>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">향후 계획</label>
<textarea id="reportNextPlan_${checkin.id}" rows="2" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="다음 작업 계획"></textarea>
</div>
<div class="flex gap-2">
<button onclick="submitWorkReport(${checkin.id}, ${s.id})" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
<i class="fas fa-save mr-1"></i>업무현황 저장
</button>
</div>
</div>
</div>
<!-- Step 3: 작업 종료 -->
<div class="p-5 border-t">
<button onclick="doCheckOut(${checkin.id})" class="w-full px-4 py-3 bg-gray-800 text-white rounded-lg text-sm hover:bg-gray-900 font-medium">
<i class="fas fa-stop-circle mr-1"></i>작업 종료 (체크아웃)
</button>
<p class="text-xs text-gray-400 text-center mt-2">업무현황을 먼저 저장한 후 작업을 종료하세요.</p>
</div>
` : ''}
${isCheckedOut ? `
<div class="p-5 border-t bg-gray-50">
<div class="text-sm text-blue-600">
<i class="fas fa-check-double mr-1"></i>작업 종료 완료 (${formatTime(checkin.check_out_time)})
</div>
${hasReport ? '<div class="text-xs text-emerald-600 mt-1"><i class="fas fa-clipboard-check mr-1"></i>업무현황 제출 완료</div>' : ''}
</div>
` : ''}
</div>`;
}).join('');
}
async function doCheckIn(scheduleId) {
const workerCount = parseInt(document.getElementById('checkinWorkers_' + scheduleId).value) || 1;
const workerNames = document.getElementById('checkinNames_' + scheduleId).value.trim();
const body = {
schedule_id: scheduleId,
actual_worker_count: workerCount,
worker_names: workerNames || null
};
try {
await api('/checkins', { method: 'POST', body: JSON.stringify(body) });
showToast('체크인 완료');
renderScheduleCards();
} catch(e) {
showToast(e.message || '체크인 실패', 'error');
}
}
async function submitWorkReport(checkinId, scheduleId) {
const workContent = document.getElementById('reportContent_' + checkinId).value.trim();
if (!workContent) { showToast('작업내용을 입력하세요', 'error'); return; }
const body = {
checkin_id: checkinId,
schedule_id: scheduleId,
actual_workers: parseInt(document.getElementById('reportWorkers_' + checkinId).value) || 0,
work_content: workContent,
progress_rate: parseInt(document.getElementById('reportProgress_' + checkinId).value) || 0,
issues: document.getElementById('reportIssues_' + checkinId).value.trim() || null,
next_plan: document.getElementById('reportNextPlan_' + checkinId).value.trim() || null
};
try {
await api('/work-reports', { method: 'POST', body: JSON.stringify(body) });
showToast('업무현황이 저장되었습니다');
renderScheduleCards();
} catch(e) {
showToast(e.message || '저장 실패', 'error');
}
}
async function doCheckOut(checkinId) {
if (!confirm('작업을 종료하시겠습니까? 업무현황을 먼저 저장했는지 확인하세요.')) return;
try {
await api('/checkins/' + checkinId + '/checkout', { method: 'PUT' });
showToast('작업 종료 (체크아웃) 완료');
renderScheduleCards();
} catch(e) {
showToast(e.message || '체크아웃 실패', 'error');
}
}
function initPartnerPortal() {
if (!initAuth()) return;
// Check if partner account
const token = getToken();
const decoded = decodeToken(token);
if (!decoded || !decoded.partner_company_id) {
location.href = '/';
return;
}
partnerCompanyId = decoded.partner_company_id;
document.getElementById('welcomeCompanyName').textContent = decoded.partner_company_name || decoded.name || '협력업체';
renderScheduleCards();
}