Files
edu/admin/skin/legal_edu_20260507.php
T

492 lines
23 KiB
PHP

<?php
require_once __DIR__ . '/../../bbs/auth.php';
edu_require_login();
include_once 'header.php';
require_once __DIR__ . '/../../bbs/db_conn.php';
$search_comp = $_GET['comp'] ?? '';
// 만약 사용자가 처음 페이지에 들어왔거나(GET값이 없음), '전체'를 누른 게 아니라면 초기값 설정
// 소속회사(comp)도 동일한 메커니즘 적용
$search_comp = $_GET['comp'] ?? '';
if (empty($search_comp) && !isset($_GET['comp'])) {
$search_comp = $sys_comp_code;
}
$search_year = $_GET['year'] ?? date('Y');
$search_dept = $_GET['dept'] ?? '';
$search_name = $_GET['name'] ?? '';
$search_comp_status = $_GET['comp_status'] ?? '';
$message_name = '';
$all_user_qty = 0;
$completed_qty = 0;
$incomplete_qty = 0;
$corp_list = [];
$rows = [];
try {
$pdo = db_conn();
// 1. 프로시저 호입 후 nextRowset으로 부분적 result set을 완전 소진
try {
$stmt_corp = $pdo->query("CALL proc_get_code2_list('CO100')");
$corp_list = $stmt_corp->fetchAll(PDO::FETCH_ASSOC);
while ($stmt_corp->nextRowset()) {
}
unset($stmt_corp);
} catch (Exception $eProc) {
$corp_list = [];
}
// 권한에 따른 법인 목록 제어
// LE10001: 전체권한 → 전체 법인 표시 (corp_list 그대로)
// LE10002: 법인권한 → 본인 법인($sys_comp_code)만 표시, 검색값도 고정
// 그 외: 법인 목록 없음
// 법인 초기값은 로그인한 사용자의 법인으로 설정
$message_name = $pdo->prepare("SELECT DESC01 FROM edu_codes WHERE base_code = 'AL100100'");
$message_name->execute();
$message_name = $message_name->fetchColumn();
if ($auth_level === 'LE10002') {
// 본인 법인만 필터링
$corp_list = array_filter($corp_list, fn($c) => $c['code'] === $sys_comp_code);
$corp_list = array_values($corp_list);
// 검색 법인도 강제 고정
$search_comp = $sys_comp_code;
} elseif ($auth_level !== 'LE10001') {
// 그 외 권한: 법인 목록 비움
$corp_list = [];
}
// 2. 전체 대상자 수 로그인한 법인과 소속회사가 같은 기준으로 계산, 퇴사자 제외
$stmt_all = $pdo->prepare("SELECT COUNT(DISTINCT member_id)
FROM edu_users
WHERE (end_date IS NULL OR (end_date > '1000-01-01' AND YEAR(end_date) >= ?))
AND (? = '' OR belong_comp = ?)
and sys_comp_code = belong_comp");
$stmt_all->execute([$search_year, $search_comp, $search_comp]);
$all_user_qty = (int) $stmt_all->fetchColumn();
// 3. 미수료 인원
$stmt_incomp = $pdo->prepare("SELECT COUNT(DISTINCT u.member_id)
FROM edu_users u
WHERE (u.end_date IS NULL OR (u.end_date > '1000-01-01' AND YEAR(u.end_date) >= ?))
AND (? = '' OR u.belong_comp = ?)
AND sys_comp_code = belong_comp
AND fn_get_progress_rate(u.sys_comp_code,?,u.member_id,'CA10003','') != 100");
$stmt_incomp->execute([$search_year, $search_comp, $search_comp, $search_year]);
$incomplete_qty = (int) $stmt_incomp->fetchColumn();
$completed_qty = max(0, $all_user_qty - $incomplete_qty);
// 4. G1 메인 쿼리 (? 위치 파라미터 사용으로 재사용 문제 없음)
$sql_inner = "SELECT a.sys_comp_code, a.belong_comp
, (SELECT code_name FROM edu_codes c WHERE c.group_code = 'CO100' AND c.code = a.belong_comp LIMIT 1) AS comp_name
, a.name, a.member_id
, a.dept_name
, IFNULL(b.formatted_tm, '00시간 00분') AS all_tm
, CASE WHEN a.member_id IN (
SELECT u2.member_id FROM edu_users u2 WHERE NOT EXISTS (
SELECT 1 FROM edu_contents c2 WHERE c2.category_code = 'CA10003' AND c2.base_year = ? AND c2.is_active = '1'
AND NOT EXISTS (SELECT 1 FROM edu_learning_histories h2 WHERE h2.content_id = c2.content_id AND h2.member_id = u2.member_id AND h2.sys_comp_code = u2.sys_comp_code AND h2.completed_at IS NOT NULL AND h2.completed_at != '')
)
) THEN '수료' ELSE '미수료' END AS completion_status
, fn_get_progress_rate(a.sys_comp_code, ?, a.member_id, 'CA10003', '') AS progress_rate -- 진행율
, fn_get_completion_date(a.sys_comp_code, ?, a.member_id, 'CA10003', '') AS completion_date -- 학습완료일
FROM edu_users a
LEFT JOIN (
SELECT
t.sys_comp_code,
t.member_id,
CONCAT(
LPAD(FLOOR(SUM(t.calc_tm)/3600), 2, '0'), '시간 ',
LPAD(FLOOR((SUM(t.calc_tm)%3600)/60), 2, '0'), '분'
) AS formatted_tm
FROM (
SELECT
z.sys_comp_code,
x.member_id,
CASE
WHEN x.completed_at IS NOT NULL AND x.completed_at <> '' THEN x.content_tm
ELSE x.watch_tm
END AS calc_tm
FROM edu_learning_histories x
JOIN edu_contents y ON x.content_id = y.content_id
JOIN edu_users z ON x.sys_comp_code = z.working_comp AND x.member_id = z.member_id
WHERE y.category_code = 'CA10003'
AND YEAR(x.first_viewed_at) = ?
) t
GROUP BY t.sys_comp_code, t.member_id
) b ON a.member_id = b.member_id AND a.sys_comp_code = b.sys_comp_code
WHERE (a.end_date IS NULL OR a.end_date = '' OR (a.end_date > '1000-01-01' AND YEAR(a.end_date) >= ?))
and a.sys_comp_code = a.belong_comp
AND (? = '' OR a.belong_comp = ?)
AND a.dept_name LIKE CONCAT('%', ?, '%')
AND a.name LIKE CONCAT('%', ?, '%')";
if ($search_comp_status === 'Y') {
$sql = "SELECT * FROM ($sql_inner) t WHERE completion_status = '수료'";
} elseif ($search_comp_status === 'N') {
$sql = "SELECT * FROM ($sql_inner) t WHERE completion_status = '미수료'";
} else {
$sql = "SELECT * FROM ($sql_inner) t";
}
$stmt_g1 = $pdo->prepare($sql);
// ? 순서: 1=completion_status(base_year), 2=progress_rate(year), 3=completion_date(year), 4=LEFT JOIN(first_viewed_at), 5=WHERE(end_date), 6=belong_comp 체크, 7=belong_comp 필터, 8=dept_name, 9=name
$stmt_g1->execute([$search_year, $search_year, $search_year, $search_year, $search_year, $search_comp, $search_comp, $search_dept, $search_name]);
$rows = $stmt_g1->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$db_error = $e->getMessage();
}
?>
<main class="max-w-[1600px] mx-auto p-6">
<header class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<h2 class="text-2xl font-bold text-gray-800 italic">법정의무교육</h2>
<div class="flex flex-wrap gap-2">
<button onclick="alert('준비중입니다.')"
class="px-4 py-2 bg-white border border-gray-200 rounded-md text-sm font-medium hover:bg-gray-50 flex items-center shadow-sm">
<i class="fa-solid fa-file-excel mr-2"></i>교육결과보고서
</button>
<button onclick="sendIncompleteNotification()"
class="px-4 py-2 bg-red-50 text-red-600 border border-red-100 rounded-md text-sm font-bold flex items-center hover:bg-red-100 shadow-sm transition">
<i class="fa-solid fa-bell mr-2"></i>미수료자 알림 (<?php echo $incomplete_qty; ?>명)
</button>
<button onclick="downloadExcel()"
class="px-4 py-2 bg-gray-100 text-gray-600 rounded-md text-sm font-medium hover:bg-gray-200 flex items-center transition">
<i class="fa-solid fa-download mr-2"></i>다운로드
</button>
</div>
</header>
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<p class="text-xs font-bold text-gray-400 mb-1">전체 대상자</p>
<p class="text-3xl font-bold text-gray-800"><?php echo $all_user_qty; ?></p>
</div>
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<p class="text-xs font-bold text-gray-400 mb-1">수료 완료</p>
<p class="text-3xl font-bold text-teal-600"><?php echo $completed_qty; ?></p>
</div>
<div class="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
<p class="text-xs font-bold text-gray-400 mb-1">미수료</p>
<p class="text-3xl font-bold text-red-500"><?php echo $incomplete_qty; ?>명</p>
</div>
</section>
<!-- 검색 조건 폼 -->
<form id="searchForm" method="GET" action="legal_edu.php"
class="bg-white p-5 rounded-xl border border-gray-200 shadow-sm mb-6 flex flex-wrap md:flex-row gap-4 items-end">
<div class="flex-1 min-w-[120px]">
<label for="comp" class="block text-xs font-bold text-gray-500 mb-2">법인 선택</label>
<?php if ($auth_level === 'LE10002'): ?>
<?php
// LE10002: 본인 법인명 표시 (변경 불가)
$fixed_corp_name = !empty($corp_list) ? htmlspecialchars($corp_list[0]['name']) : htmlspecialchars($sys_comp_code);
?>
<!-- 실제 전송값은 hidden으로, UI는 고정 텍스트로 표시 -->
<input type="hidden" name="comp" value="<?= htmlspecialchars($sys_comp_code) ?>">
<input type="text" value="<?= $fixed_corp_name ?>" readonly
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-100 text-gray-600 cursor-not-allowed">
<?php else: ?>
<select id="comp" name="comp" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50">
<option value="">전체</option>
<?php foreach ($corp_list as $corp): ?>
<option value="<?= htmlspecialchars($corp['code']) ?>" <?= $search_comp === $corp['code'] ? 'selected' : '' ?>>
<?= htmlspecialchars($corp['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</div>
<div class="flex-1 min-w-[100px]">
<label for="year" class="block text-xs font-bold text-gray-500 mb-2">기준년도</label>
<input type="text" id="year" name="year" value="<?= htmlspecialchars($search_year) ?>" placeholder="YYYY"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div class="flex-1 min-w-[120px]">
<label for="dept" class="block text-xs font-bold text-gray-500 mb-2">부서</label>
<input type="text" id="dept" name="dept" value="<?= htmlspecialchars($search_dept) ?>"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div class="flex-1 min-w-[100px]">
<label for="name" class="block text-xs font-bold text-gray-500 mb-2">성명</label>
<input type="text" id="name" name="name" value="<?= htmlspecialchars($search_name) ?>"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50"
onkeydown="if(event.key==='Enter') this.form.submit();">
</div>
<div class="flex-1 min-w-[120px]">
<label for="comp_status" class="block text-xs font-bold text-gray-500 mb-2">이수여부</label>
<select id="comp_status" name="comp_status" onchange="this.form.submit()"
class="w-full border-gray-200 rounded-lg text-sm p-2.5 bg-gray-50">
<option value="">전체</option>
<option value="Y" <?= $search_comp_status === 'Y' ? 'selected' : '' ?>>이수</option>
<option value="N" <?= $search_comp_status === 'N' ? 'selected' : '' ?>>미이수</option>
</select>
</div>
</form>
<section class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden mb-12">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="bg-gray-50 border-b border-gray-200 text-gray-500 font-bold italic">
<tr>
<th class="p-4 w-16 text-center">NO</th>
<th class="p-4">소속법인</th>
<th class="p-4">성명</th>
<th class="p-4">사번</th>
<th class="p-4">부서</th>
<th class="p-4">학습시간</th>
<th class="p-4 w-48">진도율</th>
<th class="p-4">교육이수일</th>
<th class="p-4 text-center">수료구분</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<?php if (count($rows) === 0): ?>
<tr>
<td colspan="9" class="p-8 text-center text-gray-400">데이터가 없습니다.</td>
</tr>
<?php else: ?>
<?php $idx = 1;
foreach ($rows as $row): ?>
<tr class="hover:bg-gray-50 transition cursor-pointer"
onclick="openDetailModal('<?= htmlspecialchars($row['sys_comp_code']) ?>', '<?= htmlspecialchars($row['member_id']) ?>', '<?= htmlspecialchars($row['name']) ?>', '<?= htmlspecialchars($row['dept_name']) ?>')">
<td class="p-4 text-center text-gray-500"><?= $idx++ ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['comp_name'] ?? '') ?></td>
<td class="p-4 font-bold text-gray-800"><?= htmlspecialchars($row['name'] ?? '') ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['member_id'] ?? '') ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['dept_name'] ?? '') ?></td>
<td class="p-4 text-gray-500"><?= htmlspecialchars($row['all_tm'] ?? '') ?></td>
<td class="p-4">
<div class="flex items-center space-x-2">
<?php
$progress = $row['progress_rate'] ?? '0%';
$progress_value = is_numeric($progress) ? (int) $progress : (int) preg_replace('/[^0-9]/', '', $progress);
$progress_value = min(100, max(0, $progress_value));
?>
<div class="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div class="bg-teal-500 h-full" style="width: <?= $progress_value ?>%;"></div>
</div>
<span class="text-[10px] font-bold text-teal-600"><?= $progress_value ?>%</span>
</div>
</td>
<td class="p-4 text-gray-600"><?= htmlspecialchars($row['completion_date'] ?? '-') ?></td>
<td class="p-4 text-center">
<?php if ($row['completion_status'] === '수료'): ?>
<span
class="px-3 py-1 bg-green-50 text-green-600 border border-green-100 rounded-full text-[11px] font-bold"><i
class="fa-solid fa-check-circle mr-1"></i>수료</span>
<?php else: ?>
<span class="px-3 py-1 bg-red-50 text-red-600 border border-red-100 rounded-full text-[11px] font-bold"><i
class="fa-solid fa-circle-xmark mr-1"></i>미수료</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
</main>
<!-- 상세 데이터 (G2) 모달 팝업 -->
<div id="detail-modal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-[100] hidden p-4">
<div class="bg-white w-full max-w-4xl rounded-2xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50 shrink-0">
<h3 class="text-xl font-bold text-gray-800">법정의무교육 이수 상세</h3>
<button onclick="closeDetailModal()" class="text-gray-400 hover:text-gray-600"><i
class="fa-solid fa-xmark text-xl"></i></button>
</div>
<div class="p-4 bg-white border-b border-gray-100 flex gap-6 text-sm font-bold text-gray-600 shrink-0">
<div>성명: <span id="modal-name" class="text-gray-900"></span></div>
<div>사번: <span id="modal-member-id" class="text-gray-900"></span></div>
<div>부서명: <span id="modal-dept" class="text-gray-900"></span></div>
</div>
<div class="flex-1 overflow-auto p-4 bg-gray-50/50">
<table class="w-full text-left border-collapse bg-white border border-gray-200">
<thead class="bg-gray-50 border-b border-gray-200 text-gray-500 font-bold">
<tr>
<th class="p-3 text-center w-12">NO</th>
<th class="p-3">교육과정명</th>
<th class="p-3 w-32">학습시간</th>
<th class="p-3 w-32">진도율</th>
<th class="p-3 w-32">학습완료일</th>
<th class="p-3 w-28 text-center">수료구분</th>
</tr>
</thead>
<tbody id="detail-grid-body" class="divide-y divide-gray-100 text-sm">
<!-- AJAX JS INJECTION -->
</tbody>
</table>
</div>
<div class="p-4 bg-gray-50 border-t border-gray-100 flex justify-end shrink-0">
<button onclick="closeDetailModal()"
class="px-6 py-2 bg-gray-500 text-white rounded-lg font-bold shadow-lg">닫기</button>
</div>
</div>
</div>
<script>
function downloadExcel() {
const form = document.getElementById('searchForm');
const urlParams = new URLSearchParams(new FormData(form)).toString();
window.location.href = '../bbs/legal_edu_excel.php?' + urlParams;
}
function openDetailModal(sys_comp_code, member_id, name, dept) {
document.getElementById('modal-name').textContent = name;
document.getElementById('modal-member-id').textContent = member_id;
document.getElementById('modal-dept').textContent = dept;
document.getElementById('detail-modal').classList.remove('hidden');
const gridBody = document.getElementById('detail-grid-body');
gridBody.innerHTML = '<tr><td colspan="6" class="p-6 text-center text-gray-500">로딩 중...</td></tr>';
const year = document.getElementById('year').value;
const requestUrl = `../bbs/get_legal_edu_detail.php?sys_comp_code=${encodeURIComponent(sys_comp_code)}&member_id=${encodeURIComponent(member_id)}&year=${encodeURIComponent(year)}`;
console.log('[G2] requestUrl', requestUrl, { sys_comp_code, member_id, year });
fetch(requestUrl)
.then(res => res.text())
.then(text => {
console.log('[G2] raw response', text);
let data;
try {
data = JSON.parse(text);
} catch (e) {
console.error('[G2] JSON parse error', e, text);
gridBody.innerHTML = '<tr><td colspan="6" class="p-6 text-center text-red-500">JSON 파싱 오류 발생했습니다.</td></tr>';
return;
}
gridBody.innerHTML = '';
if (!data.success || !data.items || data.items.length === 0) {
gridBody.innerHTML = '<tr><td colspan="6" class="p-6 text-center text-gray-400">학습 내역이 없습니다.</td></tr>';
return;
}
data.items.forEach((it, idx) => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-gray-50 transition';
const stHtml = it.comp_status === '수료'
? `<span class="px-3 py-1 bg-green-50 text-green-600 border border-green-100 rounded-full text-[11px] font-bold"><i class="fa-solid fa-check-circle mr-1"></i>수료</span>`
: `<span class="px-3 py-1 bg-red-50 text-red-600 border border-red-100 rounded-full text-[11px] font-bold"><i class="fa-solid fa-circle-xmark mr-1"></i>미수료</span>`;
// progress_rate에서 숫자만 추출
let progressValue = 0;
if (it.progress_rate) {
const match = it.progress_rate.toString().match(/\d+/);
progressValue = match ? parseInt(match[0]) : 0;
}
progressValue = Math.min(100, Math.max(0, progressValue));
tr.innerHTML = `
<td class="p-3 text-center text-gray-400">${idx + 1}</td>
<td class="p-3 font-bold text-gray-800">${escapeHtml(it.title)}</td>
<td class="p-3 text-gray-600">${escapeHtml(it.learn_time)}</td>
<td class="p-3">
<div class="flex items-center space-x-2">
<div class="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
<div class="bg-teal-500 h-full" style="width: ${progressValue}%;"></div>
</div>
<span class="text-[10px] font-bold text-teal-600">${progressValue}%</span>
</div>
</td>
<td class="p-3 text-gray-600">${escapeHtml(it.completion_date) || '-'}</td>
<td class="p-3 text-center">${stHtml}</td>
`;
gridBody.appendChild(tr);
});
})
.catch(err => {
console.error('데이터 로드 오류:', err);
gridBody.innerHTML = '<tr><td colspan="6" class="p-6 text-center text-red-500">데이터를 불러오는 중 오류가 발생했습니다.</td></tr>';
});
}
function closeDetailModal() {
document.getElementById('detail-modal').classList.add('hidden');
}
function sendIncompleteNotification() {
const incompleteCount = <?php echo $incomplete_qty; ?>;
if (incompleteCount === 0) {
alert('미수료자가 없습니다.');
return;
}
if (!confirm(`<?php echo $message_name; ?> \n"위의 메시지로발송됩니다."\n미수료자 ${incompleteCount}명에게 알림을 발송하시겠습니까?`)) {
return;
}
// 현재 날짜 + 14일 계산
const today = new Date();
const endDate = new Date(today);
endDate.setDate(today.getDate() + 14);
const endDateStr = endDate.toISOString().split('T')[0];
// 알림 발송 요청
fetch('../bbs/notification_send.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
code: '100',
end_date: endDateStr,
action: 'send'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`미수료자 알림이 성공적으로 발송되었습니다.\n발송 대상: ${data.sent_count || incompleteCount}명`);
} else {
alert(`알림 발송에 실패했습니다: ${data.message || '알 수 없는 오류'}`);
}
})
.catch(err => {
console.error('알림 발송 오류:', err);
alert('알림 발송 중 오류가 발생했습니다.');
});
}
function downloadExcel() {
const params = new URLSearchParams({
comp: document.getElementById('comp').value || '',
year: document.getElementById('year').value || '',
dept: document.getElementById('dept').value || '',
name: document.getElementById('name').value || '',
comp_status: document.getElementById('comp_status').value || ''
});
window.location.href = `../bbs/legal_edu_excel.php?${params.toString()}`;
}
function escapeHtml(unsafe) {
return (unsafe || '').toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
</script>
</body>
</html>