import './style.css';
import './app.css';
import logoUrl from './assets/logo.jpeg';
const brandLogoHtml = `
`;
// Trạng thái cục bộ
let loggedIn = false;
let studentInfo = null;
let stats = {
wifiSSID: '',
onlineSecs: 0,
offlineSecs: 0,
wsConnected: false,
serverReachable: false,
allowedApps: '',
monitorMode: '',
monitorLabel: '',
className: '',
classCode: '',
currentPeriod: 0,
currentCourseName: '',
shifts: [],
exam: null
};
let chatOpen = false;
let chatMessages = [];
let chatConversations = [];
let activeStaffId = 0;
let lastRenderedMsgCount = 0;
let bannerStaffId = 0;
// Quản lý webcam
let webcamStream = null;
let webcamInterval = null;
const webcamVideo = document.createElement('video');
webcamVideo.autoplay = true;
webcamVideo.playsInline = true;
webcamVideo.style.display = 'none';
document.body.appendChild(webcamVideo);
const webcamCanvas = document.createElement('canvas');
webcamCanvas.width = 320;
webcamCanvas.height = 240;
webcamCanvas.style.display = 'none';
document.body.appendChild(webcamCanvas);
function init() {
if (typeof window.go === 'undefined' || typeof window.go.main === 'undefined') {
setTimeout(init, 200);
return;
}
window.runtime.EventsOn('start_webcam_stream', startWebcam);
window.runtime.EventsOn('stop_webcam_stream', stopWebcam);
window.runtime.EventsOn('chat:message', (data) => {
updateChatBadge();
loadChatConversations().catch(console.error);
if (chatOpen && activeStaffId) {
loadChatMessages(activeStaffId, true).catch(console.error);
}
});
window.runtime.EventsOn('chat:notify', (data) => {
updateChatBadge();
if (data?.staffId) bannerStaffId = Number(data.staffId);
pulseChatFab();
});
window.runtime.EventsOn('exam:paper-sent', () => {
if (loggedIn) {
window.go.main.App.GetStats().then((s) => {
stats = { ...stats, ...s };
updateExamPanel();
}).catch(console.error);
}
});
checkLogin();
}
async function checkLogin() {
try {
const isLogged = await window.go.main.App.CheckLoginStatus();
if (isLogged) {
loggedIn = true;
studentInfo = await window.go.main.App.GetStudentInfo();
renderDashboard();
startStatsTicker();
ensureChatWidget();
updateChatBadge();
} else {
loggedIn = false;
renderLoginPrompt();
}
} catch (err) {
console.error('Error checking login status:', err);
}
}
function renderLoginPrompt() {
document.querySelector('#app').innerHTML = `
${brandLogoHtml}
Simple Care
Rikkei Education
Đăng nhập sinh viên
Ứng dụng sẽ chuyển hướng bạn đến trang Rikkei Portal. Sau khi đăng nhập thành công, hệ thống sẽ tự động đồng bộ tài khoản học tập của bạn.
`;
document.getElementById('btn-goto-login').addEventListener('click', () => {
window.go.main.App.NavigateToLogin();
});
}
function monitorModeMeta(mode) {
switch (mode) {
case 'learning':
return { icon: '🛡️', label: 'Đang giám sát', cls: 'mode-learning' };
case 'exam':
return { icon: '📝', label: 'Phòng thi', cls: 'mode-exam' };
case 'outside_schedule':
return { icon: '😴', label: 'Ngoài giờ học', cls: 'mode-outside' };
case 'not_configured':
return { icon: '⚙️', label: 'Chưa sẵn sàng', cls: 'mode-config' };
default:
return { icon: '⏳', label: 'Đang kết nối', cls: 'mode-pending' };
}
}
function attendanceClass(status) {
if (status === 4) return 'att-ok';
if (status === 3) return 'att-late';
if (status === 1 || status === 2) return 'att-leave';
if (status === 0) return 'att-absent';
return 'att-pending';
}
function renderShiftsTable(shifts) {
if (!shifts || shifts.length === 0) {
return 'Hôm nay không có ca học theo lịch lớp.
';
}
return `
| Ca |
Môn học |
Giờ |
Trực tuyến |
Ngoại tuyến |
Điểm danh |
${shifts.map(s => `
| Ca ${s.period}${s.isActiveNow ? ' đang học' : ''} |
${s.courseName || '—'} |
${s.startTime}–${s.endTime} |
${formatDuration(s.onlineSeconds || 0)} |
${formatDuration(s.offlineSeconds || 0)} |
${s.attendanceLabel || 'Chưa tính'} |
`).join('')}
`;
}
let examPaperFiles = null;
function renderExamResources() {
if (!examPaperFiles?.resources?.length) return '';
const rows = examPaperFiles.resources.map((r) => `
${escapeHtml(r.fileName || 'Tài nguyên')}
`).join('');
return `
Tài nguyên kèm đề
${rows}
`;
}
async function loadExamPaperFiles() {
const ex = stats.exam;
if (!ex || stats.monitorMode !== 'exam' || !ex.paperSent) {
examPaperFiles = null;
return;
}
try {
examPaperFiles = await window.go.main.App.GetExamPaperFiles();
} catch {
examPaperFiles = null;
}
}
function renderExamPanel() {
const ex = stats.exam;
if (!ex || stats.monitorMode !== 'exam') return '';
const paperBtn = ex.paperSent
? ``
: `Chờ giảng viên gửi đề...`;
const quizBtn = ex.quizUrl
? ``
: '';
const submitBtn = ex.submitted
? `✓ Đã nộp bài tự luận`
: ``;
return `
📝 ${ex.examName || 'Phòng thi'}
Bạn đang trong giờ thi. Đề, link thi và tài nguyên mở trong app — không dùng trình duyệt ngoài.
${paperBtn}
${quizBtn}
${submitBtn}
${renderExamResources()}
`;
}
function wireExamPanel() {
const paper = document.getElementById('btn-exam-paper');
if (paper) paper.addEventListener('click', () => {
window.go.main.App.OpenExamPaper().catch((e) => alert(e?.message || e));
});
const quiz = document.getElementById('btn-exam-quiz');
if (quiz) quiz.addEventListener('click', () => {
window.go.main.App.OpenExamQuiz().catch((e) => alert(e?.message || e));
});
const submit = document.getElementById('btn-exam-submit');
if (submit) submit.addEventListener('click', async () => {
try {
const name = await window.go.main.App.SubmitExamWork();
alert(`Đã nộp bài: ${name}`);
const fresh = await window.go.main.App.GetStats();
stats = { ...stats, ...fresh };
await loadExamPaperFiles();
updateExamPanel();
} catch (e) {
alert(e?.message || e || 'Nộp bài thất bại');
}
});
document.querySelectorAll('.btn-exam-res-view').forEach((btn) => {
btn.addEventListener('click', () => {
const id = Number(btn.getAttribute('data-id'));
window.go.main.App.OpenExamResource(id).catch((e) => alert(e?.message || e));
});
});
document.querySelectorAll('.btn-exam-res-dl').forEach((btn) => {
btn.addEventListener('click', async () => {
const id = Number(btn.getAttribute('data-id'));
try {
const path = await window.go.main.App.DownloadExamResource(id);
alert(`Đã tải về:\n${path}`);
} catch (e) {
alert(e?.message || e || 'Tải thất bại');
}
});
});
}
async function updateExamPanel() {
const host = document.getElementById('exam-panel-host');
if (!host) return;
await loadExamPaperFiles();
host.innerHTML = renderExamPanel();
wireExamPanel();
}
function renderDashboard() {
if (!studentInfo) return;
const mode = monitorModeMeta(stats.monitorMode);
const classLine = stats.className
? `${stats.className}${stats.classCode ? ` (${stats.classCode})` : ''}`
: 'Đang xác định lớp...';
document.querySelector('#app').innerHTML = `
${studentInfo.fullName}
${studentInfo.studentCode}
${studentInfo.email}
Lớp
${classLine}
Điện thoại
${studentInfo.phone || 'Chưa cung cấp'}
${mode.icon}
${mode.label}
${classLine}
${stats.monitorLabel || stats.statusMsg || 'Đang kết nối...'}
${stats.currentPeriod > 0 && stats.currentCourseName
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
: 'Theo dõi theo lịch học của lớp'}
${renderExamPanel()}
📡
WiFi
${stats.wifiSSID || 'Đang quét...'}
🖥️
Máy chủ
${stats.serverReachable ? 'Đã kết nối' : 'Mất kết nối'}
Hôm nay — theo ca
00:00:00
·
00:00:00
${stats.sessionDate || ''}
${renderShiftsTable(stats.shifts)}
`;
document.getElementById('btn-logout').addEventListener('click', async () => {
if (confirm('Bạn có chắc chắn muốn đăng xuất khỏi ứng dụng giám sát?')) {
loggedIn = false;
studentInfo = null;
stopWebcam();
const w = document.getElementById('student-chat-widget');
if (w) w.remove();
await window.go.main.App.Logout();
}
});
wireExamPanel();
}
function ensureChatWidget() {
if (document.getElementById('student-chat-widget')) return;
const wrap = document.createElement('div');
wrap.id = 'student-chat-widget';
wrap.className = 'student-chat-dock';
wrap.innerHTML = `
Chọn hội thoại
Chọn giảng viên bên trái để xem tin nhắn
`;
document.body.appendChild(wrap);
document.getElementById('student-chat-fab').addEventListener('click', () => setChatOpen(true));
document.getElementById('student-chat-close').addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
setChatOpen(false);
});
document.getElementById('student-chat-send').addEventListener('click', sendStudentChat);
document.getElementById('student-chat-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); sendStudentChat(); }
});
}
function pulseChatFab() {
const fab = document.getElementById('student-chat-fab');
if (!fab) return;
fab.classList.remove('student-chat-fab--pulse');
void fab.offsetWidth;
fab.classList.add('student-chat-fab--pulse');
}
function setChatOpen(open) {
chatOpen = open;
const modal = document.getElementById('student-chat-modal');
const fab = document.getElementById('student-chat-fab');
if (modal) modal.classList.toggle('is-open', open);
if (fab) fab.classList.toggle('is-hidden', open);
if (open) {
loadChatConversations().then(() => {
const sid = bannerStaffId || activeStaffId || Number(chatConversations[0]?.staffId || 0);
if (sid) selectStaffChat(sid);
}).catch(console.error);
}
}
function selectStaffChat(staffId) {
if (!staffId) return;
activeStaffId = staffId;
const conv = chatConversations.find((c) => Number(c.staffId) === staffId);
const head = document.getElementById('student-chat-thread-head');
if (head) head.textContent = conv?.staffName || conv?.staffEmail || 'Giảng viên';
renderChatConversations();
loadChatMessages(staffId).catch(console.error);
}
async function loadChatConversations() {
const rows = await window.go.main.App.GetChatConversations();
chatConversations = Array.isArray(rows) ? rows : [];
if (chatOpen) renderChatConversations();
updateChatBadge();
return chatConversations;
}
function renderChatConversations() {
const list = document.getElementById('student-chat-sidebar');
if (!list) return;
if (!chatConversations.length) {
list.innerHTML = 'Chưa có tin nhắn
';
return;
}
list.innerHTML = chatConversations.map((c) => {
const sid = Number(c.staffId || 0);
const unread = Number(c.unread || 0);
const active = sid === activeStaffId ? ' active' : '';
const name = escapeHtml(c.staffName || c.staffEmail || 'Giảng viên');
const preview = escapeHtml(c.lastMessage || '—');
return ``;
}).join('');
list.querySelectorAll('.student-chat-conv-row').forEach((btn) => {
btn.addEventListener('click', () => selectStaffChat(Number(btn.getAttribute('data-staff-id'))));
});
}
async function loadChatMessages(staffId, silent = false) {
const msgs = await window.go.main.App.GetChatMessages(staffId);
chatMessages = Array.isArray(msgs) ? msgs : [];
renderChatMessages(silent);
updateChatBadge();
if (!silent) loadChatConversations().catch(console.error);
}
function renderChatMessages(silent = false) {
const box = document.getElementById('student-chat-messages');
if (!box) return;
const wasAtBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 48;
if (!chatMessages.length) {
box.innerHTML = 'Chưa có tin nhắn
';
lastRenderedMsgCount = 0;
return;
}
if (chatMessages.length === lastRenderedMsgCount && silent) return;
lastRenderedMsgCount = chatMessages.length;
box.innerHTML = chatMessages.map((m) => {
const role = m.senderRole === 'student' ? 'student' : 'staff';
const time = m.createdAt ? new Date(m.createdAt).toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' }) : '';
return `
${escapeHtml(m.body || '')}
${time}
`;
}).join('');
if (!silent || wasAtBottom) {
requestAnimationFrame(() => { box.scrollTop = box.scrollHeight; });
}
}
function escapeHtml(s) {
return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
async function sendStudentChat() {
const input = document.getElementById('student-chat-input');
if (!input || !input.value.trim() || !activeStaffId) return;
try {
await window.go.main.App.SendChatMessage(input.value.trim());
input.value = '';
await loadChatMessages(activeStaffId);
} catch (err) {
alert(err?.message || err || 'Gửi tin thất bại');
}
}
async function updateChatBadge() {
const badge = document.getElementById('student-chat-badge');
if (!badge || !window.go?.main?.App?.GetChatUnread) return;
try {
const n = await window.go.main.App.GetChatUnread();
if (n > 0) {
badge.hidden = false;
badge.textContent = n > 9 ? '9+' : String(n);
} else {
badge.hidden = true;
}
} catch {
/* ignore */
}
}
function formatDuration(totalSeconds) {
const hrs = Math.floor(totalSeconds / 3600);
const mins = Math.floor((totalSeconds % 3600) / 60);
const secs = totalSeconds % 60;
const pad = (num) => String(num).padStart(2, '0');
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
}
function startStatsTicker() {
const pullStats = async () => {
if (!loggedIn) return;
try {
const freshStats = await window.go.main.App.GetStats();
stats = { ...stats, ...freshStats };
if (!Array.isArray(stats.shifts)) stats.shifts = [];
const wifiEl = document.getElementById('wifi-ssid');
if (wifiEl) wifiEl.innerText = stats.wifiSSID || 'Không có WiFi';
const serverEl = document.getElementById('server-status');
if (serverEl) {
serverEl.innerText = stats.serverReachable ? 'Đã kết nối' : 'Mất kết nối';
serverEl.style.color = stats.serverReachable ? 'var(--online-color)' : 'var(--offline-color)';
}
const statusMsgEl = document.getElementById('status-message');
if (statusMsgEl) {
statusMsgEl.innerText = stats.monitorLabel || stats.statusMsg || 'Đang kết nối...';
}
const mode = monitorModeMeta(stats.monitorMode);
const statusIconEl = document.getElementById('status-icon');
if (statusIconEl) statusIconEl.innerText = mode.icon;
const modeTagEl = document.getElementById('monitor-mode-tag');
if (modeTagEl) modeTagEl.innerText = mode.label;
const banner = document.querySelector('.status-banner');
if (banner) banner.className = `card status-banner ${mode.cls}`;
const classLine = stats.className
? `${stats.className}${stats.classCode ? ` (${stats.classCode})` : ''}`
: 'Đang xác định lớp...';
const classLineEl = document.getElementById('status-class-line');
if (classLineEl) classLineEl.innerText = classLine;
const profileClassEl = document.getElementById('profile-class');
if (profileClassEl) profileClassEl.innerText = classLine;
const statusSubEl = document.getElementById('status-sub');
if (statusSubEl) {
if (stats.monitorMode === 'exam' && stats.exam?.examName) {
statusSubEl.innerText = stats.exam.examName;
} else {
statusSubEl.innerText = stats.currentPeriod > 0 && stats.currentCourseName
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
: 'Theo dõi theo lịch học của lớp';
}
}
updateExamPanel();
const onlineEl = document.getElementById('clock-online');
if (onlineEl) onlineEl.innerText = formatDuration(stats.onlineSecs || 0);
const offlineEl = document.getElementById('clock-offline');
if (offlineEl) offlineEl.innerText = formatDuration(stats.offlineSecs || 0);
const shiftsWrap = document.getElementById('shifts-wrap');
if (shiftsWrap) shiftsWrap.innerHTML = renderShiftsTable(stats.shifts);
const sessionDateEl = document.getElementById('session-date');
if (sessionDateEl && stats.sessionDate) sessionDateEl.innerText = stats.sessionDate;
const indicator = document.querySelector('.status-indicator');
if (indicator) {
indicator.className = `status-indicator ${stats.serverReachable ? 'online' : 'offline'}`;
}
updateChatBadge();
} catch (err) {
console.error('Error fetching stats:', err);
}
};
pullStats();
setInterval(pullStats, 1000);
}
async function startWebcam() {
if (webcamStream) return;
try {
webcamStream = await navigator.mediaDevices.getUserMedia({
video: { width: 320, height: 240, frameRate: { max: 10 } }
});
webcamVideo.srcObject = webcamStream;
webcamInterval = setInterval(() => {
const ctx = webcamCanvas.getContext('2d');
if (ctx) {
ctx.drawImage(webcamVideo, 0, 0, webcamCanvas.width, webcamCanvas.height);
const dataUrl = webcamCanvas.toDataURL('image/jpeg', 0.4);
window.go.main.App.SendWebcamFrame(dataUrl);
}
}, 250);
} catch (err) {
console.error('Failed to open webcam:', err);
}
}
function stopWebcam() {
if (webcamInterval) {
clearInterval(webcamInterval);
webcamInterval = null;
}
if (webcamStream) {
webcamStream.getTracks().forEach(track => track.stop());
webcamStream = null;
}
webcamVideo.srcObject = null;
}
init();