Files
rikkei_simple_care/client/frontend/src/main.js
2026-06-30 10:12:29 +07:00

374 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import './style.css';
import './app.css';
import logoUrl from './assets/logo.jpeg';
const brandLogoHtml = `<img src="${logoUrl}" alt="Simple Care" class="brand-logo-img" />`;
// 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: []
};
// 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);
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();
} else {
loggedIn = false;
renderLoginPrompt();
}
} catch (err) {
console.error('Error checking login status:', err);
}
}
function renderLoginPrompt() {
document.querySelector('#app').innerHTML = `
<div class="login-prompt-container">
<div class="card">
<div class="login-brand-row">
${brandLogoHtml}
<div class="login-brand-text">
<div class="login-brand-name">Simple Care</div>
<div class="login-brand-sub">Rikkei Education</div>
</div>
</div>
<h2 class="login-title">Đăng nhập sinh viên</h2>
<p class="login-desc">
Ứ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.
</p>
<button class="btn btn-primary" id="btn-goto-login" style="width: 100%; padding: 0.75rem;">
Đi đến trang đăng nhập
</button>
</div>
</div>
`;
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 '<div class="shifts-empty">Hôm nay không có ca học theo lịch lớp.</div>';
}
return `
<table class="shifts-table">
<thead>
<tr>
<th>Ca</th>
<th>Môn học</th>
<th>Giờ</th>
<th>Trực tuyến</th>
<th>Ngoại tuyến</th>
<th>Điểm danh</th>
</tr>
</thead>
<tbody>
${shifts.map(s => `
<tr class="${s.isActiveNow ? 'shift-row-active' : ''}">
<td><strong>Ca ${s.period}</strong>${s.isActiveNow ? ' <span class="shift-now">đang học</span>' : ''}</td>
<td>${s.courseName || '—'}</td>
<td class="shift-time">${s.startTime}${s.endTime}</td>
<td class="time-online">${formatDuration(s.onlineSeconds || 0)}</td>
<td class="time-offline">${formatDuration(s.offlineSeconds || 0)}</td>
<td><span class="att-badge ${attendanceClass(s.attendanceStatus)}">${s.attendanceLabel || 'Chưa tính'}</span></td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
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 = `
<div class="dashboard-wrapper">
<header class="header">
<div class="brand">
${brandLogoHtml}
<div class="brand-text">
<span class="brand-name">Simple Care</span>
<span class="brand-sub">Rikkei Education</span>
</div>
</div>
<button class="btn btn-logout" id="btn-logout">Đăng xuất</button>
</header>
<div class="main-grid">
<div class="card profile-card">
<div class="avatar-container">
<img src="${studentInfo.avatar || 'https://via.placeholder.com/150'}" alt="Avatar" class="avatar" />
<div class="status-indicator ${stats.serverReachable ? 'online' : 'offline'}"></div>
</div>
<h2 class="student-name">${studentInfo.fullName}</h2>
<div class="student-code">${studentInfo.studentCode}</div>
<p class="student-email">${studentInfo.email}</p>
<div class="divider"></div>
<div class="profile-info-row">
<span>Lớp</span>
<strong id="profile-class">${classLine}</strong>
</div>
<div class="profile-info-row">
<span>Điện thoại</span>
<strong>${studentInfo.phone || 'Chưa cung cấp'}</strong>
</div>
</div>
<div class="right-column">
<div class="card status-banner ${mode.cls}">
<div class="status-banner-icon" id="status-icon">${mode.icon}</div>
<div class="status-banner-main">
<div class="status-banner-top">
<span class="monitor-mode-tag" id="monitor-mode-tag">${mode.label}</span>
<span class="status-class-line" id="status-class-line">${classLine}</span>
</div>
<div class="status-banner-value" id="status-message">${stats.monitorLabel || stats.statusMsg || 'Đang kết nối...'}</div>
<div class="status-banner-sub" id="status-sub">
${stats.currentPeriod > 0 && stats.currentCourseName
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
: 'Theo dõi theo lịch học của lớp'}
</div>
</div>
</div>
<div class="stats-grid stats-grid--two">
<div class="card stat-card">
<div class="stat-icon-wrap">📡</div>
<div class="stat-info">
<div class="stat-title">WiFi</div>
<div class="stat-value" id="wifi-ssid">${stats.wifiSSID || 'Đang quét...'}</div>
</div>
</div>
<div class="card stat-card">
<div class="stat-icon-wrap">🖥️</div>
<div class="stat-info">
<div class="stat-title">Máy chủ</div>
<div class="stat-value" id="server-status" style="color: ${stats.serverReachable ? 'var(--online-color)' : 'var(--offline-color)'}">
${stats.serverReachable ? 'Đã kết nối' : 'Mất kết nối'}
</div>
</div>
</div>
</div>
<div class="clocks-card card">
<div class="card-title-row">
<div class="card-title">Hôm nay — theo ca</div>
<div class="card-title-sub" id="session-date">${stats.sessionDate || ''}</div>
</div>
<div class="clock-display clock-display-summary">
<span class="clock-chip clock-chip--online">
<em>Trực tuyến</em>
<strong id="clock-online">00:00:00</strong>
</span>
<span class="clock-chip clock-chip--offline">
<em>Ngoại tuyến</em>
<strong id="clock-offline">00:00:00</strong>
</span>
</div>
<div class="shifts-wrap" id="shifts-wrap">
${renderShiftsTable(stats.shifts)}
</div>
</div>
</div>
</div>
</div>
`;
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();
await window.go.main.App.Logout();
}
});
}
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'}`;
}
} 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();