[tutor_portal_sso]
// ===== WordPress SSO Auto-Login (injected by deploy script) ===== if (window.__GT_SSO && window.__GT_SSO.email) { // WP user is logged in, auto-login to dashboard (async function() { try { var sso = window.__GT_SSO; var url = sso.base + '/api/tutor-dashboard/auth/sso?sso_email=' + encodeURIComponent(sso.email) + '&sso_ts=' + sso.ts + '&sso_sig=' + sso.sig; var r = await fetch(url); var j = await r.json(); if (j.success && j.token) { localStorage.setItem('tutor_token', j.token); console.log('[SSO] Auto-login success for', sso.email); } } catch(e) { console.warn('[SSO] Auto-login failed:', e.message); } })(); }
(function(){ const API='https://getutor-automation.onrender.com/api/tutor-dashboard'; const C=document.getElementById('gtd-main'); const overlay=document.getElementById('gtd-login-overlay'); const toastEl=document.getElementById('gtd-toast'); let token=localStorage.getItem('tutor_token'); let profile=null;
// ===== WordPress SSO Auto-Login ===== async function ssoCheck(){ var params=new URLSearchParams(window.location.search); var ssoEmail=params.get('sso_email'),ssoTs=params.get('sso_ts'),ssoSig=params.get('sso_sig'); if(!ssoEmail||!ssoTs||!ssoSig) return; // Client-side timestamp check: reject SSO links older than 5 minutes var ssoAge=Math.abs(Date.now()/1000-parseInt(ssoTs)); if(isNaN(ssoAge)||ssoAge>300){console.warn('[SSO] Link expired ('+Math.round(ssoAge)+'s old)');toast('登入連結已過期,請重新登入','error');window.history.replaceState({},'',window.location.pathname);return} // Show loading spinner, hide other panels var ssoLoading=document.getElementById('gtd-sso-loading'); var ssoPanel=document.getElementById('gtd-panel-sso'); if(ssoLoading)ssoLoading.style.display='block'; if(ssoPanel)ssoPanel.style.display='none'; try{ var r=await fetch(API+'/auth/sso?sso_email='+encodeURIComponent(ssoEmail)+'&sso_ts='+ssoTs+'&sso_sig='+ssoSig); var j=await r.json(); if(j.success&&j.token){token=j.token;localStorage.setItem('tutor_token',token)} else if(ssoLoading){ssoLoading.style.display='none';if(ssoPanel)ssoPanel.style.display='block'} }catch(e){ console.warn('[SSO] Auto-login failed:',e.message); if(ssoLoading)ssoLoading.style.display='none';if(ssoPanel)ssoPanel.style.display='block'; toast('自動登入失敗,請手動登入','error'); } window.history.replaceState({},'',window.location.pathname); }
// ===== Google Sign-In (shared GIS handler) ===== var _googleMode='login'; // 'login' or 'link' var _googleModeLock=false; // prevent race condition between login and link var _googleClientId=null; var _googleInitRetries=0;
async function initGoogleLogin(){ try{ var r=await fetch(API+'/auth/google-client-id'); var j=await r.json(); if(!j.clientId)return; _googleClientId=j.clientId; _googleMode='login'; google.accounts.id.initialize({ client_id:j.clientId, callback:handleGoogleCallback, auto_select:false }); google.accounts.id.renderButton( document.getElementById('gtd-google-btn'), {theme:'outline',size:'large',text:'signin_with',locale:'zh-TW',width:300} ); }catch(e){ console.warn('[Google] Init failed:',e.message); // Retry up to 3 times with backoff if(_googleInitRetries<3){ _googleInitRetries++; setTimeout(initGoogleLogin,1000*_googleInitRetries); } } } async function handleGoogleCallback(response){ // Capture mode at callback time and immediately reset to prevent race var mode=_googleMode; _googleMode='login'; _googleModeLock=false; if(mode==='link') return handleGoogleLink(response); // Login mode var errEl=document.getElementById('gtd-google-err'); var loadEl=document.getElementById('gtd-google-loading'); var btnWrap=document.getElementById('gtd-google-btn-wrap'); errEl.style.display='none'; if(btnWrap)btnWrap.style.display='none'; if(loadEl)loadEl.style.display='block'; try{ var r=await fetch(API+'/auth/google',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({credential:response.credential}) }); var j=await r.json(); if(j.success&&j.token){ token=j.token;localStorage.setItem('tutor_token',token); overlay.style.display='none';C.style.display=''; _googleMode='login'; await loadDashboard(); }else{ errEl.textContent=j.error||'Google 登入失敗';errEl.style.display='block'; if(btnWrap)btnWrap.style.display='flex';if(loadEl)loadEl.style.display='none'; } }catch(e){ errEl.textContent=e.message||'網絡錯誤';errEl.style.display='block'; if(btnWrap)btnWrap.style.display='flex';if(loadEl)loadEl.style.display='none'; } } // Init Google button when GIS library loads if(typeof google!=='undefined'&&google.accounts)initGoogleLogin(); else window.addEventListener('load',function(){ // Retry with increasing delays if Google SDK not loaded function tryInitGoogle(attempt){ if(typeof google!=='undefined'&&google.accounts){initGoogleLogin();return} if(attempt<5)setTimeout(function(){tryInitGoogle(attempt+1)},500*attempt); } tryInitGoogle(1); }); // ===== Google Account Link (inside dashboard) ===== async function initGoogleLinkSection(){ var statusEl=document.getElementById('gtd-google-link-status'); var btnWrap=document.getElementById('gtd-google-link-btn-wrap'); if(!statusEl)return; try{ var st=await api('GET','/auth/google-status'); if(st.linked){ statusEl.textContent='已綁定 Google 帳戶'; statusEl.style.color='var(--success,#16a34a)'; if(btnWrap)btnWrap.innerHTML='✓ 已綁定'; return; } statusEl.textContent='未綁定 — 綁定後可用 Google 快速登入'; if(!_googleClientId||typeof google==='undefined'||!google.accounts){ if(btnWrap)btnWrap.innerHTML='Google SDK 未載入'; return; } // Render link button (uses same GIS init, mode switches on click) // Set mode to link only when user is about to click link button _googleMode='link'; _googleModeLock=true; google.accounts.id.renderButton( document.getElementById('gtd-google-link-btn'), {theme:'outline',size:'medium',text:'signin_with',locale:'zh-TW',width:240} ); }catch(e){statusEl.textContent='無法檢查 Google 綁定狀態'} }
async function handleGoogleLink(response){ _googleMode='login'; // reset mode after link attempt var statusEl=document.getElementById('gtd-google-link-status'); var btnWrap=document.getElementById('gtd-google-link-btn-wrap'); var errEl=document.getElementById('gtd-google-link-err'); var okEl=document.getElementById('gtd-google-link-ok'); errEl.style.display='none';okEl.style.display='none'; try{ var r=await api('POST','/auth/link-google',{credential:response.credential}); if(r.success){ okEl.textContent='已成功綁定 Google 帳戶('+r.googleEmail+')— 下次可直接用 Google 登入!'; okEl.style.display='block'; statusEl.textContent='已綁定:'+r.googleEmail; statusEl.style.color='var(--success,#16a34a)'; if(btnWrap)btnWrap.innerHTML='✓ 已綁定'; }else{ errEl.textContent=r.error||'綁定失敗';errEl.style.display='block'; } }catch(e){ errEl.textContent=e.message||'網絡錯誤';errEl.style.display='block'; } }
// ===== Helpers ===== function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML} function escA(s){return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/=500)throw new Error('伺服器繁忙,請稍後再試'); throw new Error(msg||'操作失敗,請稍後再試'); } return await r.json().catch(function(){return{}}); }
// ===== Magic Link Email Login ===== document.getElementById('gtd-magic-btn').addEventListener('click',async function(){ var email=document.getElementById('gtd-magic-email').value.trim(); var msg=document.getElementById('gtd-magic-msg'); msg.textContent='';msg.style.color=''; if(!email||!email.includes('@')){ msg.textContent='請輸入有效嘅 Email 地址'; msg.style.color='var(--error,#dc2626)';return; } this.disabled=true;this.textContent='發送中...'; try{ var r=await fetch(API+'/auth/send-magic-link',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({email:email}) }); var j=await r.json().catch(function(){return{}}); if(r.status===429){ msg.textContent=j.error||'請求太頻繁,請稍後再試'; msg.style.color='var(--error,#dc2626)'; }else if(!r.ok){ msg.textContent=j.error||'暫時無法發送,請稍後再試'; msg.style.color='var(--error,#dc2626)'; }else{ msg.innerHTML='✓ 登入連結已發送到 '+esc(email)+',請查收信箱(包括垃圾郵件夾)'; msg.style.color='var(--success,#059669)'; } }catch(e){ msg.textContent='發送失敗,請稍後再試'; msg.style.color='var(--error,#dc2626)'; }finally{this.disabled=false;this.textContent='發送登入連結'} }); document.getElementById('gtd-magic-email').addEventListener('keydown',function(e){if(e.key==='Enter')document.getElementById('gtd-magic-btn').click()});
// ===== Email+Password Auth ===== document.getElementById('gtd-login-btn').addEventListener('click',async function(){ var email=document.getElementById('gtd-login-email').value.trim(); var pass=document.getElementById('gtd-login-pass').value; var err=document.getElementById('gtd-login-err'); err.textContent=''; if(!email||!pass){err.textContent='請填寫 Email 同密碼';return} this.disabled=true;this.textContent='登入中...'; try{ var r=await api('POST','/auth/login',{email:email,password:pass}); token=r.token;localStorage.setItem('tutor_token',token); overlay.style.display='none';C.style.display=''; await loadDashboard(); }catch(e){err.textContent=e.message||'登入失敗'} finally{this.disabled=false;this.textContent='登入'} });
// Enter key support for login document.getElementById('gtd-login-pass').addEventListener('keydown',function(e){if(e.key==='Enter')document.getElementById('gtd-login-btn').click()});
var claimData=null; document.getElementById('gtd-claim-btn').addEventListener('click',async function(){ var email=document.getElementById('gtd-claim-email').value.trim(); var phone=document.getElementById('gtd-claim-phone').value.trim(); var err=document.getElementById('gtd-claim-err'); err.textContent=''; if(!email||!phone){err.textContent='請填寫 Email 同電話號碼';return} this.disabled=true;this.textContent='驗證中...'; try{ var r=await api('POST','/auth/claim',{email:email,phone:phone}); claimData={email:email,phone:phone}; var prev=document.getElementById('gtd-claim-preview'); prev.innerHTML='
姓名:'+esc(r.preview.name||'未填寫')+'
院校:'+esc(r.preview.school||'未填寫')+'
職業:'+esc(r.preview.occupation||'未填寫')+'
'; document.getElementById('gtd-otp-hint').textContent='驗證碼已發送到 '+(r.maskedEmail||email)+',請查收(5 分鐘內有效)'; document.getElementById('gtd-claim-step1').style.display='none'; document.getElementById('gtd-claim-step-otp').style.display='block'; }catch(e){err.textContent=e.message||'驗證失敗'} finally{this.disabled=false;this.textContent='驗證身份'} });
document.getElementById('gtd-claim-otp-btn').addEventListener('click',async function(){ var code=document.getElementById('gtd-claim-otp').value.trim(); var err=document.getElementById('gtd-claim-err'); err.textContent=''; if(!code||code.length!==6){err.textContent='請輸入 6 位驗證碼';return} if(!claimData){err.textContent='請先完成身份驗證';return} this.disabled=true;this.textContent='驗證中...'; try{ var otpResult=await api('POST','/auth/verify-otp',{email:claimData.email,code:code}); // Store claim_token from server to bind OTP verification to password setting if(otpResult.claim_token)claimData.claim_token=otpResult.claim_token; document.getElementById('gtd-claim-step-otp').style.display='none'; document.getElementById('gtd-claim-step2').style.display='block'; }catch(e){err.textContent=e.message||'驗證碼錯誤'} finally{this.disabled=false;this.textContent='確認驗證碼'} });
document.getElementById('gtd-claim-setpw-btn').addEventListener('click',async function(){
var pass=document.getElementById('gtd-claim-pass').value;
var pass2=document.getElementById('gtd-claim-pass2').value;
var err=document.getElementById('gtd-claim-err');
err.textContent='';
if(!pass||pass.length<8){err.textContent='密碼最少 8 個字元';return}
if(pass!==pass2){err.textContent='兩次密碼不一致';return}
if(!claimData){err.textContent='請先完成身份驗證';return}
this.disabled=true;this.textContent='建立中...';
try{
await api('POST','/auth/set-password',{email:claimData.email,phone:claimData.phone,password:pass,claim_token:claimData.claim_token||''});
var r=await api('POST','/auth/login',{email:claimData.email,password:pass});
token=r.token;localStorage.setItem('tutor_token',token);
overlay.style.display='none';C.style.display='';
toast('帳號建立成功!歡迎使用 Dashboard','success');
await loadDashboard();
}catch(e){err.textContent=e.message||'建立帳號失敗'}
finally{this.disabled=false;this.textContent='建立帳號'}
});
// ===== Check Auth on Load =====
async function checkAuth(){
if(!token){overlay.style.display='';return}
try{
await api('GET','/auth/me');
overlay.style.display='none';C.style.display='';
await loadDashboard();
}catch(e){
token=null;localStorage.removeItem('tutor_token');
overlay.style.display='';
}
}
// ===== Load Dashboard =====
async function loadDashboard(){
// Show loading state
var heroH2=C.querySelector('.gtd-hero-info h2');
if(heroH2)heroH2.textContent='載入中...';
try{
profile=await api('GET','/profile');
populateAll(profile);
// Load stats separately
try{var stats=await api('GET','/stats');populateStats(stats)}catch(e){}
try{loadTrialRecords()}catch(e){}
try{initGoogleLinkSection()}catch(e){console.warn('[GoogleLink]',e.message)}
// Wizard for incomplete profiles
try{initWizard(profile.profileCompleteness||0)}catch(e){console.warn('[Wizard]',e.message)}
// Sprint 3: Earnings & Rating tabs
try{initEarningsRatingTabs()}catch(e){console.warn('[EarningsRating]',e.message)}
// Sprint 4: Referral Program
try{initReferralSection()}catch(e){console.warn('[Referral]',e.message)}
// Feature 21: Progressive Commission
try{initCommissionSection()}catch(e){console.warn('[Commission]',e.message)}
// Feature 19: Teaching Tips
try{initTipsSection()}catch(e){console.warn('[Tips]',e.message)}
// Feature 20: Parent Review Invitations
try{initReviewSection()}catch(e){console.warn('[Reviews]',e.message)}
// Sprint 5: Premium Plan
try{loadPremiumStatus()}catch(e){console.warn('[Premium]',e.message)}
}catch(e){toast('載入 Profile 失敗: '+e.message,'error')}
}
// ===== Populate =====
function populateAll(p){
if(!p){toast('Profile 資料不完整','error');return}
p.personal=p.personal||{};p.education=p.education||{};p.exams=p.exams||{};p.teaching=p.teaching||{mode:{}};p.media=p.media||{};p.subjects=p.subjects||[];p.areas=p.areas||[];p.schedule=p.schedule||{};
// Hero + Nav
var name=p.personal.chineseName||p.personal.englishName||p.tutorId;
var heroH2=C.querySelector('.gtd-hero-info h2');
if(heroH2)heroH2.textContent=name+' 的教學檔案';
var navName=document.getElementById('gtd-nav-name');
if(navName)navName.textContent=name;
updateRing(p.profileCompleteness||0);
// Personal fields
setField('chineseName',p.personal.chineseName);
setField('englishName',p.personal.englishName);
setField('gender',p.personal.gender);
setField('phone',p.personal.phone);
var hr=p.personal.hourlyRate||{};
setField('hourlyRate',(hr.min&&hr.max)?'$'+hr.min+' - $'+hr.max:'');
setField('languages',(p.personal.languages||[]).join('、'));
var accBtn=C.querySelector('#gtd-accepting');
if(accBtn){if(p.personal.acceptingNewStudents)accBtn.classList.add('on');else accBtn.classList.remove('on')}
// Education fields
setField('education',p.education.education);
setField('school',p.education.school);
setField('studyLevel',p.education.studyLevel);
setField('occupation',p.education.occupation);
setField('workExp',p.education.workExp);
setField('tutorExp',p.education.tutorExp);
setField('canTeachLevel',p.education.canTeachLevel);
// Exams
populateExams(p.exams);
// Subjects
populateSubjects(p.subjects);
// Areas
populateAreas(p.areas);
// Teaching mode
C.querySelectorAll('.gtd-mode-btn').forEach(function(btn){
var mode=btn.dataset.mode;
if(p.teaching.mode[mode])btn.classList.add('active');else btn.classList.remove('active');
});
// Specialties
C.querySelectorAll('.gtd-specialty-tag').forEach(function(tag){
if((p.teaching.specialties||[]).indexOf(tag.textContent.trim())!==-1)tag.classList.add('active');
else tag.classList.remove('active');
});
// Self intro
var introEl=C.querySelector('.gtd-intro-display');
if(introEl)introEl.textContent=p.selfIntro||'';
// Photo
var photoCircle=C.querySelector('#gtd-photo-preview');
if(photoCircle&&p.media.photoUrl){
photoCircle.innerHTML='';
}
// Videos
if(p.media.introVideoUrl){var iv=document.getElementById('gtd-intro-video');if(iv)iv.value=p.media.introVideoUrl;embedVideo('gtd-intro-embed',p.media.introVideoUrl)}
if(p.media.teachingVideoUrl){var tv=document.getElementById('gtd-teach-video');if(tv)tv.value=p.media.teachingVideoUrl;embedVideo('gtd-teach-embed',p.media.teachingVideoUrl)}
// Schedule
populateSchedule(p.schedule);
// Public preview
populatePreview(p);
}
var FIELD_HINTS={chineseName:'姓名影響信任度',englishName:'方便外籍家長搵你',gender:'配對性別偏好',school:'三大院校成交率高 30%',education:'學歷影響定價',occupation:'全職導師更受歡迎',workExp:'有經驗嘅導師更搶手',tutorExp:'補習經驗越長越受信賴',canTeachLevel:'影響配對年級範圍',hourlyRate:'令家長快速了解收費',languages:'多語言 = 更多機會'}; function setField(name,val){ var el=C.querySelector('[data-field="'+name+'"] .gtd-val'); if(!el)return; if(val){el.innerHTML=esc(val)} else{ var hint=FIELD_HINTS[name]||''; el.innerHTML=''; } }
function updateRing(pct){ pct=Math.max(0,Math.min(100,pct)); var pctEl=C.querySelector('.gtd-ring-pct');if(pctEl)pctEl.textContent=pct+'%'; var fg=C.querySelector('.gtd-ring .fg');if(fg)fg.setAttribute('stroke-dashoffset',314-Math.round(314*pct/100)); // Update missing items panel if(profile)updateMissingPanel(profile,pct); }
function getMissingItems(p){
var items=[];
var pers=p.personal||{},edu=p.education||{},media=p.media||{},teaching=p.teaching||{};
if(!(pers.chineseName||pers.englishName))items.push({label:'填寫姓名',pts:5,section:'personal',field:'chineseName'});
if(!media.photoUrl)items.push({label:'上傳個人照片',pts:5,section:'photo',desc:'有相嘅導師成功率高 65%'});
if(!p.selfIntro||p.selfIntro.length<100)items.push({label:p.selfIntro?'自我介紹寫夠 100 字':'撰寫自我介紹',pts:p.selfIntro&&p.selfIntro.length>=30?7:15,section:'selfIntro',desc:'配對率 +34%'});
if(!p.subjects||!p.subjects.length)items.push({label:'選擇可教科目',pts:15,section:'subjects',desc:'配對嘅基礎條件'});
if(!p.areas||!p.areas.length)items.push({label:'選擇補習地區',pts:10,section:'areas',desc:'揀多幾區更多機會'});
var exams=p.exams||{},examCnt=Object.keys(exams.dse||{}).length+Object.keys(exams.alevel||{}).length+Object.keys(exams.igcse||{}).length+Object.keys(exams.ib||{}).length;
if(!examCnt)items.push({label:'填寫公開試成績',pts:10,section:'exams',desc:'被選中機會 +25%'});
var slots=Object.values(p.schedule||{}).filter(function(v){return v}).length;
if(slots<5)items.push({label:slots?'選擇更多補習時段':'填寫補習時段',pts:slots?5:10,section:'schedule',desc:'令配對更精準'});
if(!media.introVideoUrl&&!media.teachingVideoUrl)items.push({label:'上傳影片介紹',pts:5,section:'video',desc:'配對率 x2'});
if(!edu.school)items.push({label:'填寫就讀院校',pts:4,section:'education',field:'school'});
if(!edu.occupation)items.push({label:'填寫職業',pts:3,section:'education',field:'occupation'});
if(!(edu.workExp||edu.tutorExp))items.push({label:'填寫補習經驗',pts:3,section:'education',field:'tutorExp'});
var modeCnt=Object.values((teaching.mode||{})).filter(function(v){return v}).length;
if(!modeCnt)items.push({label:'選擇教學模式',pts:2,section:'mode'});
if(!(teaching.specialties&&teaching.specialties.length))items.push({label:'選擇教學特色',pts:3,section:'specialties'});
items.sort(function(a,b){return b.pts-a.pts});
return items;
}
function updateMissingPanel(p,pct){
var panel=document.getElementById('gtd-missing-panel');
var label=document.getElementById('gtd-missing-label');
var list=document.getElementById('gtd-missing-list');
var heroMsg=document.getElementById('gtd-hero-msg');
var compare=document.getElementById('gtd-income-compare');
if(!panel||!label||!list)return;
var items=getMissingItems(p);
if(pct>=100||!items.length){
panel.style.display='none';
if(compare)compare.style.display='none';
if(heroMsg){heroMsg.textContent='你嘅檔案已完整!配對率已達最高。';heroMsg.style.color='var(--success)';heroMsg.style.fontWeight='600'}
return;
}
panel.style.display='';
if(compare)compare.style.display='flex';
var totalPts=items.reduce(function(s,i){return s+i.pts},0);
label.textContent='僅差 '+items.length+' 個項目即可完成 → 預計 +'+totalPts+'% 完成度';
if(heroMsg){
if(pct<50)heroMsg.textContent='你嘅檔案仲有好大改善空間!完成以下項目,令更多家長搵到你。';
else if(pct<80)heroMsg.textContent='已經唔錯!再完成幾個項目就可以大幅提升配對機會。';
else heroMsg.textContent='就快完成!差少少就可以解鎖最高配對率。';
heroMsg.style.color='';heroMsg.style.fontWeight='';
}
var h='';
var sectionMap={photo:'gtd-section-photo',selfIntro:'gtd-card-full[data-field="selfIntro"]',subjects:'gtd-section-subjects',areas:'gtd-section-areas',schedule:'gtd-section-schedule',video:'gtd-section-video',exams:'gtd-exam-dse'};
var top=items.slice(0,5);
for(var i=0;i ';
}
list.innerHTML=h;
list.querySelectorAll('.gtd-missing-item').forEach(function(el){
el.addEventListener('click',function(){
var sec=this.dataset.scroll;
var target=null;
if(sec==='photo')target=document.getElementById('gtd-section-photo');
else if(sec==='selfIntro')target=C.querySelector('[data-field="selfIntro"].gtd-intro-display');
else if(sec==='subjects')target=document.getElementById('gtd-section-subjects');
else if(sec==='areas')target=document.getElementById('gtd-section-areas');
else if(sec==='schedule')target=document.getElementById('gtd-section-schedule');
else if(sec==='video')target=document.getElementById('gtd-section-video');
else if(sec==='exams')target=document.getElementById('gtd-exam-dse');
else if(sec==='personal'||sec==='education')target=C.querySelector('[data-field="'+el.dataset.scroll+'"]')||(sec==='education'?C.querySelector('[data-field="school"]'):null);
else if(sec==='mode')target=document.getElementById('gtd-teach-mode');
else if(sec==='specialties')target=document.getElementById('gtd-specialties');
if(target){target.scrollIntoView({behavior:'smooth',block:'center'});target.style.boxShadow='0 0 0 3px rgba(30,58,95,0.35)';setTimeout(function(){target.style.boxShadow=''},2000)}
});
});
} // ===== Exams =====
var GRADE_MAP={dse:{'5**':{cls:'gtd-dse-5ss',w:100},'5*':{cls:'gtd-dse-5s',w:85},'5':{cls:'gtd-dse-5',w:71},'4':{cls:'gtd-dse-4',w:57},'3':{cls:'gtd-dse-3',w:43},'2':{cls:'gtd-dse-low',w:28},'1':{cls:'gtd-dse-low',w:14}},alevel:{"A*":{cls:'gtd-dse-5ss',w:100},A:{cls:'gtd-dse-5s',w:87},B:{cls:'gtd-dse-5',w:74},C:{cls:'gtd-dse-4',w:61},D:{cls:'gtd-dse-3',w:48},E:{cls:'gtd-dse-low',w:35}},igcse:{'9':{cls:'gtd-dse-5ss',w:100},'8':{cls:'gtd-dse-5s',w:89},'7':{cls:'gtd-dse-5',w:78},'6':{cls:'gtd-dse-4',w:67},'5':{cls:'gtd-dse-3',w:56},'4':{cls:'gtd-dse-low',w:45}},ib:{'7':{cls:'gtd-dse-5ss',w:100},'6':{cls:'gtd-dse-5s',w:86},'5':{cls:'gtd-dse-5',w:72},'4':{cls:'gtd-dse-4',w:58},'3':{cls:'gtd-dse-3',w:44}}};
function getGradeInfo(system,grade){return(GRADE_MAP[system]&&GRADE_MAP[system][grade])||{cls:'gtd-dse-low',w:30}} function populateExams(exams){
['dse','alevel','igcse','ib'].forEach(function(sys){
var panel=document.getElementById('gtd-exam-'+sys);if(!panel)return;
var container=panel.querySelector('.gtd-dse');
if(!container){container=document.createElement('div');container.className='gtd-dse';panel.prepend(container)}
container.innerHTML='';
var data=exams[sys]||{};
var entries=Object.entries(data);
if(entries.length===0){
container.innerHTML=' 未有'+sys.toUpperCase()+' 成績記錄 ';
return;
}
entries.forEach(function(e){
var subj=e[0],grade=e[1];
var info=getGradeInfo(sys,grade);
var row=document.createElement('div');row.className='gtd-dse-row';
row.innerHTML=''+esc(subj)+' ';
container.appendChild(row);
});
});
} // ===== Subjects =====
function populateSubjects(subjects){
var container=C.querySelector('#gtd-subject-tags');if(!container)return;
// Remove existing tags (keep the input wrap)
container.querySelectorAll('.gtd-subj-tag').forEach(function(t){t.remove()});
var inputWrap=container.querySelector('.gtd-subj-input-wrap');
(subjects||[]).forEach(function(s){
var tag=document.createElement('span');tag.className='gtd-subj-tag';tag.dataset.subj=s;
tag.innerHTML=esc(s)+'';
container.insertBefore(tag,inputWrap);
tag.querySelector('.gtd-subj-remove').onclick=function(){tag.remove();syncQuickSubjects();debouncedSaveSubjects()};
});
syncQuickSubjects();
}
function syncQuickSubjects(){
var selected=getSelectedSubjects();
C.querySelectorAll('.gtd-subj-quick').forEach(function(btn){
if(selected.indexOf(btn.dataset.subj)!==-1)btn.classList.add('active');else btn.classList.remove('active');
});
}
function getSelectedSubjects(){
return Array.from(C.querySelectorAll('#gtd-subject-tags .gtd-subj-tag')).map(function(t){return t.dataset.subj});
} // ===== Areas =====
function populateAreas(areas){
C.querySelectorAll('.gtd-area-chip').forEach(function(chip){
if((areas||[]).indexOf(chip.dataset.area)!==-1)chip.classList.add('active');else chip.classList.remove('active');
});
updateAreaCount();
}
function updateAreaCount(){
var cnt=C.querySelectorAll('.gtd-area-chip.active').length;
var el=document.getElementById('gtd-area-count');if(el)el.textContent='已選 '+cnt+' 區';
} // ===== Schedule =====
function populateSchedule(schedule){
C.querySelectorAll('.gtd-sch-cell').forEach(function(cell){
if(schedule&&schedule[cell.dataset.slot])cell.classList.add('active');else cell.classList.remove('active');
});
} // ===== Stats =====
function populateStats(stats){
var boxes=C.querySelectorAll('.gtd-card:nth-child(4) > div:first-child > div');
// Actually, let me use a more reliable selector
var statEls=C.querySelectorAll('[style*="grid-template-columns:1fr 1fr"] > div');
if(statEls.length>=2){
var n1=statEls[0].querySelector('div:first-child');if(n1)n1.textContent=stats.totalMatches||'--';
var n2=statEls[1].querySelector('div:first-child');if(n2)n2.textContent=(stats.successRate||'--')+(stats.successRate?'%':'');
}
} // ===== Trial Records =====
async function loadTrialRecords(){
try{
var data=await api('GET','/trial-records');
var list=document.getElementById('gtd-trial-list');
var badge=document.getElementById('gtd-trial-count');
var recs=data.records||[];
if(badge)badge.textContent=recs.length||'';
if(!recs.length){list.innerHTML=' ';return}
var h='';
for(var i=0;i ';
h+=' ';
h+=' ';
if(!ok&&r.cancelReason)h+=' ';
h+=' ';
h+=' ';
h+=' ';
h+=' ';
}
list.innerHTML=h;
}catch(e){
var el=document.getElementById('gtd-trial-list');
if(el)el.innerHTML=' ';
}
} // ===== Public Preview =====
function populatePreview(p){
var avatar=C.querySelector('.gtd-pp-avatar');
if(avatar){
var name=p.personal.chineseName||p.personal.englishName||'';
if(p.media.photoUrl)avatar.innerHTML=' // ===== Video Embed =====
function ytId(url){if(!url)return null;var m=url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|shorts\/))([a-zA-Z0-9_-]{11})/);return m?m[1]:null}
function embedVideo(embedId,url){
var emb=document.getElementById(embedId);if(!emb||!url)return;
// Validate URL scheme to prevent javascript:/data: injection
if(!/^https?:\/\//i.test(url)&&!url.startsWith('blob:')){return}
var vid=ytId(url);
if(vid){emb.innerHTML='';emb.className='gtd-video-embed'}
else{emb.innerHTML='';emb.className='gtd-video-upload-area has-video'}
} // ===== Save Functions =====
async function saveProfileField(section,field,value){
var body={};
if(section==='personal'||section==='education'||section==='teaching'||section==='media'){
body[section]={};body[section][field]=value;
}else if(section==='selfIntro'){
body.selfIntro=value;
}
try{
var r=await api('PUT','/profile',body);
// Update local profile cache to keep UI in sync
if(profile){
if(section==='selfIntro'){profile.selfIntro=value}
else if(profile[section]&&field){profile[section][field]=value}
if(r.profileCompleteness!==undefined)profile.profileCompleteness=r.profileCompleteness;
}
if(r.profileCompleteness!==undefined)updateRing(r.profileCompleteness);
toast('已儲存','success');
}catch(e){toast('儲存失敗: '+e.message,'error')}
} var debouncedSaveSchedule=debounce(async function(){
var schedule={};
C.querySelectorAll('.gtd-sch-cell').forEach(function(cell){schedule[cell.dataset.slot]=cell.classList.contains('active')});
try{
var r=await api('PUT','/schedule',{schedule:schedule});
if(profile)profile.schedule=schedule;
if(r.profileCompleteness!==undefined){updateRing(r.profileCompleteness);if(profile)profile.profileCompleteness=r.profileCompleteness}
toast('時段已儲存('+r.slots+' 個時段)','success');
}catch(e){toast('儲存時段失敗','error')}
},2000); var debouncedSaveSubjects=debounce(async function(){
var subjects=getSelectedSubjects();
try{
var r=await api('PUT','/subjects',{subjects:subjects});
if(profile)profile.subjects=subjects;
if(r.profileCompleteness!==undefined){updateRing(r.profileCompleteness);if(profile)profile.profileCompleteness=r.profileCompleteness}
toast('科目已儲存','success');
}catch(e){toast('儲存科目失敗','error')}
},2000); var debouncedSaveAreas=debounce(async function(){
var areas=Array.from(C.querySelectorAll('.gtd-area-chip.active')).map(function(c){return c.dataset.area});
try{
var r=await api('PUT','/areas',{areas:areas});
if(profile)profile.areas=areas;
if(r.profileCompleteness!==undefined){updateRing(r.profileCompleteness);if(profile)profile.profileCompleteness=r.profileCompleteness}
toast('地區已儲存','success');
}catch(e){toast('儲存地區失敗','error')}
},2000); // Merge mode + specialties into one debounced save to prevent race condition
var debouncedSaveTeaching=debounce(async function(){
var mode={};C.querySelectorAll('.gtd-mode-btn').forEach(function(b){mode[b.dataset.mode]=b.classList.contains('active')});
var specs=Array.from(C.querySelectorAll('.gtd-specialty-tag.active')).map(function(t){return t.textContent.trim()});
try{
var r=await api('PUT','/profile',{teaching:{mode:mode,specialties:specs}});
if(profile){profile.teaching=profile.teaching||{};profile.teaching.mode=mode;profile.teaching.specialties=specs}
if(r.profileCompleteness!==undefined){updateRing(r.profileCompleteness);if(profile)profile.profileCompleteness=r.profileCompleteness}
toast('教學設定已儲存','success');
}catch(e){toast('儲存失敗','error')}
},1500);
// Keep aliases for backward compatibility with event handlers
var debouncedSaveMode=debouncedSaveTeaching;
var debouncedSaveSpecialties=debouncedSaveTeaching; // ===== UI Interactions ===== // Video compress
function compressVideo(file){
return new Promise(function(resolve,reject){
var v=document.createElement('video');v.src=URL.createObjectURL(file);v.muted=true;
v.onloadedmetadata=function(){
var scale=Math.min(1,720/v.videoWidth);
var w=Math.round(v.videoWidth*scale),h=Math.round(v.videoHeight*scale);
var canvas=document.createElement('canvas');canvas.width=w;canvas.height=h;
var ctx=canvas.getContext('2d');
var cs=canvas.captureStream(24);
try{v.muted=false;var os=v.captureStream?v.captureStream():v.mozCaptureStream();os.getAudioTracks().forEach(function(t){cs.addTrack(t)})}catch(e){}
var mt=MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus')?'video/webm;codecs=vp9,opus':'video/webm';
var rec=new MediaRecorder(cs,{mimeType:mt,videoBitsPerSecond:800000});
var chunks=[];
rec.ondataavailable=function(e){if(e.data.size)chunks.push(e.data)};
rec.onstop=function(){resolve(new File([new Blob(chunks,{type:'video/webm'})],'video.webm',{type:'video/webm'}))};
v.currentTime=0;v.play().catch(reject);rec.start(100);
function draw(){if(v.ended||v.paused){rec.stop();return}ctx.drawImage(v,0,0,w,h);requestAnimationFrame(draw)}
requestAnimationFrame(draw);
v.onended=function(){rec.stop()};
setTimeout(function(){if(rec.state==='recording')rec.stop()},130000);
};
v.onerror=reject;v.load();
});
} // Video upload areas
C.querySelectorAll('.gtd-video-upload-area').forEach(function(area){
area.addEventListener('click',function(){
if(this.querySelector('video')||this.querySelector('iframe'))return;
var fi=this.nextElementSibling;
if(fi&&fi.type==='file')fi.click();
});
});
C.querySelectorAll('.gtd-video-file').forEach(function(inp){
inp.addEventListener('change',function(){
var file=this.files[0];if(!file)return;
var embed=document.getElementById(this.dataset.embed);
var st=document.getElementById(this.dataset.status);
var fieldType=this.dataset.field||''; // 'introVideo' or 'teachVideo'
var apiField=fieldType==='introVideo'?'introVideoUrl':'teachingVideoUrl';
if(!file.type.match(/^video\/(mp4|quicktime|webm|x-matroska)$/)){st.textContent='✕ 只接受 MP4/MOV/WebM';st.style.color='var(--error)';return}
var tmpV=document.createElement('video');tmpV.src=URL.createObjectURL(file);
tmpV.onloadedmetadata=function(){
if(tmpV.duration>120){st.textContent='✕ 影片不能超過 2 分鐘';st.style.color='var(--error)';return}
// Helper: upload file to server after preview
function uploadVideoFile(fileToUpload){
st.textContent='上傳到伺服器...';st.style.color='var(--gold)';
var fd=new FormData();fd.append('video',fileToUpload);fd.append('type',fieldType);
api('POST','/video-upload',fd).then(function(r){
if(r.videoUrl){
saveProfileField('media',apiField,r.videoUrl);
st.textContent='✓ 已儲存';st.style.color='var(--success)';
}else{
st.textContent='✓ 已預覽(需手動儲存連結)';st.style.color='var(--warning,#d97706)';
}
}).catch(function(){
st.textContent='✓ 已預覽(自動上傳失敗,可手動貼連結)';st.style.color='var(--warning,#d97706)';
});
}
if(file.size>10*1024*1024){
st.textContent='壓縮中...';st.style.color='var(--gold)';
compressVideo(file).then(function(compressed){
var url=URL.createObjectURL(compressed);
embed.innerHTML='';
embed.classList.add('has-video');
uploadVideoFile(compressed);
}).catch(function(){
var url=URL.createObjectURL(file);
embed.innerHTML='';
embed.classList.add('has-video');
uploadVideoFile(file);
});
}else{
var url=URL.createObjectURL(file);
embed.innerHTML='';
embed.classList.add('has-video');
uploadVideoFile(file);
}
};
});
}); // Video link toggles
C.querySelectorAll('.gtd-video-link-toggle').forEach(function(btn){
btn.addEventListener('click',function(){
var t=document.getElementById(this.dataset.target);
if(t)t.style.display=t.style.display==='none'?'flex':'none';
});
}); // Video link save
C.querySelectorAll('.gtd-video-save').forEach(function(btn){
btn.addEventListener('click',async function(){
var inp=document.getElementById(this.dataset.input),emb=document.getElementById(this.dataset.embed);
var v=inp.value.trim();
var field=this.dataset.field==='introVideo'?'introVideoUrl':'teachingVideoUrl';
embedVideo(this.dataset.embed,v);
await saveProfileField('media',field,v);
});
}); // Exam tabs
C.querySelectorAll('.gtd-exam-tab').forEach(function(tab){
tab.addEventListener('click',function(){
C.querySelectorAll('.gtd-exam-tab').forEach(function(t){t.classList.remove('active')});
C.querySelectorAll('.gtd-exam-panel').forEach(function(p){p.classList.remove('active')});
this.classList.add('active');
var panel=document.getElementById('gtd-exam-'+this.dataset.exam);
if(panel)panel.classList.add('active');
});
}); // Teaching mode toggle
C.querySelectorAll('.gtd-mode-btn').forEach(function(btn){
btn.addEventListener('click',function(){this.classList.toggle('active');debouncedSaveMode()});
}); // Specialty tags
C.querySelectorAll('.gtd-specialty-tag').forEach(function(tag){
tag.addEventListener('click',function(){this.classList.toggle('active');debouncedSaveSpecialties()});
}); // Accepting toggle
var accBtn=C.querySelector('#gtd-accepting');
if(accBtn)accBtn.addEventListener('click',function(){
this.classList.toggle('on');
saveProfileField('personal','acceptingNewStudents',this.classList.contains('on'));
}); // Schedule cells
C.querySelectorAll('.gtd-sch-cell').forEach(function(cell){
cell.addEventListener('click',function(){this.classList.toggle('active');debouncedSaveSchedule()});
}); // Schedule save button (immediate)
var schSaveBtn=C.querySelector('.gtd-sch-actions .gtd-btn-gold');
if(schSaveBtn)schSaveBtn.addEventListener('click',function(){debouncedSaveSchedule();debouncedSaveSchedule.flush&&debouncedSaveSchedule.flush()}); // Area chips
C.querySelectorAll('.gtd-area-chip').forEach(function(chip){
chip.addEventListener('click',function(){this.classList.toggle('active');updateAreaCount();debouncedSaveAreas()});
}); // Subject search + add/remove
(function(){
var subjectInput=C.querySelector('#gtd-subject-search');
var subjectTags=C.querySelector('#gtd-subject-tags');
var subjectDropdown=C.querySelector('#gtd-subject-dropdown');
if(!subjectInput||!subjectTags)return;
var allSubjects=['中文','英文','數學','通識','物理','化學','生物','經濟','歷史','地理','中國歷史','ICT','BAFS','視覺藝術','音樂','體育','M1','M2','數學(延伸一)','數學(延伸二)','中國文學','英語文學','日文','韓文','法文','西班牙文','德文','普通話','小學全科','小學英文','小學數學','小學中文','Mathematics','Chemistry','Physics','Biology','Economics','English Language','English Literature','History','Geography','Further Mathematics','Accounting','Business Studies','Computer Science','Art & Design','Music','Psychology','Sociology','鋼琴','結他','小提琴','大提琴','長笛','單簧管','聲樂','素描','油畫','水彩','書法','國畫','游泳','籃球','足球','羽毛球','乒乓球','田徑','瑜伽','普通話拼音','IELTS','TOEFL','SAT','GRE','功課輔導']; function addSubject(name){
name=name.trim();if(!name)return;
if(getSelectedSubjects().indexOf(name)!==-1)return;
var tag=document.createElement('span');tag.className='gtd-subj-tag';tag.dataset.subj=name;
tag.innerHTML=esc(name)+'';
var inputWrap=subjectTags.querySelector('.gtd-subj-input-wrap');
subjectTags.insertBefore(tag,inputWrap);
tag.querySelector('.gtd-subj-remove').onclick=function(){tag.remove();syncQuickSubjects();debouncedSaveSubjects()};
syncQuickSubjects();
debouncedSaveSubjects();
}
function showDropdown(filter){
var sel=getSelectedSubjects();
var filtered=allSubjects.filter(function(s){return sel.indexOf(s)===-1&&s.toLowerCase().indexOf(filter.toLowerCase())!==-1});
if(!filter||!filtered.length){subjectDropdown.style.display='none';return}
subjectDropdown.innerHTML=filtered.slice(0,8).map(function(s){return' '}).join('');
subjectDropdown.style.display='block';
subjectDropdown.querySelectorAll('.gtd-subj-option').forEach(function(opt){
opt.onmousedown=function(e){e.preventDefault();addSubject(this.textContent);subjectInput.value='';subjectDropdown.style.display='none'};
});
}
subjectInput.addEventListener('input',function(){showDropdown(this.value)});
subjectInput.addEventListener('focus',function(){if(this.value)showDropdown(this.value)});
subjectInput.addEventListener('blur',function(){setTimeout(function(){subjectDropdown.style.display='none'},150)});
subjectInput.addEventListener('keydown',function(e){
if(e.key==='Enter'){e.preventDefault();var v=this.value.trim();if(v){addSubject(v);this.value='';subjectDropdown.style.display='none'}}
});
// Quick add buttons
C.querySelectorAll('.gtd-subj-quick').forEach(function(btn){
btn.addEventListener('click',function(){
this.classList.toggle('active');
if(this.classList.contains('active')){addSubject(this.dataset.subj)}
else{var sel=subjectTags.querySelector('[data-subj="'+this.dataset.subj+'"]');if(sel){sel.remove();debouncedSaveSubjects()}}
});
});
})(); // Photo upload
var photoInput=document.getElementById('gtd-photo-input');
if(photoInput)photoInput.addEventListener('change',async function(){
var file=this.files[0];if(!file)return;
var st=document.getElementById('gtd-photo-status');
if(file.size>5*1024*1024){if(st){st.textContent='✕ 相片不能超過 5MB';st.style.color='var(--error)'}return}
if(st){st.textContent='上傳中...';st.style.color='var(--gold)'}
var fd=new FormData();fd.append('photo',file);
try{
var r=await api('POST','/photo',fd);
var circle=document.getElementById('gtd-photo-preview');
if(circle)circle.innerHTML=' // Inline field editing
C.addEventListener('click',function(e){
var btn=e.target.closest('.gtd-edit-btn');if(!btn)return;
var field=btn.dataset.field||btn.closest('[data-field]').dataset.field;if(!field)return; if(field==='selfIntro'){
var d=C.querySelector('.gtd-intro-display');if(!d||d.style.display==='none')return;
var raw=d.textContent;d.style.display='none';
var w=document.createElement('div');
w.innerHTML=' ';
d.parentNode.insertBefore(w,d.nextSibling);w.querySelector('textarea').focus();
w.querySelector('.gtd-edit-cancel').onclick=function(){w.remove();d.style.display=''};
w.querySelector('.gtd-edit-save').onclick=async function(){
var val=w.querySelector('textarea').value;
d.textContent=val;d.style.display='';w.remove();
await saveProfileField('selfIntro',null,val);
var ppIntro=C.querySelector('.gtd-pp-intro');if(ppIntro)ppIntro.textContent=val;
};
return;
} var row=btn.closest('.gtd-field');if(!row)return;
var vs=row.querySelector('.gtd-val');if(!vs||vs.style.display==='none')return;
var cur=vs.querySelector('.gtd-field-empty')?'':vs.textContent.trim();
var opts=row.dataset.options||'';
vs.style.display='none';btn.style.display='none';
var w=document.createElement('span');w.style.cssText='display:flex;align-items:center;gap:6px;flex:1'; if(field==='hourlyRate'){
var parts=cur.replace(/\$/g,'').split('-').map(function(s){return s.trim()});
w.innerHTML='$ - $';
}else if(opts){
var sel='';
}else{
w.innerHTML='';
}
row.querySelector('.gtd-field-val').appendChild(w);
var inp=w.querySelector('input,select');if(inp)inp.focus();
if(inp&&inp.tagName==='INPUT'){inp.addEventListener('keydown',function(ev){if(ev.key==='Enter')w.querySelector('.gtd-edit-save').click();if(ev.key==='Escape')w.querySelector('.gtd-edit-cancel').click()})}
w.querySelector('.gtd-edit-cancel').onclick=function(){w.remove();vs.style.display='';btn.style.display=''};
w.querySelector('.gtd-edit-save').onclick=async function(){
if(field==='hourlyRate'){
var inputs=w.querySelectorAll('input');
var min=parseInt(inputs[0].value)||0,max=parseInt(inputs[1].value)||0;
vs.innerHTML=min&&max?'$'+min+' - $'+max:'未填寫';
vs.style.display='';btn.style.display='';w.remove();
await saveProfileField('personal','hourlyRate',{min:min,max:max});
}else{
var v=(w.querySelector('input')||w.querySelector('select')).value;
vs.innerHTML=v?esc(v):'未填寫';
vs.style.display='';btn.style.display='';w.remove();
// Determine section
var section='personal';
var eduFields=['education','school','studyLevel','occupation','workExp','tutorExp','canTeachLevel'];
if(eduFields.indexOf(field)!==-1)section='education';
await saveProfileField(section,field,v);
// Update hero name if relevant
if(field==='chineseName'||field==='englishName'){
var heroH2=C.querySelector('.gtd-hero-info h2');
if(heroH2)heroH2.textContent=(profile.personal.chineseName||profile.personal.englishName||'')+'的教學檔案';
populatePreview(profile);
}
}
};
}); // Exam add button
C.querySelectorAll('.gtd-exam-add').forEach(function(btn){
btn.addEventListener('click',function(){
var panel=this.closest('.gtd-exam-panel');if(!panel)return;
var sys=panel.id.replace('gtd-exam-','');
var container=panel.querySelector('.gtd-dse');
if(!container){container=document.createElement('div');container.className='gtd-dse';panel.prepend(container)}
// Remove "no records" message
var noMsg=container.querySelector('p');if(noMsg)noMsg.remove();
// Add inline form
var form=document.createElement('div');form.className='gtd-dse-row';form.style.gap='6px';
form.innerHTML='';
container.appendChild(form);
form.querySelector('input').focus();
form.querySelector('.gtd-edit-cancel').onclick=function(){form.remove()};
form.querySelector('.gtd-edit-save').onclick=async function(){
var inputs=form.querySelectorAll('input');
var subj=inputs[0].value.trim(),grade=inputs[1].value.trim();
if(!subj||!grade){toast('請填寫科目同成績','error');return}
if(!profile||!profile.exams){toast('Profile 未載入','error');return}
if(!profile.exams[sys])profile.exams[sys]={};
profile.exams[sys][subj]=grade;
form.remove();
populateExams(profile.exams);
try{
var body={};body[sys]=profile.exams[sys];
await api('PUT','/exams',body);
toast('成績已儲存','success');
}catch(e){toast('儲存成績失敗','error')}
};
});
}); // Logout button
var logoutBtn=document.getElementById('gtd-logout-btn');
if(logoutBtn)logoutBtn.addEventListener('click',async function(){
// Cancel pending saves before clearing token
debouncedSaveSchedule.cancel();debouncedSaveSubjects.cancel();debouncedSaveAreas.cancel();debouncedSaveMode.cancel();debouncedSaveSpecialties.cancel();
// Server-side token invalidation
try{await api('POST','/auth/logout',{})}catch(e){/* proceed with client logout regardless */}
token=null;profile=null;localStorage.removeItem('tutor_token');
overlay.style.display='';C.style.display='none';
}); // ===== Step Wizard =====
var wzActive=false,wzStep=1,WZ_MAP={},WZ_ALWAYS=[];
function tagWzCards(){
var g=C.querySelectorAll('.gtd-grid');
var g1=g[0]?Array.from(g[0].children).filter(function(c){return c.classList.contains('gtd-card')}):[];
var g2=g[1]?Array.from(g[1].children).filter(function(c){return c.classList.contains('gtd-card')}):[];
// Step 1: Photo, Personal
// Step 2: Education, Exams
// Step 3: Areas, Schedule, Teaching Mode
// Step 4: Subjects, SelfIntro, Video
WZ_MAP={1:[g1[0],g1[1]],2:[g1[2],g1[5]],3:[g1[7],g2[1],g1[8]],4:[g1[6],g1[9],g2[0]]};
WZ_ALWAYS=[g1[3],g1[4],g2[2]].filter(Boolean);
Object.keys(WZ_MAP).forEach(function(k){WZ_MAP[k]=WZ_MAP[k].filter(Boolean)});
}
function initWizard(pct){
tagWzCards();
var dismissed=localStorage.getItem('gtd_wz_done');
if(dismissed||pct>=60){hideWizard();return}
showWizard();
}
function showWizard(){
wzActive=true;
var el=document.getElementById('gtd-wizard');if(el)el.classList.add('active');
setWzStep(1);
}
function hideWizard(){
wzActive=false;
var el=document.getElementById('gtd-wizard');if(el)el.classList.remove('active');
var allCards=C.querySelectorAll('.gtd-grid .gtd-card');
allCards.forEach(function(c){c.style.display=''});
}
function setWzStep(n){
wzStep=n;
if(!wzActive)return;
// Update step indicators
document.querySelectorAll('.gtd-wz-step').forEach(function(s){
var sn=parseInt(s.dataset.step);
s.classList.toggle('active',sn===n);
s.classList.toggle('done',sn // ===== Mobile Schedule Presets =====
var PRESET_SLOTS={
morning:['0900','1000','1100'],
afternoon:['1200','1300','1400'],
afterschool:['1500','1530','1600','1630','1700','1730'],
evening:['1800','1830','1900','1930','2000','2030','2100','2130','2200'],
weekend:['0900','1000','1100','1200','1300','1400','1500','1530','1600','1630','1700','1730','1800','1830','1900','1930','2000','2030','2100']
};
var PRESET_DAYS={morning:['mon','tue','wed','thu','fri','sat','sun'],afternoon:['mon','tue','wed','thu','fri','sat','sun'],afterschool:['mon','tue','wed','thu','fri'],evening:['mon','tue','wed','thu','fri'],weekend:['sat','sun']};
document.querySelectorAll('.gtd-sch-preset').forEach(function(btn){
btn.addEventListener('click',function(){
var p=this.dataset.preset;
var isActive=this.classList.contains('active');
this.classList.toggle('active');
var days=PRESET_DAYS[p]||[];
var times=PRESET_SLOTS[p]||[];
days.forEach(function(day){
times.forEach(function(time){
var slot=day+'_'+time;
var cell=C.querySelector('.gtd-sch-cell[data-slot="'+slot+'"]');
if(cell){
if(isActive)cell.classList.remove('active');
else cell.classList.add('active');
}
});
});
debouncedSaveSchedule();
});
}); // ===== Subject Category Filter =====
document.querySelectorAll('.gtd-subj-cat').forEach(function(btn){
btn.addEventListener('click',function(){
document.querySelectorAll('.gtd-subj-cat').forEach(function(b){b.classList.remove('active')});
this.classList.add('active');
var cat=this.dataset.cat;
document.querySelectorAll('#gtd-subj-quick-wrap .gtd-subj-quick').forEach(function(q){
if(cat==='all'||(q.dataset.cat||'').split(',').indexOf(cat)!==-1){q.style.display=''}
else{q.style.display='none'}
});
});
}); // ===== Sprint 3: Earnings & Rating Tabs =====
var _earningsLoaded=false,_ratingLoaded=false; async function initEarningsRatingTabs(){
var section=document.getElementById('gtd-earnings-rating-section');
if(!section)return; var earningsOk=false,ratingOk=false;
try{
var d=await api('GET','/api/tutor/my-earnings');
earningsOk=true;
populateEarnings(d);_earningsLoaded=true;
}catch(e){/* earnings not available */}
try{
var d2=await api('GET','/api/tutor/my-rating');
ratingOk=true;
populateRating(d2);_ratingLoaded=true;
}catch(e){/* rating not available */} if(earningsOk||ratingOk){
section.style.display='';
var tabNav=document.getElementById('gtd-tab-nav');
if(tabNav){
var tabs=tabNav.querySelectorAll('.gtd-tab-btn');
tabs.forEach(function(t){
if(t.dataset.tab==='earnings'&&!earningsOk)t.style.display='none';
if(t.dataset.tab==='rating'&&!ratingOk)t.style.display='none';
});
var visibleTabs=Array.from(tabs).filter(function(t){return t.style.display!=='none'});
if(visibleTabs.length===1){
tabs.forEach(function(t){t.classList.remove('active')});
visibleTabs[0].classList.add('active');
var tabId=visibleTabs[0].dataset.tab;
document.getElementById('gtd-tab-earnings').classList.toggle('active',tabId==='earnings');
document.getElementById('gtd-tab-rating').classList.toggle('active',tabId==='rating');
}
}
} document.querySelectorAll('#gtd-tab-nav .gtd-tab-btn').forEach(function(btn){
btn.addEventListener('click',function(){
document.querySelectorAll('#gtd-tab-nav .gtd-tab-btn').forEach(function(b){b.classList.remove('active')});
this.classList.add('active');
var tab=this.dataset.tab;
document.getElementById('gtd-tab-earnings').classList.toggle('active',tab==='earnings');
document.getElementById('gtd-tab-rating').classList.toggle('active',tab==='rating');
});
});
} function populateEarnings(data){
var s=data.summary||{};
var el;
el=document.getElementById('gtd-earn-total');if(el)el.textContent='$'+formatNum(s.totalEarned||0);
el=document.getElementById('gtd-earn-cases');if(el)el.textContent=s.successCases||0;
el=document.getElementById('gtd-earn-avg');if(el)el.textContent='$'+formatNum(s.avgPerCase||0); var tbody=document.getElementById('gtd-earn-tbody');
if(!tbody)return;
var payments=data.payments||[];
if(!payments.length){
tbody.innerHTML=' ';
return;
}
var h='';
for(var i=0;i ';
h+=' ';
h+=' ';
h+=' ';
h+=' ';
h+='
';
}
tbody.innerHTML=h;
} function populateRating(data){
var el;
var hasRating=data.avgScore!==null&&data.avgScore!==undefined&&data.avgScore>0;
el=document.getElementById('gtd-rate-avg');
if(el)el.textContent=hasRating?data.avgScore.toFixed(1):'暫未有評分';
if(el&&!hasRating)el.style.fontSize='18px';
var starsEl=document.getElementById('gtd-rate-stars');
if(starsEl&&hasRating){
var h='';
for(var i=1;i<=5;i++){
if(data.avgScore>=i)h+='★';
else if(data.avgScore>=i-0.5)h+='★';
else h+='★';
}
starsEl.innerHTML=h;
}
if(starsEl&&!hasRating){starsEl.innerHTML=''}
el=document.getElementById('gtd-rate-count');
if(el)el.textContent=hasRating?(data.scoreCount||0)+' 個評分':'未有家長評分';
el=document.getElementById('gtd-rate-success');
if(el)el.textContent=data.successRate!==null&&data.successRate!==undefined?data.successRate+'%':'--';
el=document.getElementById('gtd-rate-total');
if(el)el.textContent=data.totalCases||'--';
el=document.getElementById('gtd-rate-success-count');
if(el)el.textContent=data.successCount||'--';
var pctSection=document.getElementById('gtd-rate-percentile');
var pctNum=document.getElementById('gtd-rate-pct-num');
if(pctSection&&pctNum&&data.percentile!==null&&data.percentile!==undefined){
pctSection.style.display='';
pctNum.textContent='Top '+(100-data.percentile)+'%';
}
} function formatNum(n){
if(n===0||n===null||n===undefined)return '0';
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,',');
} function formatDate(d){
try{
var dt=new Date(d);
if(isNaN(dt.getTime()))return '--';
var y=dt.getFullYear(),m=String(dt.getMonth()+1).padStart(2,'0'),day=String(dt.getDate()).padStart(2,'0');
return y+'-'+m+'-'+day;
}catch(e){return '--'}
} // ===== Referral Program (Sprint 4) =====
async function initReferralSection(){
var section=document.getElementById('gtd-referral-section');
if(!section)return;
try{
var codeData=await api('GET','/referral-code');
if(!codeData||!codeData.code){return} var linkInput=document.getElementById('gtd-ref-link');
if(linkInput)linkInput.value=codeData.link||'';
var copyBtn=document.getElementById('gtd-ref-copy');
if(copyBtn){
copyBtn.addEventListener('click',function(){
if(linkInput&&linkInput.value){
navigator.clipboard.writeText(linkInput.value).then(function(){
copyBtn.textContent='已複製!';setTimeout(function(){copyBtn.textContent='複製連結'},2000);
}).catch(function(){
linkInput.select();document.execCommand('copy');
copyBtn.textContent='已複製!';setTimeout(function(){copyBtn.textContent='複製連結'},2000);
});
}
});
}
var waBtn=document.getElementById('gtd-ref-wa');
if(waBtn&&codeData.whatsappUrl)waBtn.href=codeData.whatsappUrl;
try{
var stats=await api('GET','/referral-stats');
if(stats&&stats.stats){
var s=stats.stats;
var totalEl=document.getElementById('gtd-ref-total');
var regEl=document.getElementById('gtd-ref-registered');
var compEl=document.getElementById('gtd-ref-completed');
if(totalEl)totalEl.textContent=s.total||0;
if(regEl)regEl.textContent=s.registered||0;
if(compEl)compEl.textContent=s.completed||0;
var bonusEl=document.getElementById('gtd-ref-bonus');
var bonusText=document.getElementById('gtd-ref-bonus-text');
if(bonusEl&&bonusText&&(s.earnedBonus>0||s.pendingBonus>0)){
bonusEl.style.display='';
var parts=[];
if(s.earnedBonus>0)parts.push('已賺取: $'+s.earnedBonus);
if(s.pendingBonus>0)parts.push('待發放: $'+s.pendingBonus);
bonusText.textContent=parts.join(' / ');
}
var historyEl=document.getElementById('gtd-ref-history');
if(historyEl&&stats.referrals&&stats.referrals.length>0){
var html='';
stats.referrals.forEach(function(r){
var iconClass='gtd-ref-icon-pending',iconText='⌛',badgeClass='gtd-ref-badge-pending',badgeText='等待註冊';
if(r.status==='registered'){iconClass='gtd-ref-icon-registered';iconText='✓';badgeClass='gtd-ref-badge-registered';badgeText='已註冊'}
else if(r.status==='first_deal'){iconClass='gtd-ref-icon-done';iconText='$';badgeClass='gtd-ref-badge-done';badgeText='已接單 +$'+(r.bonusAmount||200)}
var dateStr=r.createdAt?new Date(r.createdAt).toLocaleDateString('zh-HK',{year:'numeric',month:'short',day:'numeric'}):'';
html+=' ';
html+=' ';
html+=''+badgeText+'';
html+=' ';
});
historyEl.innerHTML=html;
}
}
}catch(e2){console.warn('[Referral] Stats:',e2.message)}
}catch(e){console.warn('[Referral] Load failed:',e.message)}
} // ===== Progressive Commission (Feature 21) =====
async function initCommissionSection(){
var section=document.getElementById('gtd-commission-section');
if(!section)return;
try{
var opts={method:'GET',headers:{}};
if(token)opts.headers['Authorization']='Bearer '+token;
var r=await fetch(API+'/commission-info',opts);
if(!r.ok){section.style.display='none';return}
var d=await r.json();
section.style.display='';
populateCommission(d);
}catch(e){section.style.display='none'}
} function populateCommission(d){
var rate=d.currentRate!=null?d.currentRate:30;
var hero=document.getElementById('gtd-comm-hero');
if(hero){
hero.className='gtd-comm-hero';
if(rate===0)hero.classList.add('rate-zero');
else if(rate<=10)hero.classList.add('rate-low');
else if(rate<=20)hero.classList.add('rate-mid');
else hero.classList.add('rate-high');
}
var rateEl=document.getElementById('gtd-comm-rate');
if(rateEl)rateEl.textContent=rate;
var monthEl=document.getElementById('gtd-comm-month');
if(monthEl)monthEl.textContent=(d.monthLabel||'')+' 你而家嘅佣金率';
var revEl=document.getElementById('gtd-comm-revenue');
if(revEl)revEl.textContent='$'+formatNum(d.currentRevenue||0);
var amtEl=document.getElementById('gtd-comm-amount');
if(amtEl)amtEl.textContent='$'+formatNum(d.commissionAmount||0);
// Progress bar
var progressEl=document.getElementById('gtd-comm-progress');
if(progressEl)progressEl.style.width=(d.progressPercent||0)+'%';
var nextEl=document.getElementById('gtd-comm-next-text');
var progressWrap=document.getElementById('gtd-comm-progress-wrap');
if(rate===0){
if(nextEl)nextEl.textContent='恭喜!你已達到 0% 佣金';
if(progressWrap)progressWrap.style.display='none';
}else if(d.amountToNextTier>0){
if(nextEl)nextEl.textContent='再多 $'+formatNum(d.amountToNextTier)+' 收入就降到 '+d.nextTierRate+'%';
} // GT keep percentage
var keepEl=document.getElementById('gtd-comm-gt-keep');
if(keepEl)keepEl.textContent=(100-rate)+'%'; // Tier table
var tbody=document.getElementById('gtd-comm-tier-tbody');
if(tbody&&d.tierTable){
var h='';
for(var i=0;i ';
h+=' ';
h+=' ';
h+='
';
}
tbody.innerHTML=h;
} // Toggle tier table
var toggleBtn=document.getElementById('gtd-comm-tiers-btn');
var tableWrap=document.getElementById('gtd-comm-tier-table');
if(toggleBtn&&tableWrap){
toggleBtn.addEventListener('click',function(){
var isOpen=tableWrap.classList.toggle('open');
toggleBtn.classList.toggle('open',isOpen);
});
}
} // ===== Teaching Tips (Feature 19) =====
async function initTipsSection(){
var section=document.getElementById('gtd-tips-section');
if(!section)return;
var contentEl=document.getElementById('gtd-tips-content');
if(!contentEl)return;
// Try loading tips — if 403, feature disabled, keep "coming soon" message
try{
var myTips=await api('GET','/my-tips');
// Feature is enabled — render the form + list
var subjectOpts='';
var gradeOpts='';
var html=' 你嘅教學經驗可以幫到其他老師同學生!文章會放上 GETUTOR 網站,標明你嘅名同資歷。 ';
html+=' ';
html+='';
html+='';
html+=' ';
html+='';
html+=' ';
html+=' ';
contentEl.innerHTML=html; // Character count
var textarea=document.getElementById('gtd-tip-content');
var charcount=document.getElementById('gtd-tip-charcount');
if(textarea&&charcount){
textarea.addEventListener('input',function(){
var len=textarea.value.length;
charcount.textContent=len+' / 2000';
charcount.className='gtd-tips-charcount'+(len>1800?' warn':'')+(len>2000?' over':'');
});
} // Submit handler
var submitBtn=document.getElementById('gtd-tip-submit');
if(submitBtn){
submitBtn.addEventListener('click',async function(){
var subj=document.getElementById('gtd-tip-subject').value;
var grade=document.getElementById('gtd-tip-grade').value;
var title=document.getElementById('gtd-tip-title').value;
var content=document.getElementById('gtd-tip-content').value;
if(!subj){toast('請選擇科目','error');return}
if(!title.trim()){toast('請輸入標題','error');return}
if(content.trim().length<50){toast('內容最少 50 個字','error');return}
submitBtn.disabled=true;submitBtn.textContent='提交中...';
try{
await api('POST','/submit-tip',{subject:subj,gradeLevel:grade,title:title,content:content});
toast('提交成功!文章審核後會發佈到網站','success');
document.getElementById('gtd-tip-title').value='';
document.getElementById('gtd-tip-content').value='';
document.getElementById('gtd-tip-subject').selectedIndex=0;
document.getElementById('gtd-tip-grade').selectedIndex=0;
if(charcount)charcount.textContent='0 / 2000';
loadMyTips();
}catch(e){toast(e.message||'提交失敗','error')}
finally{submitBtn.disabled=false;submitBtn.textContent='提交心得'}
});
}
// Render existing tips
renderMyTips(myTips.tips||[]);
}catch(e){
// Feature disabled or error — show "coming soon"
console.warn('[Tips]',e.message);
}
}
async function loadMyTips(){
try{
var data=await api('GET','/my-tips');
renderMyTips(data.tips||[]);
}catch(e){console.warn('[Tips] Load:',e.message)}
}
function renderMyTips(tips){
var container=document.getElementById('gtd-tips-items');
if(!container)return;
if(!tips||tips.length===0){container.innerHTML='暫未提交過文章';return}
var html='';
tips.sort(function(a,b){return new Date(b.submittedAt)-new Date(a.submittedAt)});
tips.forEach(function(t){
var iconClass='gtd-tip-icon-pending',iconText='⌛',badgeClass='gtd-tip-badge-pending',badgeText='待審核';
if(t.status==='approved'){iconClass='gtd-tip-icon-approved';iconText='✓';badgeClass='gtd-tip-badge-approved';badgeText='已批准'}
else if(t.status==='published'){iconClass='gtd-tip-icon-published';iconText='★';badgeClass='gtd-tip-badge-published';badgeText='已發佈'}
var dateStr=t.submittedAt?new Date(t.submittedAt).toLocaleDateString('zh-HK',{year:'numeric',month:'short',day:'numeric'}):'';
html+=' ';
html+=' ';
html+=''+badgeText+'';
html+=' ';
});
container.innerHTML=html;
} // ===== Dashboard Tabs =====
var _cjbLoaded=false;var _cjbApplied={};
document.querySelectorAll('.gtd-tab').forEach(function(tab){
tab.addEventListener('click',function(){
document.querySelectorAll('.gtd-tab').forEach(function(t){t.classList.remove('active')});
document.querySelectorAll('#gtd-panel-profile,#gtd-panel-cases').forEach(function(p){p.classList.remove('active')});
this.classList.add('active');
var panel=document.getElementById('gtd-panel-'+this.dataset.tab);
if(panel)panel.classList.add('active');
if(this.dataset.tab==='cases'&&!_cjbLoaded){_cjbLoaded=true;loadAvailableCases();loadMyApplications()}
});
});
function formatBudget(c){if(c.budgetMin&&c.budgetMax)return '$'+c.budgetMin+'-$'+c.budgetMax+'/hr';if(c.budgetMax)return '$'+c.budgetMax+'/hr';if(c.budgetRaw)return c.budgetRaw;return '面議'}
function timeAgo(d){var ms=Date.now()-new Date(d).getTime();var hrs=Math.floor(ms/3600000);if(hrs<1)return '剛剛發佈';if(hrs<24)return hrs+'小時前';return Math.floor(hrs/24)+'日前'}
function renderCaseCard(c){
var isApplied=!!_cjbApplied[c.id];var newBadge=c.isNew?'NEW':'';
return ' '
+' '
+' ';
}
async function loadAvailableCases(){
var list=document.getElementById('gtd-cjb-list');var totalEl=document.getElementById('gtd-cjb-total');
list.innerHTML=' ';
var subj=document.getElementById('gtd-cjb-subject').value;var dist=document.getElementById('gtd-cjb-district').value;var grade=document.getElementById('gtd-cjb-grade').value;
var qs='?';if(subj)qs+='subject='+encodeURIComponent(subj)+'&';if(dist)qs+='district='+encodeURIComponent(dist)+'&';if(grade)qs+='grade='+encodeURIComponent(grade)+'&';
try{
var r=await api('GET','/available-cases'+qs);if(totalEl)totalEl.textContent=r.total||0;
if(!r.cases||r.cases.length===0){list.innerHTML=' ';return}
list.innerHTML=r.cases.map(renderCaseCard).join('');
list.querySelectorAll('.gtd-cjb-apply:not(.applied)').forEach(function(btn){btn.addEventListener('click',function(){applyCase(this)})});
}catch(e){
if(e.message&&(e.message.includes('即將推出')||e.message.includes('尚未開放'))){list.innerHTML=' '}
else if(e.message&&e.message.includes('網絡錯誤')){list.innerHTML=' '}
else{list.innerHTML=' '}
}
}
async function applyCase(btn){
var caseId=btn.dataset.caseid;if(!caseId||btn.disabled)return;btn.disabled=true;btn.textContent='申請中...';
try{await api('POST','/apply-case',{caseId:caseId});_cjbApplied[caseId]=true;btn.className='gtd-cjb-apply applied';btn.innerHTML='✓ 已申請';toast('申請成功!如果配對適合,我哋會主動聯絡你。','success');loadMyApplications()}
catch(e){btn.disabled=false;btn.textContent='我有興趣';toast(e.message||'申請失敗,請稍後再試','error')}
}
async function loadMyApplications(){
var list=document.getElementById('gtd-myapps-list');var totalEl=document.getElementById('gtd-myapps-total');
list.innerHTML=' 載入中... ';
try{
var r=await api('GET','/my-applications');if(totalEl)totalEl.textContent=r.total||0;
if(!r.applications||r.applications.length===0){list.innerHTML=' 暫未有申請記錄 ';return}
r.applications.forEach(function(a){_cjbApplied[a.caseId]=true});
var sm={pending:'等待中',reviewed:'已查看',matched:'已配對',rejected:'未配對'};
var sc={pending:'#94a3b8',reviewed:'#3b82f6',matched:'#059669',rejected:'#dc2626'};
list.innerHTML=r.applications.map(function(a){
var st=sm[a.status]||a.status;var color=sc[a.status]||'#94a3b8';
return ' '
+''+st+' ';
}).join('');
}catch(e){list.innerHTML=' 暫未有申請記錄 '}
}
['gtd-cjb-subject','gtd-cjb-district','gtd-cjb-grade'].forEach(function(id){var el=document.getElementById(id);if(el)el.addEventListener('change',function(){loadAvailableCases()})}); // ===== Parent Review Invitations (Feature 20) =====
async function initReviewSection(){
var section=document.getElementById('gtd-review-section');
if(!section)return;
var disabledEl=document.getElementById('gtd-rev-disabled');
var activeEl=document.getElementById('gtd-rev-active');
try{
// Test if feature is enabled by loading reviews
var data=await api('GET','/my-reviews');
// Feature is enabled — show form
disabledEl.style.display='none';
activeEl.style.display=''; // Populate review history
if(data&&data.reviews&&data.reviews.length>0){
var listEl=document.getElementById('gtd-rev-list');
var historyEl=document.getElementById('gtd-rev-history');
if(listEl)listEl.style.display='';
var html='';
data.reviews.forEach(function(r){
var iconClass='gtd-rev-icon-invited',iconText='✉',badgeClass='gtd-rev-badge-invited',badgeText='已邀請';
if(r.status==='completed'){iconClass='gtd-rev-icon-completed';iconText='✓';badgeClass='gtd-rev-badge-completed';badgeText='已評價'}
else if(r.status==='published'){iconClass='gtd-rev-icon-published';iconText='⭐';badgeClass='gtd-rev-badge-published';badgeText='已發佈'}
var maskedName=r.parentName?(r.parentName.charAt(0)+'**'):'--';
var subjectText=r.subject?(' · '+esc(r.subject)):'';
var dateStr=r.invitedAt?new Date(r.invitedAt).toLocaleDateString('zh-HK',{year:'numeric',month:'short',day:'numeric'}):'';
html+=' ';
html+=' ';
html+=''+badgeText+'';
html+=' ';
});
if(historyEl)historyEl.innerHTML=html;
} // Submit form
var submitBtn=document.getElementById('gtd-rev-submit');
if(submitBtn){
submitBtn.addEventListener('click',async function(){
var parentName=document.getElementById('gtd-rev-parent-name').value.trim();
var parentPhone=document.getElementById('gtd-rev-parent-phone').value.trim();
var subject=document.getElementById('gtd-rev-subject').value;
var message=document.getElementById('gtd-rev-message').value.trim();
if(!parentName){toast('請輸入家長姓名','error');return}
if(!/^\d{8}$/.test(parentPhone)){toast('請輸入 8 位數字電話號碼','error');return}
submitBtn.disabled=true;submitBtn.textContent='發送中...';
try{
var result=await api('POST','/request-review',{parentPhone:parentPhone,parentName:parentName,subject:subject,message:message});
if(result&&result.reviewLink){
// Show result
var resultEl=document.getElementById('gtd-rev-result');
var linkInput=document.getElementById('gtd-rev-link');
if(linkInput)linkInput.value=result.reviewLink;
if(resultEl)resultEl.style.display='';
// WhatsApp share
var waBtn=document.getElementById('gtd-rev-wa');
if(waBtn){
var waMsg=encodeURIComponent(parentName+' 你好!多謝你揀 GETUTOR 搵補習老師。如果你對我嘅教學滿意,希望你可以花 1 分鐘寫個簡短評價 🙏 '+result.reviewLink);
waBtn.href='https://wa.me/852'+parentPhone+'?text='+waMsg;
}
// Copy button
var copyBtn=document.getElementById('gtd-rev-copy');
if(copyBtn){
copyBtn.onclick=function(){
navigator.clipboard.writeText(result.reviewLink).then(function(){
copyBtn.textContent='已複製!';setTimeout(function(){copyBtn.textContent='複製連結'},2000);
}).catch(function(){
linkInput.select();document.execCommand('copy');
copyBtn.textContent='已複製!';setTimeout(function(){copyBtn.textContent='複製連結'},2000);
});
};
}
// Reset form
document.getElementById('gtd-rev-parent-name').value='';
document.getElementById('gtd-rev-parent-phone').value='';
document.getElementById('gtd-rev-subject').value='';
document.getElementById('gtd-rev-message').value='';
toast('邀請已成功發送!','success');
// Refresh list
try{initReviewSection()}catch(e){}
}
}catch(e){toast(e.message||'發送邀請失敗','error')}
finally{submitBtn.disabled=false;submitBtn.textContent='發送邀請'}
});
}
}catch(e){
// Feature disabled or error — show disabled message
if(e.message&&(e.message.includes('即將推出')||e.message.includes('尚未開放'))){
disabledEl.style.display='';
activeEl.style.display='none';
}else{
console.warn('[Reviews] Load failed:',e.message);
}
}
} // ===== Premium Plan (Sprint 5) =====
async function loadPremiumStatus(){
var section=document.getElementById('gtd-premium-section');
var content=document.getElementById('gtd-prem-content');
var badgeWrap=document.getElementById('gtd-prem-badge-wrap');
if(!section||!content) return;
try{
var d=await api('GET','/api/tutor/premium-status');
if(d.featureDisabled){section.style.display='none';return}
section.style.display='';
if(d.isPremium){renderPremiumActive(d,content,badgeWrap)}
else{renderPremiumCTA(d,content,badgeWrap)}
}catch(e){
console.warn('[Premium] Load failed:',e.message);
if(content)content.innerHTML=' ';
if(section)section.style.display='';
}
} function renderPremiumActive(d,content,badgeWrap){
var section=document.getElementById('gtd-premium-section');
if(section)section.classList.add('is-premium');
badgeWrap.innerHTML=' PREMIUM';
var endDate=new Date(d.endDate);
var endStr=endDate.getFullYear()+'-'+(endDate.getMonth()+1).toString().padStart(2,'0')+'-'+endDate.getDate().toString().padStart(2,'0');
content.innerHTML=' '
+' '
+' '
+' '
+' '
+' ';
var navName=document.getElementById('gtd-nav-name');
if(navName&&!document.getElementById('gtd-nav-prem-badge')){
navName.insertAdjacentHTML('afterend',' ');
}
} function renderPremiumCTA(d,content,badgeWrap){
var section=document.getElementById('gtd-premium-section');
if(section)section.classList.remove('is-premium');
badgeWrap.innerHTML='';
var pendingHtml=d.hasPendingRequest?' ':'';
content.innerHTML=pendingHtml
+' '
+' Premium 老師平均配對率高 2 倍,中介費減半,新 Case 即時收到。 '
+' '
+' '
+' '
+' '
+' '
+' '
+' '
+(d.hasPendingRequest?'':'')
+' ';
var applyBtn=document.getElementById('gtd-prem-apply-btn');
if(applyBtn){
applyBtn.addEventListener('click',async function(){
this.disabled=true;this.textContent='提交中...';
try{
var pj=await api('POST','/api/tutor/premium-request',{});
if(pj.success){this.textContent='申請已提交,等緊審批中';this.style.background='var(--text4)';this.style.cursor='not-allowed';toast('Premium 申請已提交,我哋會盡快處理!','success');loadPremiumStatus()}
else{toast(pj.error||'申請失敗','error');this.disabled=false;this.innerHTML=' 申請 Premium'}
}catch(e){toast('網絡錯誤,請稍後再試','error');this.disabled=false;this.innerHTML=' 申請 Premium'}
});
}
} // ===== Init =====
// Hide dashboard content by default until auth verified (prevent flash)
if(C)C.style.display='none';
ssoCheck().then(function(){ checkAuth() }); })();
';
else avatar.textContent=name?name.charAt(0):'?';
}
var ppName=C.querySelector('.gtd-pp-name');if(ppName)ppName.textContent=p.personal.chineseName||p.personal.englishName||'';
var ppSub=C.querySelector('.gtd-pp-sub');if(ppSub)ppSub.textContent=(p.education.school||'')+(p.education.school&&p.education.occupation?' · ':'')+(p.education.occupation||'');
// Badges - top DSE results
var badges=C.querySelector('.gtd-pp-badges');
if(badges){
badges.innerHTML='';
var dse=p.exams.dse||{};
Object.entries(dse).sort(function(a,b){var order={'5**':0,'5*':1,'5':2,'4':3};return(order[a[1]]||9)-(order[b[1]]||9)}).slice(0,3).forEach(function(e){
badges.innerHTML+=''+esc(e[0])+' '+esc(e[1])+'';
});
}
var ppIntro=C.querySelector('.gtd-pp-intro');if(ppIntro)ppIntro.textContent=p.selfIntro||'';
}
';
if(st){st.textContent='✓ 已上傳';st.style.color='var(--success)'}
toast('照片已上傳','success');
// Update preview avatar too
var ppAvatar=C.querySelector('.gtd-pp-avatar');
if(ppAvatar)ppAvatar.innerHTML='
';
}catch(e){if(st){st.textContent='✕ 上傳失敗';st.style.color='var(--error)'}toast('照片上傳失敗','error')}
});
你暫時未有收入紀錄
'+esc(dateStr)+'
'+esc(p.subject||p.caseId||'--')+'
$'+formatNum(p.totalAmount||0)+'
$'+formatNum(p.collected||0)+'
'+statusText+'
'+rangeStr+'
'+t.rate+'%
'+(100-t.rate)+'%
我的文章
升級 Premium,搵更多學生
'
+'
功能
免費
Premium
睇到新 Case
24 小時後
即時 (T=0)
中介費
2 堂
1 堂
Profile 排序
普通
置頂
Case 進度通知
基本
即時 + 詳細
或馬上提交補習個案🥰