feat: 안전 코드 tksafety 이관 + 사용자 관리 정리 + UI Tailwind 전환
Phase 1: tksafety에 출입신청/체크리스트 API·웹 추가, tkfb 안전 코드 삭제
Phase 2: 사용자 관리 페이지 삭제, API 축소, 알림 수신자 tkuser 이관
Phase 3: tkuser 권한 페이지 정의 업데이트
Phase 4: 전체 34개 페이지 Tailwind CSS + tkfb-core.js 전환,
미사용 CSS 20개·인프라 JS 10개·템플릿·컴포넌트 삭제
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -110,8 +110,8 @@ async function update(req, res) {
|
||||
return res.status(403).json({ success: false, error: '본인이 작성한 보고만 수정할 수 있습니다' });
|
||||
}
|
||||
|
||||
// 확인 완료된 보고 수정 불가
|
||||
if (existing.confirmed_by) {
|
||||
// 확인 완료된 보고 수정 불가 (협력업체만 제한, 구매팀은 수정 가능)
|
||||
if (req.user.partner_company_id && existing.confirmed_by) {
|
||||
return res.status(400).json({ success: false, error: '확인 완료된 보고는 수정할 수 없습니다' });
|
||||
}
|
||||
|
||||
@@ -136,6 +136,32 @@ async function confirm(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// 작업보고 반려
|
||||
async function reject(req, res) {
|
||||
try {
|
||||
const existing = await workReportModel.findById(req.params.id);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: '작업보고를 찾을 수 없습니다' });
|
||||
}
|
||||
if (existing.confirmed_by) {
|
||||
return res.status(400).json({ success: false, error: '확인된 보고는 반려할 수 없습니다. 먼저 확인 취소하세요' });
|
||||
}
|
||||
if (existing.rejected_by) {
|
||||
return res.status(400).json({ success: false, error: '이미 반려된 보고입니다' });
|
||||
}
|
||||
const { reason } = req.body;
|
||||
if (!reason || !reason.trim()) {
|
||||
return res.status(400).json({ success: false, error: '반려 사유를 입력하세요' });
|
||||
}
|
||||
const rejectedBy = req.user.user_id || req.user.id;
|
||||
const row = await workReportModel.reject(req.params.id, rejectedBy, reason.trim());
|
||||
res.json({ success: true, data: row });
|
||||
} catch (err) {
|
||||
console.error('WorkReport reject error:', err);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 종합 요약
|
||||
async function summary(req, res) {
|
||||
try {
|
||||
@@ -252,4 +278,47 @@ async function exportExcel(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { list, getById, myReports, create, update, confirm, summary, exportExcel };
|
||||
// 작업보고 삭제
|
||||
async function deleteReport(req, res) {
|
||||
try {
|
||||
const existing = await workReportModel.findById(req.params.id);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: '작업보고를 찾을 수 없습니다' });
|
||||
}
|
||||
const checkinId = existing.checkin_id;
|
||||
await workReportModel.deleteReport(req.params.id);
|
||||
|
||||
// 남은 보고가 0건이면 체크아웃 되돌리기
|
||||
if (checkinId) {
|
||||
const remaining = await workReportModel.countByCheckin(checkinId);
|
||||
if (remaining === 0) {
|
||||
await checkinModel.resetCheckout(checkinId);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '삭제되었습니다' });
|
||||
} catch (err) {
|
||||
console.error('WorkReport delete error:', err);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
// 확인 취소
|
||||
async function unconfirm(req, res) {
|
||||
try {
|
||||
const existing = await workReportModel.findById(req.params.id);
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: '작업보고를 찾을 수 없습니다' });
|
||||
}
|
||||
if (!existing.confirmed_by) {
|
||||
return res.status(400).json({ success: false, error: '이미 미확인 상태입니다' });
|
||||
}
|
||||
const row = await workReportModel.unconfirm(req.params.id);
|
||||
res.json({ success: true, data: row });
|
||||
} catch (err) {
|
||||
console.error('WorkReport unconfirm error:', err);
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { list, getById, myReports, create, update, confirm, reject, deleteReport, unconfirm, summary, exportExcel };
|
||||
|
||||
@@ -65,6 +65,11 @@ async function update(id, data) {
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
async function resetCheckout(id) {
|
||||
const db = getPool();
|
||||
await db.query('UPDATE partner_work_checkins SET check_out_time = NULL WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
async function countActive() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
@@ -73,4 +78,4 @@ async function countActive() {
|
||||
return rows[0].cnt;
|
||||
}
|
||||
|
||||
module.exports = { findBySchedule, findById, findTodayByCompany, checkIn, checkOut, update, countActive };
|
||||
module.exports = { findBySchedule, findById, findTodayByCompany, checkIn, checkOut, update, resetCheckout, countActive };
|
||||
|
||||
@@ -3,12 +3,14 @@ const { getPool } = require('./partnerModel');
|
||||
async function findAll({ company_id, date_from, date_to, schedule_id, confirmed, page = 1, limit = 50 } = {}) {
|
||||
const db = getPool();
|
||||
let sql = `SELECT wr.*, pc.company_name, ps.work_description AS schedule_description,
|
||||
su_reporter.name AS reporter_name, su_confirmer.name AS confirmed_by_name
|
||||
su_reporter.name AS reporter_name, su_confirmer.name AS confirmed_by_name,
|
||||
su_rejector.name AS rejected_by_name
|
||||
FROM partner_work_reports wr
|
||||
LEFT JOIN partner_companies pc ON wr.company_id = pc.id
|
||||
LEFT JOIN partner_schedules ps ON wr.schedule_id = ps.id
|
||||
LEFT JOIN sso_users su_reporter ON wr.reporter_id = su_reporter.user_id
|
||||
LEFT JOIN sso_users su_confirmer ON wr.confirmed_by = su_confirmer.user_id
|
||||
LEFT JOIN sso_users su_rejector ON wr.rejected_by = su_rejector.user_id
|
||||
WHERE 1=1`;
|
||||
const params = [];
|
||||
if (company_id) { sql += ' AND wr.company_id = ?'; params.push(company_id); }
|
||||
@@ -29,12 +31,14 @@ async function findById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT wr.*, pc.company_name, ps.work_description AS schedule_description,
|
||||
su_reporter.name AS reporter_name, su_confirmer.name AS confirmed_by_name
|
||||
su_reporter.name AS reporter_name, su_confirmer.name AS confirmed_by_name,
|
||||
su_rejector.name AS rejected_by_name
|
||||
FROM partner_work_reports wr
|
||||
LEFT JOIN partner_companies pc ON wr.company_id = pc.id
|
||||
LEFT JOIN partner_schedules ps ON wr.schedule_id = ps.id
|
||||
LEFT JOIN sso_users su_reporter ON wr.reporter_id = su_reporter.user_id
|
||||
LEFT JOIN sso_users su_confirmer ON wr.confirmed_by = su_confirmer.user_id
|
||||
LEFT JOIN sso_users su_rejector ON wr.rejected_by = su_rejector.user_id
|
||||
WHERE wr.id = ?`, [id]);
|
||||
const report = rows[0] || null;
|
||||
if (report) {
|
||||
@@ -133,10 +137,11 @@ async function update(id, data) {
|
||||
if (data.issues !== undefined) { fields.push('issues = ?'); values.push(data.issues || null); }
|
||||
if (data.next_plan !== undefined) { fields.push('next_plan = ?'); values.push(data.next_plan || null); }
|
||||
|
||||
if (fields.length > 0) {
|
||||
values.push(id);
|
||||
await conn.query(`UPDATE partner_work_reports SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||
}
|
||||
// 수정(재제출) 시 반려 상태 자동 해제
|
||||
fields.push('rejected_by = NULL', 'rejected_at = NULL', 'rejection_reason = NULL');
|
||||
|
||||
values.push(id);
|
||||
await conn.query(`UPDATE partner_work_reports SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||
|
||||
// workers 교체 (있으면)
|
||||
if (data.workers !== undefined) {
|
||||
@@ -164,11 +169,19 @@ async function update(id, data) {
|
||||
async function confirm(id, confirmedBy) {
|
||||
const db = getPool();
|
||||
await db.query(
|
||||
'UPDATE partner_work_reports SET confirmed_by = ?, confirmed_at = NOW() WHERE id = ? AND confirmed_by IS NULL',
|
||||
'UPDATE partner_work_reports SET confirmed_by = ?, confirmed_at = NOW() WHERE id = ? AND confirmed_by IS NULL AND rejected_by IS NULL',
|
||||
[confirmedBy, id]);
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
async function reject(id, rejectedBy, reason) {
|
||||
const db = getPool();
|
||||
await db.query(
|
||||
'UPDATE partner_work_reports SET rejected_by = ?, rejected_at = NOW(), rejection_reason = ? WHERE id = ? AND confirmed_by IS NULL',
|
||||
[rejectedBy, reason, id]);
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
async function findAllAggregated({ company_id, schedule_id, date_from, date_to } = {}) {
|
||||
const db = getPool();
|
||||
let sql = `SELECT
|
||||
@@ -234,4 +247,16 @@ async function exportData({ company_id, schedule_id, date_from, date_to } = {})
|
||||
return rows;
|
||||
}
|
||||
|
||||
module.exports = { findAll, findById, findByCheckin, countByCheckin, create, update, confirm, findAllAggregated, exportData };
|
||||
async function deleteReport(id) {
|
||||
const db = getPool();
|
||||
await db.query('DELETE FROM partner_work_reports WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
async function unconfirm(id) {
|
||||
const db = getPool();
|
||||
await db.query(
|
||||
'UPDATE partner_work_reports SET confirmed_by = NULL, confirmed_at = NULL WHERE id = ?', [id]);
|
||||
return findById(id);
|
||||
}
|
||||
|
||||
module.exports = { findAll, findById, findByCheckin, countByCheckin, create, update, confirm, reject, deleteReport, unconfirm, findAllAggregated, exportData };
|
||||
|
||||
@@ -13,5 +13,8 @@ router.get('/:id', ctrl.getById);
|
||||
router.post('/', ctrl.create); // partner can create
|
||||
router.put('/:id', ctrl.update);
|
||||
router.put('/:id/confirm', requirePage('purchasing_workreport'), ctrl.confirm);
|
||||
router.put('/:id/reject', requirePage('purchasing_workreport'), ctrl.reject);
|
||||
router.put('/:id/unconfirm', requirePage('purchasing_workreport'), ctrl.unconfirm);
|
||||
router.delete('/:id', requirePage('purchasing_workreport'), ctrl.deleteReport);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -46,15 +46,18 @@ function renderReportTable(list, total) {
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = list.map(r => {
|
||||
tbody.innerHTML = list.map((r, idx) => {
|
||||
const displaySeq = idx + 1;
|
||||
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>';
|
||||
: r.rejected_by
|
||||
? '<span class="badge badge-red">반려</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="text-center hide-mobile">${displaySeq}</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>
|
||||
@@ -70,6 +73,8 @@ function renderReportTable(list, total) {
|
||||
<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>
|
||||
<button onclick="openEditMode(${r.id})" class="text-amber-600 hover:text-amber-800 text-xs ml-1" title="수정"><i class="fas fa-edit"></i></button>
|
||||
<button onclick="deleteReport(${r.id})" class="text-red-600 hover:text-red-800 text-xs ml-1" title="삭제"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
@@ -143,6 +148,16 @@ async function viewReportDetail(id) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let confirmBtn;
|
||||
if (d.confirmed_at) {
|
||||
confirmBtn = `<button onclick="unconfirmReport(${d.id})" class="px-4 py-2 bg-amber-500 text-white rounded-lg text-sm hover:bg-amber-600"><i class="fas fa-undo mr-1"></i>확인 취소</button>`;
|
||||
} else if (d.rejected_by) {
|
||||
confirmBtn = `<span class="px-4 py-2 bg-red-100 text-red-600 rounded-lg text-sm">반려됨</span>`;
|
||||
} else {
|
||||
confirmBtn = `<button onclick="rejectReport(${d.id})" class="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700"><i class="fas fa-ban mr-1"></i>반려</button>
|
||||
<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>`;
|
||||
}
|
||||
|
||||
const html = `
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
@@ -150,7 +165,7 @@ async function viewReportDetail(id) {
|
||||
<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-xs text-gray-500 mb-1">보고일</div>
|
||||
<div class="text-sm">${formatDateTime(d.report_date || d.created_at)}</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -181,18 +196,25 @@ async function viewReportDetail(id) {
|
||||
</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 class="text-sm">${d.confirmed_at ? '<span class="badge badge-green">확인완료</span> ' + formatDateTime(d.confirmed_at) : d.rejected_by ? '<span class="badge badge-red">반려</span> ' + formatDateTime(d.rejected_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>` : ''}
|
||||
${d.rejected_by ? `<div class="sm:col-span-2">
|
||||
<div class="text-xs text-gray-500 mb-1">반려 정보</div>
|
||||
<div class="text-sm bg-red-50 rounded-lg p-3">
|
||||
<div class="text-red-700 font-medium mb-1">반려 사유: ${escapeHtml(d.rejection_reason || '')}</div>
|
||||
<div class="text-xs text-red-500">반려자: ${escapeHtml(d.rejected_by_name || '')} · ${formatDateTime(d.rejected_at)}</div>
|
||||
</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>` : ''}`;
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button onclick="deleteReport(${d.id})" class="px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700"><i class="fas fa-trash mr-1"></i>삭제</button>
|
||||
<button onclick="openEditMode(${d.id})" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700"><i class="fas fa-edit mr-1"></i>수정</button>
|
||||
${confirmBtn}
|
||||
</div>`;
|
||||
|
||||
document.getElementById('reportDetailContent').innerHTML = html;
|
||||
document.getElementById('reportDetailPanel').classList.remove('hidden');
|
||||
@@ -206,6 +228,177 @@ function closeReportDetail() {
|
||||
document.getElementById('reportDetailPanel').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function deleteReport(id) {
|
||||
if (!confirm('이 업무현황을 삭제하시겠습니까? 삭제 후 복구할 수 없습니다.')) return;
|
||||
try {
|
||||
await api('/work-reports/' + id, { method: 'DELETE' });
|
||||
showToast('삭제되었습니다');
|
||||
closeReportDetail();
|
||||
loadReports();
|
||||
} catch(e) {
|
||||
showToast(e.message || '삭제 실패', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectReport(id) {
|
||||
const reason = prompt('반려 사유를 입력하세요:');
|
||||
if (!reason || !reason.trim()) return;
|
||||
try {
|
||||
await api('/work-reports/' + id + '/reject', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason: reason.trim() })
|
||||
});
|
||||
showToast('반려 처리되었습니다');
|
||||
viewReportDetail(id);
|
||||
loadReports();
|
||||
} catch(e) {
|
||||
showToast(e.message || '반려 처리 실패', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function unconfirmReport(id) {
|
||||
if (!confirm('확인 처리를 취소하시겠습니까?')) return;
|
||||
try {
|
||||
await api('/work-reports/' + id + '/unconfirm', { method: 'PUT' });
|
||||
showToast('확인이 취소되었습니다');
|
||||
viewReportDetail(id);
|
||||
loadReports();
|
||||
} catch(e) {
|
||||
showToast(e.message || '확인 취소 실패', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function openEditMode(id) {
|
||||
try {
|
||||
const r = await api('/work-reports/' + id);
|
||||
const d = r.data || r;
|
||||
|
||||
let workersRowsHtml = '';
|
||||
if (d.workers && d.workers.length > 0) {
|
||||
workersRowsHtml = d.workers.map((w, i) => editWorkerRowHtml(i, w)).join('');
|
||||
} else {
|
||||
workersRowsHtml = editWorkerRowHtml(0, {});
|
||||
}
|
||||
|
||||
const html = `
|
||||
<form id="editReportForm" onsubmit="saveEditReport(event, ${d.id})">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 mb-1 block">업체</label>
|
||||
<div class="text-sm font-medium">${escapeHtml(d.company_name || '')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 mb-1 block">보고일</label>
|
||||
<div class="text-sm">${formatDateTime(d.report_date || d.created_at)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 mb-1 block">실투입 인원</label>
|
||||
<input type="number" name="actual_workers" value="${d.actual_workers || 0}" min="0" class="w-full border rounded px-2 py-1 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 mb-1 block">진행률 (%)</label>
|
||||
<input type="number" name="progress_rate" value="${d.progress_rate || 0}" min="0" max="100" class="w-full border rounded px-2 py-1 text-sm">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="text-xs text-gray-500 mb-1 block">작업내용</label>
|
||||
<textarea name="work_content" rows="3" class="w-full border rounded px-2 py-1 text-sm">${escapeHtml(d.work_content || '')}</textarea>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="text-xs text-gray-500 mb-1 block">이슈사항</label>
|
||||
<textarea name="issues" rows="2" class="w-full border rounded px-2 py-1 text-sm">${escapeHtml(d.issues || '')}</textarea>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="text-xs text-gray-500 mb-1 block">향후 계획</label>
|
||||
<textarea name="next_plan" rows="2" class="w-full border rounded px-2 py-1 text-sm">${escapeHtml(d.next_plan || '')}</textarea>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="text-xs text-gray-500">작업자 목록</label>
|
||||
<button type="button" onclick="addEditWorkerRow()" class="text-xs text-blue-600 hover:text-blue-800"><i class="fas fa-plus mr-1"></i>추가</button>
|
||||
</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-right py-1">투입시간</th><th class="w-8"></th></tr></thead>
|
||||
<tbody id="editWorkersBody">${workersRowsHtml}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" onclick="viewReportDetail(${d.id})" class="px-4 py-2 border rounded-lg text-sm hover:bg-gray-50">취소</button>
|
||||
<button type="submit" class="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm hover:bg-emerald-700"><i class="fas fa-save mr-1"></i>저장</button>
|
||||
</div>
|
||||
</form>`;
|
||||
|
||||
document.getElementById('reportDetailContent').innerHTML = html;
|
||||
document.getElementById('reportDetailPanel').classList.remove('hidden');
|
||||
} catch(e) {
|
||||
showToast('보고 정보를 불러올 수 없습니다', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function editWorkerRowHtml(idx, w) {
|
||||
return `<tr class="edit-worker-row border-b border-gray-100">
|
||||
<td class="py-1"><input type="text" name="worker_name_${idx}" value="${escapeHtml(w.worker_name || '')}" class="w-full border rounded px-2 py-1 text-sm" placeholder="작업자명" required></td>
|
||||
<td class="py-1 text-right"><input type="number" name="worker_hours_${idx}" value="${w.hours_worked || 8}" min="0" step="0.5" class="w-20 border rounded px-2 py-1 text-sm text-right"></td>
|
||||
<td class="py-1 text-center"><button type="button" onclick="removeEditWorkerRow(this)" class="text-red-400 hover:text-red-600 text-xs"><i class="fas fa-times"></i></button></td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function addEditWorkerRow() {
|
||||
const tbody = document.getElementById('editWorkersBody');
|
||||
const idx = tbody.querySelectorAll('.edit-worker-row').length;
|
||||
tbody.insertAdjacentHTML('beforeend', editWorkerRowHtml(idx, {}));
|
||||
}
|
||||
|
||||
function removeEditWorkerRow(btn) {
|
||||
const tbody = document.getElementById('editWorkersBody');
|
||||
if (tbody.querySelectorAll('.edit-worker-row').length <= 1) {
|
||||
showToast('작업자는 최소 1명 필요합니다', 'error');
|
||||
return;
|
||||
}
|
||||
btn.closest('tr').remove();
|
||||
}
|
||||
|
||||
async function saveEditReport(e, id) {
|
||||
e.preventDefault();
|
||||
const form = document.getElementById('editReportForm');
|
||||
const data = {
|
||||
actual_workers: parseInt(form.querySelector('[name="actual_workers"]').value) || 0,
|
||||
progress_rate: parseInt(form.querySelector('[name="progress_rate"]').value) || 0,
|
||||
work_content: form.querySelector('[name="work_content"]').value,
|
||||
issues: form.querySelector('[name="issues"]').value,
|
||||
next_plan: form.querySelector('[name="next_plan"]').value,
|
||||
workers: []
|
||||
};
|
||||
|
||||
const rows = document.querySelectorAll('#editWorkersBody .edit-worker-row');
|
||||
rows.forEach((row, i) => {
|
||||
const nameInput = row.querySelector('input[type="text"]');
|
||||
const hoursInput = row.querySelector('input[type="number"]');
|
||||
if (nameInput && nameInput.value.trim()) {
|
||||
data.workers.push({
|
||||
worker_name: nameInput.value.trim(),
|
||||
hours_worked: parseFloat(hoursInput.value) || 8
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await api('/work-reports/' + id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
showToast('수정되었습니다');
|
||||
viewReportDetail(id);
|
||||
loadReports();
|
||||
} catch(e) {
|
||||
showToast(e.message || '수정 실패', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function initWorkReportPage() {
|
||||
if (!initAuth()) return;
|
||||
// Set default date range to this month
|
||||
|
||||
Reference in New Issue
Block a user