feat: 구매/안전 시스템 전면 개편 — tkpurchase 개편 + tksafety 신규 + 권한 보강
Phase 1: tkuser 협력업체 CRUD 이관 (읽기전용 → 전체 CRUD) Phase 2: tkpurchase 개편 — 일용공 신청/확정, 작업일정, 업무현황, 계정관리, 협력업체 포털 Phase 3: tksafety 신규 시스템 — 방문관리 + 안전교육 신고 Phase 4: SSO 인증 보강 (partner_company_id JWT, 만료일 체크), 권한 테이블 기반 접근 제어 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
250
tkpurchase/web/static/js/tkpurchase-schedule.js
Normal file
250
tkpurchase/web/static/js/tkpurchase-schedule.js
Normal file
@@ -0,0 +1,250 @@
|
||||
/* tkpurchase-schedule.js - Schedule management */
|
||||
|
||||
let schedulePage = 1;
|
||||
const scheduleLimit = 20;
|
||||
let companySearchTimer = null;
|
||||
|
||||
async function loadSchedules() {
|
||||
const company = document.getElementById('filterCompany').value.trim();
|
||||
const dateFrom = document.getElementById('filterDateFrom').value;
|
||||
const dateTo = document.getElementById('filterDateTo').value;
|
||||
const status = document.getElementById('filterStatus').value;
|
||||
|
||||
let query = `?page=${schedulePage}&limit=${scheduleLimit}`;
|
||||
if (company) query += '&company=' + encodeURIComponent(company);
|
||||
if (dateFrom) query += '&date_from=' + dateFrom;
|
||||
if (dateTo) query += '&date_to=' + dateTo;
|
||||
if (status) query += '&status=' + status;
|
||||
|
||||
try {
|
||||
const r = await api('/schedules' + query);
|
||||
renderScheduleTable(r.data || [], r.total || 0);
|
||||
} catch(e) {
|
||||
console.warn('Schedule load error:', e);
|
||||
document.getElementById('scheduleTableBody').innerHTML = '<tr><td colspan="7" class="text-center text-red-400 py-8">로딩 실패</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderScheduleTable(list, total) {
|
||||
const tbody = document.getElementById('scheduleTableBody');
|
||||
if (!list.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-8">일정이 없습니다</td></tr>';
|
||||
document.getElementById('schedulePagination').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const statusMap = {
|
||||
scheduled: ['badge-amber', '예정'],
|
||||
in_progress: ['badge-green', '진행중'],
|
||||
completed: ['badge-blue', '완료'],
|
||||
cancelled: ['badge-gray', '취소']
|
||||
};
|
||||
|
||||
tbody.innerHTML = list.map(s => {
|
||||
const [cls, label] = statusMap[s.status] || ['badge-gray', s.status];
|
||||
const canEdit = s.status === 'scheduled';
|
||||
return `<tr>
|
||||
<td class="font-medium">${escapeHtml(s.company_name || '')}</td>
|
||||
<td>${formatDate(s.work_date)}</td>
|
||||
<td class="max-w-xs truncate">${escapeHtml(s.work_description || '')}</td>
|
||||
<td class="hide-mobile">${escapeHtml(s.workplace_name || '')}</td>
|
||||
<td class="text-center">${s.expected_workers || 0}명</td>
|
||||
<td><span class="badge ${cls}">${label}</span></td>
|
||||
<td class="text-right">
|
||||
${canEdit ? `<button onclick="openEditSchedule(${s.id})" class="text-blue-600 hover:text-blue-800 text-xs mr-1" title="수정"><i class="fas fa-edit"></i></button>
|
||||
<button onclick="deleteSchedule(${s.id})" class="text-red-500 hover:text-red-700 text-xs" title="삭제"><i class="fas fa-trash"></i></button>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Pagination
|
||||
const totalPages = Math.ceil(total / scheduleLimit);
|
||||
renderSchedulePagination(totalPages);
|
||||
}
|
||||
|
||||
function renderSchedulePagination(totalPages) {
|
||||
const container = document.getElementById('schedulePagination');
|
||||
if (totalPages <= 1) { container.innerHTML = ''; return; }
|
||||
|
||||
let html = '';
|
||||
if (schedulePage > 1) {
|
||||
html += `<button onclick="goToSchedulePage(${schedulePage - 1})" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">«</button>`;
|
||||
}
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
if (i === schedulePage) {
|
||||
html += `<button class="px-3 py-1 bg-emerald-600 text-white rounded text-sm">${i}</button>`;
|
||||
} else if (Math.abs(i - schedulePage) <= 2 || i === 1 || i === totalPages) {
|
||||
html += `<button onclick="goToSchedulePage(${i})" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">${i}</button>`;
|
||||
} else if (Math.abs(i - schedulePage) === 3) {
|
||||
html += '<span class="text-gray-400">...</span>';
|
||||
}
|
||||
}
|
||||
if (schedulePage < totalPages) {
|
||||
html += `<button onclick="goToSchedulePage(${schedulePage + 1})" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">»</button>`;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function goToSchedulePage(p) {
|
||||
schedulePage = p;
|
||||
loadSchedules();
|
||||
}
|
||||
|
||||
/* ===== Company Autocomplete ===== */
|
||||
function setupCompanyAutocomplete(inputId, dropdownId, hiddenId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const dropdown = document.getElementById(dropdownId);
|
||||
const hidden = document.getElementById(hiddenId);
|
||||
|
||||
input.addEventListener('input', function() {
|
||||
hidden.value = '';
|
||||
clearTimeout(companySearchTimer);
|
||||
const q = this.value.trim();
|
||||
if (q.length < 1) { dropdown.classList.add('hidden'); return; }
|
||||
companySearchTimer = setTimeout(async () => {
|
||||
try {
|
||||
const r = await api('/partners/search?q=' + encodeURIComponent(q));
|
||||
const list = r.data || [];
|
||||
if (!list.length) {
|
||||
dropdown.innerHTML = '<div class="px-3 py-2 text-sm text-gray-400">결과 없음</div>';
|
||||
} else {
|
||||
dropdown.innerHTML = list.map(c =>
|
||||
`<div class="px-3 py-2 text-sm hover:bg-emerald-50 cursor-pointer" onclick="selectCompany('${inputId}','${hiddenId}','${dropdownId}',${c.id},'${escapeHtml(c.name)}')">${escapeHtml(c.name)}</div>`
|
||||
).join('');
|
||||
}
|
||||
dropdown.classList.remove('hidden');
|
||||
} catch(e) {
|
||||
dropdown.classList.add('hidden');
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
setTimeout(() => dropdown.classList.add('hidden'), 200);
|
||||
});
|
||||
}
|
||||
|
||||
function selectCompany(inputId, hiddenId, dropdownId, id, name) {
|
||||
document.getElementById(inputId).value = name;
|
||||
document.getElementById(hiddenId).value = id;
|
||||
document.getElementById(dropdownId).classList.add('hidden');
|
||||
}
|
||||
|
||||
/* ===== Add Schedule ===== */
|
||||
function openAddSchedule() {
|
||||
document.getElementById('addScheduleForm').reset();
|
||||
document.getElementById('addCompanyId').value = '';
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
document.getElementById('addWorkDate').value = tomorrow.toISOString().substring(0, 10);
|
||||
document.getElementById('addScheduleModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeAddSchedule() {
|
||||
document.getElementById('addScheduleModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function submitAddSchedule(e) {
|
||||
e.preventDefault();
|
||||
const companyId = document.getElementById('addCompanyId').value;
|
||||
if (!companyId) { showToast('업체를 선택하세요', 'error'); return; }
|
||||
|
||||
const body = {
|
||||
partner_company_id: parseInt(companyId),
|
||||
work_date: document.getElementById('addWorkDate').value,
|
||||
work_description: document.getElementById('addWorkDescription').value.trim(),
|
||||
workplace_name: document.getElementById('addWorkplaceName').value.trim(),
|
||||
expected_workers: parseInt(document.getElementById('addExpectedWorkers').value) || 0,
|
||||
notes: document.getElementById('addNotes').value.trim()
|
||||
};
|
||||
|
||||
try {
|
||||
await api('/schedules', { method: 'POST', body: JSON.stringify(body) });
|
||||
showToast('일정이 등록되었습니다');
|
||||
closeAddSchedule();
|
||||
loadSchedules();
|
||||
} catch(e) {
|
||||
showToast(e.message || '등록 실패', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Edit Schedule ===== */
|
||||
let scheduleCache = {};
|
||||
|
||||
async function openEditSchedule(id) {
|
||||
try {
|
||||
const r = await api('/schedules/' + id);
|
||||
const s = r.data || r;
|
||||
scheduleCache[id] = s;
|
||||
|
||||
document.getElementById('editScheduleId').value = id;
|
||||
document.getElementById('editCompanySearch').value = s.company_name || '';
|
||||
document.getElementById('editCompanyId').value = s.partner_company_id || '';
|
||||
document.getElementById('editWorkDate').value = formatDate(s.work_date);
|
||||
document.getElementById('editWorkDescription').value = s.work_description || '';
|
||||
document.getElementById('editWorkplaceName').value = s.workplace_name || '';
|
||||
document.getElementById('editExpectedWorkers').value = s.expected_workers || 0;
|
||||
document.getElementById('editNotes').value = s.notes || '';
|
||||
document.getElementById('editScheduleModal').classList.remove('hidden');
|
||||
} catch(e) {
|
||||
showToast('일정 정보를 불러올 수 없습니다', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function closeEditSchedule() {
|
||||
document.getElementById('editScheduleModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function submitEditSchedule(e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('editScheduleId').value;
|
||||
const companyId = document.getElementById('editCompanyId').value;
|
||||
if (!companyId) { showToast('업체를 선택하세요', 'error'); return; }
|
||||
|
||||
const body = {
|
||||
partner_company_id: parseInt(companyId),
|
||||
work_date: document.getElementById('editWorkDate').value,
|
||||
work_description: document.getElementById('editWorkDescription').value.trim(),
|
||||
workplace_name: document.getElementById('editWorkplaceName').value.trim(),
|
||||
expected_workers: parseInt(document.getElementById('editExpectedWorkers').value) || 0,
|
||||
notes: document.getElementById('editNotes').value.trim()
|
||||
};
|
||||
|
||||
try {
|
||||
await api('/schedules/' + id, { method: 'PUT', body: JSON.stringify(body) });
|
||||
showToast('일정이 수정되었습니다');
|
||||
closeEditSchedule();
|
||||
loadSchedules();
|
||||
} catch(e) {
|
||||
showToast(e.message || '수정 실패', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Delete Schedule ===== */
|
||||
async function deleteSchedule(id) {
|
||||
if (!confirm('이 일정을 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
await api('/schedules/' + id, { method: 'DELETE' });
|
||||
showToast('일정이 삭제되었습니다');
|
||||
loadSchedules();
|
||||
} catch(e) {
|
||||
showToast(e.message || '삭제 실패', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Init ===== */
|
||||
function initSchedulePage() {
|
||||
if (!initAuth()) return;
|
||||
// Set default date range
|
||||
const now = new Date();
|
||||
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
document.getElementById('filterDateFrom').value = firstDay.toISOString().substring(0, 10);
|
||||
document.getElementById('filterDateTo').value = lastDay.toISOString().substring(0, 10);
|
||||
|
||||
// Setup autocomplete for both modals
|
||||
setupCompanyAutocomplete('addCompanySearch', 'addCompanyDropdown', 'addCompanyId');
|
||||
setupCompanyAutocomplete('editCompanySearch', 'editCompanyDropdown', 'editCompanyId');
|
||||
|
||||
loadSchedules();
|
||||
}
|
||||
Reference in New Issue
Block a user