Đăng nhập hệ thống

Phiên sắp hết hạn

Hệ thống sẽ tự động đăng xuất sau 60 giây do không có thao tác.
Nhấn Tiếp tục để gia hạn phiên.

TỔNG HỢP THEO CA - HÔM NAY
TOP CÔNG VIỆC - HÔM NAY
CÔNG VIỆC THEO TỔ - HÔM NAY
CÔNG VIỆC THEO TỔ - TUẦN NÀY
TOP CÔNG VIỆC - TUẦN NÀY
XU HƯỚNG THEO TUẦN - THÁNG NÀY
TOP CÔNG VIỆC - THÁNG NÀY
CÔNG VIỆC THEO TỔ - THÁNG NÀY
XU HƯỚNG 12 THÁNG - NĂM NAY
TOP CÔNG VIỆC - NĂM NAY
CÔNG VIỆC THEO TỔ - NĂM NAY
ĐÁNH GIÁ SLA - TUẦN NÀY
SỰ CỐ THEO BỘ PHẬN - TUẦN NÀY
TOP MÃ LỖI - TUẦN NÀY
NHÂN SỰ XỬ LÝ SỰ CỐ - TUẦN NÀY
⚠ NHÂN SỰ KHÔNG ĐẠT NHIỀU NHẤT - TUẦN NÀY
ĐÁNH GIÁ SLA - THÁNG NÀY
SỰ CỐ THEO BỘ PHẬN - THÁNG NÀY
TOP MÃ LỖI - THÁNG NÀY
NHÂN SỰ XỬ LÝ SỰ CỐ - THÁNG NÀY
⚠ NHÂN SỰ KHÔNG ĐẠT NHIỀU NHẤT - THÁNG NÀY
ĐÁNH GIÁ SLA - NĂM NAY
SỰ CỐ THEO BỘ PHẬN - NĂM NAY
TOP MÃ LỖI - NĂM NAY
NHÂN SỰ XỬ LÝ SỰ CỐ - NĂM NAY
⚠ NHÂN SỰ KHÔNG ĐẠT NHIỀU NHẤT - NĂM NAY
RANKING NHÂN SỰ XỬ LÝ SỰ CỐ - NĂM NAY
// ============================================================ // SESSION HOOKS // ============================================================ function onAfterLogin(maNv, staffLabel) { // Auto-load dashboard vận hành khi vào trang loadDashVH(); } function onAfterLogout() {} // ============================================================ // SESSION CONSTANTS — shared với trang Dashboard // ============================================================ const LOGIN_KEY='vtvcab_login_maNv', LOGIN_LABEL_KEY='vtvcab_login_label', LOGIN_TIME_KEY='vtvcab_login_time'; const LOGIN_CACHE_KEY='vtvcab_login_user_cache_v1'; const CORRECT_PASS='vtvcab', TIMEOUT_MS=30*60*1000; let _timeoutTimer=null, _countdownInterval=null; const loginOverlay=document.getElementById('loginOverlay'); const loginUserEl=document.getElementById('loginUser'); const loginPassEl=document.getElementById('loginPass'); const loginErrEl=document.getElementById('loginErr'); const loginBtn=document.getElementById('loginBtn'); function showLoginError(msg){loginErrEl.textContent=msg;loginErrEl.classList.add('show');loginUserEl.classList.add('err');loginPassEl.classList.add('err');setTimeout(()=>{loginUserEl.classList.remove('err');loginPassEl.classList.remove('err');},2500);} function clearLoginError(){loginErrEl.classList.remove('show');} function normLoginInput(v){return String(v||'').trim().toLowerCase();} function getLoginCache(input){ try{ const raw=localStorage.getItem(LOGIN_CACHE_KEY); const map=raw?JSON.parse(raw):{}; return map[normLoginInput(input)]||null; }catch(e){return null;} } function setLoginCache(input,data){ try{ const raw=localStorage.getItem(LOGIN_CACHE_KEY); const map=raw?JSON.parse(raw):{}; const key=normLoginInput(input); map[key]={maNv:data.maNv||input,label:data.label||data.hoTen||input,ts:Date.now()}; localStorage.setItem(LOGIN_CACHE_KEY,JSON.stringify(map)); }catch(e){} } function refreshLoginInBackground(input){ gasCall('verifyLogin',{input}).then(res=>{ if(res&&res.ok) setLoginCache(input,res); else { try{localStorage.removeItem(LOGIN_KEY);localStorage.removeItem(LOGIN_LABEL_KEY);localStorage.removeItem(LOGIN_TIME_KEY);}catch(e){} } }).catch(()=>{}); } function enterApp(maNv,staffLabel){ try{localStorage.setItem(LOGIN_KEY,maNv);localStorage.setItem(LOGIN_LABEL_KEY,staffLabel);localStorage.setItem(LOGIN_TIME_KEY,Date.now().toString());}catch(e){} window._loggedInMaNv=maNv; window._loggedInLabel=staffLabel; loginOverlay.classList.add('hidden'); document.getElementById('topNav').classList.add('visible'); startTimeoutTimer(); onAfterLogin(maNv, staffLabel); } function doLogout(reason){ clearTimeoutTimer(); hideTimeoutWarning(); try{localStorage.removeItem(LOGIN_KEY);localStorage.removeItem(LOGIN_LABEL_KEY);localStorage.removeItem(LOGIN_TIME_KEY);}catch(e){} window._loggedInMaNv=null; window._loggedInLabel=null; loginOverlay.classList.remove('hidden'); document.getElementById('topNav').classList.remove('visible'); loginUserEl.value=''; loginPassEl.value=''; clearLoginError(); if(reason==='timeout'){loginErrEl.textContent='Phiên làm việc đã hết hạn (30 phút không có thao tác). Vui lòng đăng nhập lại.';loginErrEl.classList.add('show');} onAfterLogout(); } async function attemptLogin(){ const maNv=String(loginUserEl.value||'').trim(); const pass=String(loginPassEl.value||'').trim(); if(!maNv){showLoginError('Vui lòng nhập Mã nhân viên hoặc Email.');return;} if(!pass){showLoginError('Vui lòng nhập mật khẩu.');return;} if(pass!==CORRECT_PASS){showLoginError('Sai mật khẩu. Vui lòng thử lại.');return;} // Nếu user đã từng đăng nhập thành công trên trình duyệt này, vào ngay để tránh chờ GAS cold start. const cached=getLoginCache(maNv); if(cached&&cached.label){ enterApp(cached.maNv||maNv,cached.label); refreshLoginInBackground(maNv); return; } loginBtn.disabled=true; loginBtn.textContent='Đang kiểm tra...'; clearLoginError(); try { const res=await gasCall('verifyLogin',{input:maNv}); loginBtn.disabled=false; loginBtn.textContent='Đăng nhập'; if(res.ok){ setLoginCache(maNv,res); enterApp(res.maNv||maNv,res.label); } else showLoginError(res.message||'Mã nhân viên hoặc Email không tồn tại.'); } catch(e){loginBtn.disabled=false;loginBtn.textContent='Đăng nhập';showLoginError('Lỗi kết nối. Vui lòng thử lại.');} } loginBtn.addEventListener('click',attemptLogin); [loginUserEl,loginPassEl].forEach(el=>{ el.addEventListener('keydown',e=>{if(e.key==='Enter')attemptLogin();}); el.addEventListener('input',clearLoginError); }); // TIMEOUT const WARNING_MS=60000; const timeoutWarnEl=document.getElementById('timeoutWarning'); const countdownSecEl=document.getElementById('countdownSec'); function startTimeoutTimer(){clearTimeoutTimer();_timeoutTimer=setTimeout(showTimeoutWarning,TIMEOUT_MS-WARNING_MS);} function clearTimeoutTimer(){if(_timeoutTimer){clearTimeout(_timeoutTimer);_timeoutTimer=null;}if(_countdownInterval){clearInterval(_countdownInterval);_countdownInterval=null;}} function showTimeoutWarning(){ if(!window._loggedInMaNv)return; timeoutWarnEl.classList.add('show'); let s=Math.round(WARNING_MS/1000); countdownSecEl.textContent=s; _countdownInterval=setInterval(()=>{s--;countdownSecEl.textContent=s;if(s<=0){clearInterval(_countdownInterval);_countdownInterval=null;timeoutWarnEl.classList.remove('show');doLogout('timeout');}},1000); } function hideTimeoutWarning(){timeoutWarnEl.classList.remove('show');if(_countdownInterval){clearInterval(_countdownInterval);_countdownInterval=null;}} function extendSession(){hideTimeoutWarning();try{localStorage.setItem(LOGIN_TIME_KEY,Date.now().toString());}catch(e){}startTimeoutTimer();} function resetActivityTimer(){ if(!window._loggedInMaNv)return; try{localStorage.setItem(LOGIN_TIME_KEY,Date.now().toString());}catch(e){} if(timeoutWarnEl.classList.contains('show'))return; clearTimeoutTimer();startTimeoutTimer(); } ['click','keydown','input','change','scroll','touchstart'].forEach(evt=>document.addEventListener(evt,resetActivityTimer,{passive:true})); document.addEventListener('visibilitychange',function(){ if(document.visibilityState!=='visible')return; if(!window._loggedInMaNv)return; try{const time=localStorage.getItem(LOGIN_TIME_KEY);const age=time?(Date.now()-parseInt(time,10)):Infinity;if(age>=TIMEOUT_MS){doLogout('timeout');}else{resetActivityTimer();}}catch(e){} }); // SESSION RESTORE (function checkSession(){ try{ const maNv=localStorage.getItem(LOGIN_KEY); const label=localStorage.getItem(LOGIN_LABEL_KEY); const time=localStorage.getItem(LOGIN_TIME_KEY); if(maNv&&label){ const now=Date.now(); const age=time?(now-parseInt(time,10)):Infinity; if(age>=TIMEOUT_MS){ localStorage.removeItem(LOGIN_KEY);localStorage.removeItem(LOGIN_LABEL_KEY);localStorage.removeItem(LOGIN_TIME_KEY); loginOverlay.classList.remove('hidden'); return; } localStorage.setItem(LOGIN_TIME_KEY,now.toString()); window._loggedInMaNv=maNv; window._loggedInLabel=label; loginOverlay.classList.add('hidden'); document.getElementById('topNav').classList.add('visible'); const remaining=TIMEOUT_MS-age; clearTimeoutTimer(); if(remaining<=WARNING_MS)showTimeoutWarning(); else _timeoutTimer=setTimeout(showTimeoutWarning,remaining-WARNING_MS); onAfterLogin(maNv,label); return; } }catch(e){} loginOverlay.classList.remove('hidden'); })();