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: []
};
let chatOpen = false;
let chatMessages = [];
let chatConversations = [];
let activeStaffId = 0;
let chatAudioCtx = null;
function playChatSound() {
try {
const Ctx = window.AudioContext || window.webkitAudioContext;
if (!chatAudioCtx) chatAudioCtx = new Ctx();
const ctx = chatAudioCtx;
if (ctx.state === 'suspended') ctx.resume();
const tone = (freq, start, dur) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.0001, start);
gain.gain.exponentialRampToValueAtTime(0.15, start + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, start + dur);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(start);
osc.stop(start + dur + 0.02);
};
const t = ctx.currentTime;
tone(880, t, 0.12);
tone(1174, t + 0.14, 0.14);
} catch {
/* ignore */
}
}
// 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).catch(console.error);
if (data?.senderRole === 'staff') {
playChatSound();
}
});
window.runtime.EventsOn('chat:notify', (data) => {
updateChatBadge();
showChatToast(data);
playChatSound();
});
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('')}
`;
}
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'}
📡
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();
}
});
}
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 = `
`;
document.body.appendChild(wrap);
document.getElementById('student-chat-fab').addEventListener('click', toggleChat);
document.getElementById('student-chat-close').addEventListener('click', () => 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 showChatToast(data) {
const toast = document.getElementById('student-chat-toast');
if (!toast) return;
const from = data?.from || 'Giảng viên';
const preview = data?.preview || 'Bạn có tin nhắn mới';
toast.innerHTML = `${escapeHtml(from)}${escapeHtml(preview)}`;
toast.hidden = false;
clearTimeout(showChatToast._t);
showChatToast._t = setTimeout(() => { toast.hidden = true; }, 5000);
}
function setChatOpen(open) {
chatOpen = open;
const panel = document.getElementById('student-chat-messenger');
if (panel) panel.hidden = !open;
if (open) {
loadChatConversations().catch(console.error);
if (activeStaffId) loadChatMessages(activeStaffId).catch(console.error);
}
}
function toggleChat() {
setChatOpen(!chatOpen);
}
async function loadChatConversations() {
const rows = await window.go.main.App.GetChatConversations();
chatConversations = Array.isArray(rows) ? rows : [];
renderChatConversations();
updateChatBadge();
}
function renderChatConversations() {
const list = document.getElementById('student-chat-conv-list');
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-btn').forEach((btn) => {
btn.addEventListener('click', () => {
const sid = Number(btn.getAttribute('data-staff-id'));
pickStaffChat(sid);
});
});
}
async function pickStaffChat(staffId) {
activeStaffId = staffId;
const head = document.getElementById('student-chat-thread-head');
const conv = chatConversations.find((c) => Number(c.staffId) === staffId);
if (head) head.textContent = conv?.staffName || conv?.staffEmail || 'Giảng viên';
const input = document.getElementById('student-chat-input');
const sendBtn = document.getElementById('student-chat-send');
if (input) input.disabled = !staffId;
if (sendBtn) sendBtn.disabled = !staffId;
renderChatConversations();
if (staffId) await loadChatMessages(staffId);
}
async function loadChatMessages(staffId) {
const msgs = await window.go.main.App.GetChatMessages(staffId);
chatMessages = Array.isArray(msgs) ? msgs : [];
renderChatMessages();
updateChatBadge();
loadChatConversations().catch(console.error);
}
function renderChatMessages() {
const box = document.getElementById('student-chat-messages');
if (!box) return;
if (!chatMessages.length) {
box.innerHTML = 'Chưa có tin nhắn
';
return;
}
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' }) : '';
const label = role === 'staff' ? (m.staffName || 'Giảng viên') : 'Bạn';
return `${label}
${escapeHtml(m.body || '')}
${time}
`;
}).join('');
box.scrollTop = box.scrollHeight;
}
function escapeHtml(s) {
return String(s).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) {
statusSubEl.innerText = stats.currentPeriod > 0 && stats.currentCourseName
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
: 'Theo dõi theo lịch học của lớp';
}
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();