374 lines
13 KiB
JavaScript
374 lines
13 KiB
JavaScript
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();
|