Section A (Backend): - POST /api/proxy-input: TBM 세션+팀배정+작업보고서 일괄 생성 (트랜잭션) - GET /api/proxy-input/daily-status: 일별 TBM/보고서 입력 현황 - GET /api/proxy-input/daily-status/detail: 작업자별 상세 - tbm_sessions에 is_proxy_input, proxy_input_by 컬럼 추가 - system1/system2/tkuser requireMinLevel → shared requirePage 전환 - permissionModel에 factory_proxy_input, factory_daily_status 키 등록 Section B (Frontend): - daily-status.html: 날짜 네비 + 요약 카드 + 필터 탭 + 작업자 리스트 + 바텀시트 - proxy-input.html: 미입력자 카드 + 확장 폼 + 일괄 설정 + 저장 - tkfb-core.js NAV_MENU에 입력 현황/대리입력 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
212 lines
7.7 KiB
JavaScript
212 lines
7.7 KiB
JavaScript
/**
|
|
* 대리입력 + 일별 현황 모델
|
|
*/
|
|
const { getDb } = require('../dbPool');
|
|
|
|
const ProxyInputModel = {
|
|
/**
|
|
* 중복 배정 체크 (같은 날짜 + 같은 작업자)
|
|
*/
|
|
checkDuplicateAssignments: async (conn, sessionDate, userIds) => {
|
|
if (!userIds.length) return [];
|
|
const placeholders = userIds.map(() => '?').join(',');
|
|
const [rows] = await conn.query(`
|
|
SELECT ta.user_id, w.worker_name, ta.session_id
|
|
FROM tbm_team_assignments ta
|
|
JOIN tbm_sessions s ON ta.session_id = s.session_id
|
|
JOIN workers w ON ta.user_id = w.worker_id
|
|
WHERE s.session_date = ? AND ta.user_id IN (${placeholders}) AND s.status != 'cancelled'
|
|
`, [sessionDate, ...userIds]);
|
|
return rows;
|
|
},
|
|
|
|
/**
|
|
* 작업자 존재 여부 체크
|
|
*/
|
|
validateWorkers: async (conn, userIds) => {
|
|
if (!userIds.length) return [];
|
|
const placeholders = userIds.map(() => '?').join(',');
|
|
const [rows] = await conn.query(`
|
|
SELECT worker_id FROM workers WHERE worker_id IN (${placeholders}) AND status = 'active'
|
|
`, [...userIds]);
|
|
return rows.map(r => r.worker_id);
|
|
},
|
|
|
|
/**
|
|
* TBM 세션 생성 (대리입력)
|
|
*/
|
|
createProxySession: async (conn, data) => {
|
|
const [result] = await conn.query(`
|
|
INSERT INTO tbm_sessions (session_date, leader_user_id, status, is_proxy_input, proxy_input_by, created_by, safety_notes, work_location)
|
|
VALUES (?, ?, 'completed', 1, ?, ?, ?, ?)
|
|
`, [data.session_date, data.leader_id, data.proxy_input_by, data.created_by, data.safety_notes || '', data.work_location || '']);
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* 팀 배정 생성
|
|
*/
|
|
createTeamAssignment: async (conn, data) => {
|
|
const [result] = await conn.query(`
|
|
INSERT INTO tbm_team_assignments (session_id, user_id, project_id, work_type_id, task_id, workplace_id, work_hours, is_present)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1)
|
|
`, [data.session_id, data.user_id, data.project_id, data.work_type_id, data.task_id || null, data.workplace_id || null, data.work_hours]);
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* 작업보고서 생성 (accumulative)
|
|
*/
|
|
createWorkReport: async (conn, data) => {
|
|
const [result] = await conn.query(`
|
|
INSERT INTO daily_work_reports (report_date, user_id, project_id, work_type_id, task_id, work_status_id, work_hours, start_time, end_time, note, tbm_session_id, tbm_assignment_id, created_by, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
|
`, [data.report_date, data.user_id, data.project_id, data.work_type_id, data.task_id || null, data.work_status_id || 1, data.work_hours, data.start_time || null, data.end_time || null, data.note || '', data.tbm_session_id, data.tbm_assignment_id, data.created_by]);
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* 일별 현황 조회
|
|
*/
|
|
getDailyStatus: async (date) => {
|
|
const db = await getDb();
|
|
|
|
// 1. 활성 작업자
|
|
const [workers] = await db.query(`
|
|
SELECT w.worker_id AS user_id, w.worker_name, w.job_type,
|
|
COALESCE(d.department_name, '미배정') AS department_name
|
|
FROM workers w
|
|
LEFT JOIN departments d ON w.department_id = d.department_id
|
|
WHERE w.status = 'active'
|
|
ORDER BY w.worker_name
|
|
`);
|
|
|
|
// 2. TBM 배정 현황
|
|
const [tbmAssignments] = await db.query(`
|
|
SELECT ta.user_id, ta.session_id, s.leader_user_id,
|
|
lu.worker_name AS leader_name, s.is_proxy_input
|
|
FROM tbm_team_assignments ta
|
|
JOIN tbm_sessions s ON ta.session_id = s.session_id
|
|
LEFT JOIN workers lu ON s.leader_user_id = lu.worker_id
|
|
WHERE s.session_date = ? AND s.status != 'cancelled'
|
|
`, [date]);
|
|
|
|
// 3. 작업보고서 현황
|
|
const [reports] = await db.query(`
|
|
SELECT dwr.user_id, SUM(dwr.work_hours) AS total_hours, COUNT(*) AS entry_count
|
|
FROM daily_work_reports dwr
|
|
WHERE dwr.report_date = ?
|
|
GROUP BY dwr.user_id
|
|
`, [date]);
|
|
|
|
// 메모리에서 조합
|
|
const tbmMap = {};
|
|
tbmAssignments.forEach(ta => {
|
|
if (!tbmMap[ta.user_id]) tbmMap[ta.user_id] = [];
|
|
tbmMap[ta.user_id].push(ta);
|
|
});
|
|
|
|
const reportMap = {};
|
|
reports.forEach(r => { reportMap[r.user_id] = r; });
|
|
|
|
let tbmCompleted = 0, reportCompleted = 0, bothCompleted = 0, bothMissing = 0;
|
|
|
|
const workerList = workers.map(w => {
|
|
const hasTbm = !!tbmMap[w.user_id];
|
|
const hasReport = !!reportMap[w.user_id];
|
|
const tbmSessions = (tbmMap[w.user_id] || []).map(ta => ({
|
|
session_id: ta.session_id,
|
|
leader_name: ta.leader_name,
|
|
is_proxy_input: !!ta.is_proxy_input
|
|
}));
|
|
const totalReportHours = reportMap[w.user_id]?.total_hours || 0;
|
|
|
|
let status = 'both_missing';
|
|
if (hasTbm && hasReport) { status = 'complete'; bothCompleted++; }
|
|
else if (hasTbm && !hasReport) { status = 'tbm_only'; }
|
|
else if (!hasTbm && hasReport) { status = 'report_only'; }
|
|
else { bothMissing++; }
|
|
|
|
if (hasTbm) tbmCompleted++;
|
|
if (hasReport) reportCompleted++;
|
|
|
|
return {
|
|
user_id: w.user_id, worker_name: w.worker_name, job_type: w.job_type,
|
|
department_name: w.department_name, has_tbm: hasTbm, has_report: hasReport,
|
|
tbm_sessions: tbmSessions, total_report_hours: totalReportHours, status
|
|
};
|
|
});
|
|
|
|
return {
|
|
date,
|
|
summary: {
|
|
total_active_workers: workers.length,
|
|
tbm_completed: tbmCompleted,
|
|
tbm_missing: workers.length - tbmCompleted,
|
|
report_completed: reportCompleted,
|
|
report_missing: workers.length - reportCompleted,
|
|
both_completed: bothCompleted,
|
|
both_missing: bothMissing
|
|
},
|
|
workers: workerList
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 작업자별 상세 조회
|
|
*/
|
|
getDailyStatusDetail: async (date, userId) => {
|
|
const db = await getDb();
|
|
|
|
// 작업자 정보
|
|
const [workerRows] = await db.query(`
|
|
SELECT w.worker_id AS user_id, w.worker_name, w.job_type,
|
|
COALESCE(d.department_name, '미배정') AS department_name
|
|
FROM workers w
|
|
LEFT JOIN departments d ON w.department_id = d.department_id
|
|
WHERE w.worker_id = ?
|
|
`, [userId]);
|
|
|
|
// TBM 세션
|
|
const [tbmSessions] = await db.query(`
|
|
SELECT ta.session_id, s.status, s.is_proxy_input,
|
|
lu.worker_name AS leader_name,
|
|
pu.name AS proxy_input_by_name,
|
|
p.project_name, wt.work_type_name, ta.work_hours
|
|
FROM tbm_team_assignments ta
|
|
JOIN tbm_sessions s ON ta.session_id = s.session_id
|
|
LEFT JOIN workers lu ON s.leader_user_id = lu.worker_id
|
|
LEFT JOIN sso_users pu ON s.proxy_input_by = pu.user_id
|
|
LEFT JOIN projects p ON ta.project_id = p.project_id
|
|
LEFT JOIN work_types wt ON ta.work_type_id = wt.work_type_id
|
|
WHERE s.session_date = ? AND ta.user_id = ? AND s.status != 'cancelled'
|
|
`, [date, userId]);
|
|
|
|
// 작업보고서
|
|
const [workReports] = await db.query(`
|
|
SELECT dwr.report_id, dwr.work_hours, dwr.created_at, dwr.created_by,
|
|
cu.name AS created_by_name,
|
|
p.project_name, wt.work_type_name, t.task_name,
|
|
ws.status_name AS work_status,
|
|
s.is_proxy_input
|
|
FROM daily_work_reports dwr
|
|
LEFT JOIN sso_users cu ON dwr.created_by = cu.user_id
|
|
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
|
LEFT JOIN work_types wt ON dwr.work_type_id = wt.work_type_id
|
|
LEFT JOIN tasks t ON dwr.task_id = t.task_id
|
|
LEFT JOIN work_statuses ws ON dwr.work_status_id = ws.work_status_id
|
|
LEFT JOIN tbm_sessions s ON dwr.tbm_session_id = s.session_id
|
|
WHERE dwr.report_date = ? AND dwr.user_id = ?
|
|
ORDER BY dwr.created_at
|
|
`, [date, userId]);
|
|
|
|
return {
|
|
worker: workerRows[0] || null,
|
|
tbm_sessions: tbmSessions,
|
|
work_reports: workReports
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports = ProxyInputModel;
|