@@ -341,10 +341,10 @@ <h2>π’ Post Job</h2>
341341 < div class ="proctor-cam hidden " id ="proctorCam "> < video id ="proctorVideo " autoplay muted playsinline > </ video > < div class ="cam-label "> π΄ REC</ div > </ div >
342342
343343< script >
344- const W = "https://solitary-sound-11b9.simpaticohrconsultancy.workers.dev/ " ;
344+ const W = "https://solitary-sound-11b9.simpaticohrconsultancy.workers.dev" ;
345345const SILENCE_TIMEOUT = 3.0 , MIN_ANS = 10 , MAX_RETRIES = 3 , MAX_VIOLATIONS = 3 ;
346- const HR_PASSWORD = "simpatico2025" ;
347346let hrAuth = false ;
347+ let curToken = null ; // stores token for real-mode interview update
348348let mode = "mock" , cv = "" , lang = "en" , role = "" , level = "" , totalQ = 10 ;
349349let mainQ = 0 , isFollowUp = false , chat = [ ] , answers = [ ] ;
350350let rec = null , transcript = "" , timerInt = null , secs = 0 ;
@@ -402,10 +402,7 @@ <h2>π’ Post Job</h2>
402402// βββββββββββββββββββββββββββββββββββββββ
403403function nav ( p ) {
404404 if ( p === 'hr' && ! hrAuth ) {
405- const pwd = prompt ( "π Enter HR Admin Password:" ) ;
406- if ( ! pwd ) return ;
407- if ( pwd !== HR_PASSWORD ) { toast ( "β Wrong password!" ) ; return }
408- hrAuth = true ; toast ( "β
HR Access granted!" ) ;
405+ showHRLogin ( ) ; return ;
409406 }
410407 const pages = [ 'home' , 'jobs' , 'apply' , 'setup' , 'live' , 'results' , 'hr' ] ;
411408 pages . forEach ( x => document . getElementById ( 'page-' + x ) ?. classList . add ( 'hidden' ) ) ;
@@ -417,6 +414,48 @@ <h2>π’ Post Job</h2>
417414 if ( p === 'hr' ) loadHR ( ) ;
418415}
419416
417+ function showHRLogin ( ) {
418+ let modal = document . getElementById ( 'hrLoginModal' ) ;
419+ if ( ! modal ) {
420+ modal = document . createElement ( 'div' ) ;
421+ modal . id = 'hrLoginModal' ;
422+ modal . style . cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.75);backdrop-filter:blur(10px);z-index:9000;display:flex;align-items:center;justify-content:center;padding:20px;' ;
423+ modal . innerHTML = `<div style="background:rgba(15,23,42,.95);border:1px solid rgba(255,255,255,.1);border-radius:20px;padding:32px;width:100%;max-width:380px;text-align:center;">
424+ <div style="font-size:2rem;margin-bottom:8px;">π</div>
425+ <h3 style="margin-bottom:6px;color:#f8fafc;">HR Admin Access</h3>
426+ <p style="color:#94a3b8;font-size:0.82rem;margin-bottom:20px;">Enter your HR admin password</p>
427+ <input type="password" id="hrPwdInput" placeholder="Password" style="width:100%;padding:12px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:10px;color:#f8fafc;font-size:0.95rem;outline:none;font-family:inherit;margin-bottom:8px;"
428+ onkeydown="if(event.key==='Enter')doHRLogin()">
429+ <div id="hrLoginErr" style="color:#ef4444;font-size:0.8rem;margin-bottom:8px;display:none;"></div>
430+ <button onclick="doHRLogin()" style="width:100%;padding:13px;background:linear-gradient(135deg,#6366f1,#8b5cf6);border:none;border-radius:12px;color:#fff;font-weight:700;cursor:pointer;font-size:0.95rem;font-family:inherit;margin-bottom:8px;">π Login</button>
431+ <button onclick="document.getElementById('hrLoginModal').style.display='none'" style="width:100%;padding:11px;background:transparent;border:1px solid rgba(255,255,255,.1);border-radius:12px;color:#94a3b8;font-weight:600;cursor:pointer;font-size:0.88rem;font-family:inherit;">Cancel</button>
432+ </div>` ;
433+ document . body . appendChild ( modal ) ;
434+ modal . addEventListener ( 'click' , e => { if ( e . target === modal ) modal . style . display = 'none' ; } ) ;
435+ }
436+ modal . style . display = 'flex' ;
437+ setTimeout ( ( ) => document . getElementById ( 'hrPwdInput' ) . focus ( ) , 100 ) ;
438+ }
439+
440+ function doHRLogin ( ) {
441+ const input = document . getElementById ( 'hrPwdInput' ) ;
442+ const err = document . getElementById ( 'hrLoginErr' ) ;
443+ const pwd = input . value . trim ( ) ;
444+ if ( ! pwd ) { err . textContent = 'Enter password' ; err . style . display = 'block' ; return ; }
445+ // Check against adminConfig (set by admin dashboard) or fallback default
446+ const cfg = JSON . parse ( localStorage . getItem ( 'adminConfig' ) || '{}' ) ;
447+ const correctPwd = cfg . hrPassword || 'simpatico2025' ;
448+ if ( pwd !== correctPwd ) {
449+ err . textContent = 'β Wrong password' ; err . style . display = 'block' ;
450+ input . value = '' ; input . focus ( ) ; return ;
451+ }
452+ hrAuth = true ;
453+ document . getElementById ( 'hrLoginModal' ) . style . display = 'none' ;
454+ input . value = '' ; err . style . display = 'none' ;
455+ toast ( 'β
HR Access granted!' ) ;
456+ nav ( 'hr' ) ;
457+ }
458+
420459// βββββββββββββββββββββββββββββββββββββββ
421460// JOBS
422461// βββββββββββββββββββββββββββββββββββββββ
@@ -431,12 +470,12 @@ <h2>π’ Post Job</h2>
431470 el . innerHTML = data . map ( j => `<div class="job-card">
432471 <h3>${ j . title || '' } </h3>
433472 <div class="job-meta">
434- <span>π’ ${ j [ "Company Name" ] || j . department || '' } </span>
473+ <span>π’ ${ j . company_name || j [ "Company Name" ] || j . department || '' } </span>
435474 <span>π ${ j . location || '' } ${ j . Country ?'(' + j . Country + ')' :'' } </span>
436475 <span>π ${ j . level || 'Mid-Level' } </span>
437476 </div>
438477 <p style="font-size:0.82rem;color:var(--muted);margin:6px 0;">${ ( j . description || '' ) . substring ( 0 , 180 ) } </p>
439- <div>${ ( j . skills || [ ] ) . map ( s => '<span class="skill-tag">' + s + '</span>' ) . join ( '' ) } </div>
478+ <div>${ ( Array . isArray ( j . skills ) ? j . skills : typeof j . skills === 'string' && j . skills ? j . skills . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) : [ ] ) . map ( s => '<span class="skill-tag">' + s + '</span>' ) . join ( '' ) } </div>
440479 <button class="btn btn-go" style="margin-top:10px;padding:10px;"
441480 onclick="applyTo('${ j . id } ','${ ( j . title || '' ) . replace ( / ' / g, "\\'" ) } ')">π Apply</button>
442481 </div>` ) . join ( '' ) ;
@@ -541,7 +580,7 @@ <h3>${j.title||''}</h3>
541580 else if ( ext === 'pdf' ) t = await exPDF ( f ) ;
542581 else if ( ext === 'doc' || ext === 'docx' ) t = await exDOC ( f ) ;
543582 else t = await f . text ( ) ;
544- return t . replace ( / \s + / g, ' ' ) . replace ( / [ ^ \x20 - \x7E \n ] / g, '' ) . trim ( ) . substring ( 0 , 5000 ) ;
583+ return t . replace ( / [ \x00 - \x08 \x0B \x0C \x0E - \x1F \x7F ] / g, '' ) . replace ( / \s + / g, ' ' ) . trim ( ) . substring ( 0 , 5000 ) ;
545584}
546585
547586async function exPDF ( f ) {
@@ -572,8 +611,10 @@ <h3>${j.title||''}</h3>
572611async function exFB ( f ) {
573612 const b = await f . arrayBuffer ( ) ;
574613 const t = new TextDecoder ( 'utf-8' , { fatal :false } ) . decode ( b ) ;
575- const r = t . match ( / [ a - z A - Z 0 - 9 @ . , : ; ! ? \- / ( ) \s ] { 5 , } / g) ;
576- return r ?r . filter ( s => ( s . match ( / [ a - z A - Z ] / g) || [ ] ) . length / s . length > 0.5 && s . trim ( ) . length > 5 ) . join ( ' ' ) :'' ;
614+ // Only strip control characters, keep unicode (for Indian language CVs)
615+ const clean = t . replace ( / [ \x00 - \x08 \x0B \x0C \x0E - \x1F \x7F ] / g, '' ) ;
616+ const r = clean . match ( / [ \w \u0900 - \u097F \u0D00 - \u0D7F \u0600 - \u06FF \u0400 - \u04FF @ . , : ; ! ? \- / ( ) \s ] { 5 , } / g) ;
617+ return r ?r . filter ( s => s . trim ( ) . length > 5 ) . join ( ' ' ) :'' ;
577618}
578619
579620document . getElementById ( 'cvFile' ) . addEventListener ( 'change' , async e => {
@@ -595,7 +636,7 @@ <h3>${j.title||''}</h3>
595636 const ctrl = new AbortController ( ) ;
596637 const tm = setTimeout ( ( ) => ctrl . abort ( ) , 45000 ) ;
597638 try {
598- const r = await fetch ( W , { method :"POST" , headers :{ "Content-Type" :"application/json" } ,
639+ const r = await fetch ( W + "/" , { method :"POST" , headers :{ "Content-Type" :"application/json" } ,
599640 body :JSON . stringify ( { messages} ) , signal :ctrl . signal } ) ;
600641 clearTimeout ( tm ) ;
601642 if ( ! r . ok ) throw new Error ( 'Error ' + r . status ) ;
@@ -1008,6 +1049,7 @@ <h3>${j.title||''}</h3>
10081049
10091050 curAppId = interview . application_id ;
10101051 curJobId = interview . job_id ;
1052+ curToken = token ; // β
Store for saveResults() UPDATE
10111053
10121054 await db ( 'update' , 'interviews' , {
10131055 status :'in_progress' , started_at :new Date ( ) . toISOString ( )
@@ -1177,10 +1219,7 @@ <h3>${j.title||''}</h3>
11771219
11781220async function saveResults ( r ) {
11791221 try {
1180- if ( ! curAppId ) return ;
1181- const token = crypto . randomUUID ?crypto . randomUUID ( ) :Date . now ( ) . toString ( 36 ) ;
1182- await db ( 'insert' , 'interviews' , {
1183- application_id :curAppId , job_id :curJobId , token,
1222+ const resultData = {
11841223 interview_type :mode , status :'completed' ,
11851224 interview_role :role , interview_level :level , interview_language :lang ,
11861225 question_count :totalQ ,
@@ -1191,8 +1230,23 @@ <h3>${j.title||''}</h3>
11911230 strengths :r . strengths || [ ] , improvements :r . improvements || [ ] ,
11921231 feedback :r . feedback || [ ] , answers :answers ,
11931232 violation_log :violations , violation_count :violations . length
1194- } ) ;
1195- await db ( 'update' , 'applications' , { status :'interviewed' } , { id :curAppId } ) ;
1233+ } ;
1234+
1235+ if ( mode === 'real' && curToken ) {
1236+ // UPDATE the existing token-linked interview row (do NOT create duplicate)
1237+ await db ( 'update' , 'interviews' , resultData , { token :curToken } ) ;
1238+ } else {
1239+ // Mock mode β insert a new result row if we have an app context, else skip
1240+ if ( curAppId ) {
1241+ const newToken = crypto . randomUUID ?crypto . randomUUID ( ) :Date . now ( ) . toString ( 36 ) ;
1242+ await db ( 'insert' , 'interviews' , {
1243+ ...resultData ,
1244+ application_id :curAppId , job_id :curJobId , token :newToken
1245+ } ) ;
1246+ }
1247+ }
1248+
1249+ if ( curAppId ) await db ( 'update' , 'applications' , { status :'interviewed' } , { id :curAppId } ) ;
11961250 console . log ( '[DB] Results saved!' ) ;
11971251 } catch ( e ) { console . error ( '[DB]' , e ) }
11981252}
@@ -1329,14 +1383,15 @@ <h3>${j.title}</h3>
13291383 title,
13301384 category :document . getElementById ( 'hrCat' ) . value ,
13311385 department :document . getElementById ( 'hrDept' ) . value . trim ( ) ,
1386+ company_name :document . getElementById ( 'hrCompany' ) . value . trim ( ) ,
13321387 "Company Name" :document . getElementById ( 'hrCompany' ) . value . trim ( ) ,
13331388 location :document . getElementById ( 'hrLocation' ) . value . trim ( ) ,
13341389 level :document . getElementById ( 'hrLevel' ) . value ,
13351390 description :document . getElementById ( 'hrJD' ) . value . trim ( ) ,
13361391 skills :document . getElementById ( 'hrSkills' ) . value . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ,
13371392 question_count :parseInt ( document . getElementById ( 'hrQCount' ) . value ) ,
13381393 ats_threshold :parseInt ( document . getElementById ( 'hrATS' ) . value ) ,
1339- is_active :true , status :'active'
1394+ is_active :true , status :'active' , created_at : new Date ( ) . toISOString ( )
13401395 } ) ;
13411396 toast ( 'β
Posted!' ) ;
13421397 document . getElementById ( 'hrTitle' ) . value = '' ;
@@ -1411,13 +1466,47 @@ <h3>${j.title}</h3>
14111466 expires_at :new Date ( Date . now ( ) + 72 * 3600000 ) . toISOString ( )
14121467 } ) ;
14131468 const url = `${ location . origin } ${ location . pathname } ?token=${ token } ` ;
1469+
1470+ // Copy to clipboard
14141471 try { await navigator . clipboard . writeText ( url ) } catch ( e ) { }
1415- toast ( 'β
Link created!' ) ;
1416- alert ( `π§ Send to ${ email } :\n\n${ url } \n\n(Copied to clipboard!)` ) ;
1472+
1473+ // Show proper link modal instead of alert()
1474+ let lm = document . getElementById ( 'inviteLinkModal' ) ;
1475+ if ( ! lm ) {
1476+ lm = document . createElement ( 'div' ) ;
1477+ lm . id = 'inviteLinkModal' ;
1478+ lm . style . cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.75);backdrop-filter:blur(8px);z-index:9000;display:flex;align-items:center;justify-content:center;padding:20px;' ;
1479+ lm . innerHTML = `<div style="background:rgba(15,23,42,.95);border:1px solid rgba(255,255,255,.1);border-radius:20px;padding:28px;width:100%;max-width:460px;">
1480+ <h3 style="margin-bottom:6px;color:#f8fafc;">π§ Interview Invitation Ready</h3>
1481+ <p id="inviteEmailLabel" style="color:#94a3b8;font-size:0.82rem;margin-bottom:16px;"></p>
1482+ <label style="display:block;font-size:0.75rem;color:#94a3b8;margin-bottom:6px;font-weight:600;">INTERVIEW LINK (send this to the candidate)</label>
1483+ <div style="display:flex;gap:8px;">
1484+ <input type="text" id="inviteLinkInput" readonly style="flex:1;padding:10px;background:rgba(99,102,241,.08);border:1px solid rgba(99,102,241,.3);border-radius:10px;color:#a5b4fc;font-size:0.82rem;outline:none;font-family:monospace;">
1485+ <button onclick="copyInviteLink()" id="inviteCopyBtn" style="padding:10px 16px;background:#6366f1;border:none;border-radius:10px;color:#fff;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap;">π Copy</button>
1486+ </div>
1487+ <p style="margin-top:10px;font-size:0.75rem;color:#64748b;">β° Expires in 72 hours β’ Token is single-use</p>
1488+ <button onclick="document.getElementById('inviteLinkModal').style.display='none'" style="width:100%;padding:12px;margin-top:16px;background:transparent;border:1px solid rgba(255,255,255,.1);border-radius:12px;color:#94a3b8;font-weight:600;cursor:pointer;font-size:0.88rem;font-family:inherit;">Close</button>
1489+ </div>` ;
1490+ document . body . appendChild ( lm ) ;
1491+ lm . addEventListener ( 'click' , e => { if ( e . target === lm ) lm . style . display = 'none' ; } ) ;
1492+ }
1493+ document . getElementById ( 'inviteEmailLabel' ) . textContent = 'For: ' + email ;
1494+ document . getElementById ( 'inviteLinkInput' ) . value = url ;
1495+ lm . style . display = 'flex' ;
1496+
1497+ toast ( 'β
Invite link ready & copied!' ) ;
14171498 loadCands ( selHRJob , '' ) ;
14181499 } catch ( e ) { toast ( 'β ' + e . message ) }
14191500}
14201501
1502+ function copyInviteLink ( ) {
1503+ const input = document . getElementById ( 'inviteLinkInput' ) ;
1504+ const btn = document . getElementById ( 'inviteCopyBtn' ) ;
1505+ try { navigator . clipboard . writeText ( input . value ) } catch ( e ) { input . select ( ) ; document . execCommand ( 'copy' ) }
1506+ btn . textContent = 'β
Copied!' ;
1507+ setTimeout ( ( ) => { btn . textContent = 'π Copy' } , 2000 ) ;
1508+ }
1509+
14211510// βββββββββββββββββββββββββββββββββββββββ
14221511// TOKEN FROM URL
14231512// βββββββββββββββββββββββββββββββββββββββ
0 commit comments